diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index 50745252..9c437b51 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -3,6 +3,13 @@ Version history
This library adheres to `Semantic Versioning 2.0 `_.
+**UNRELEASED**
+
+- Fixed cancellation edge case on asyncio where a task spawning another with
+ ``TaskGroup.start()`` is not protected from external cancellation even when the
+ subtask has not yet called ``task_status.started()`` and is in a shielded cancel scope
+ (`#837 `_)
+
**4.7.0**
- Updated ``TaskGroup`` to work with asyncio's eager task factories
diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py
index 1f536940..3fe65930 100644
--- a/tests/test_taskgroups.py
+++ b/tests/test_taskgroups.py
@@ -18,6 +18,7 @@
from anyio import (
TASK_STATUS_IGNORED,
CancelScope,
+ Event,
create_task_group,
current_effective_deadline,
current_time,
@@ -1602,6 +1603,40 @@ async def in_task_group(task_status: TaskStatus[None]) -> None:
assert not tg.cancel_scope.cancel_called
+async def test_cancel_shielding_start() -> None:
+ """
+ Test that if the host task that has spawned a subtask via ``start()`` is cancelled,
+ a shielded cancel scope in the child task will shield it from cancellation.
+
+ Regression test for #837.
+
+ """
+
+ async def taskfunc(*, task_status: TaskStatus[None]) -> None:
+ with CancelScope(shield=True):
+ entered_inner_scope.set()
+ try:
+ await checkpoint()
+ await checkpoint()
+ except get_cancelled_exc_class():
+ pytest.fail("Shouldn't be cancelled in a shielded scope")
+
+ # The cancellation should be triggered here, and not any earlier
+ await checkpoint()
+
+ async def start_inner_task() -> None:
+ await inner_tg.start(taskfunc)
+
+ entered_inner_scope = Event()
+ async with (
+ create_task_group() as tg,
+ create_task_group() as inner_tg,
+ ):
+ tg.start_soon(start_inner_task)
+ await entered_inner_scope.wait()
+ tg.cancel_scope.cancel()
+
+
if sys.version_info <= (3, 11):
def no_other_refs() -> list[object]: