Skip to content

Commit 0d93f14

Browse files
committed
avoid error when coro.cr_frame is None
1 parent 84fbcb7 commit 0d93f14

File tree

3 files changed

+30
-2
lines changed

3 files changed

+30
-2
lines changed

newsfragments/3337.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a rare TOC/TOU bug in `trio.lowlevel.Task.iter_await_frames` where
2+
the underlying coroutine has already been closed.

src/trio/_core/_run.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,11 +1588,13 @@ def print_stack_for_task(task):
15881588
while coro is not None:
15891589
if hasattr(coro, "cr_frame"):
15901590
# A real coroutine
1591-
yield coro.cr_frame, coro.cr_frame.f_lineno
1591+
if cr_frame := coro.cr_frame: # TOCTOU-safe check for None
1592+
yield cr_frame, cr_frame.f_lineno
15921593
coro = coro.cr_await
15931594
elif hasattr(coro, "gi_frame"):
15941595
# A generator decorated with @types.coroutine
1595-
yield coro.gi_frame, coro.gi_frame.f_lineno
1596+
if gi_frame := coro.gi_frame: # TOCTOU-safe check for None
1597+
yield gi_frame, gi_frame.f_lineno
15961598
coro = coro.gi_yieldfrom
15971599
elif coro.__class__.__name__ in [
15981600
"async_generator_athrow",

src/trio/_core/_tests/test_run.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3005,3 +3005,27 @@ async def main() -> None:
30053005
await _core.wait_task_rescheduled(inner_abort)
30063006

30073007
_core.run(main)
3008+
3009+
3010+
async def test_closed_task_iter_await_frames() -> None:
3011+
"""Test that Task.__iter__ handles cr_frame/gi_frame becoming None.
3012+
3013+
When a coroutine completes, its cr_frame/gi_frame attributes become None.
3014+
The iterator should handle this gracefully rather than trying to access
3015+
attributes on None.
3016+
"""
3017+
completed_task = None
3018+
3019+
async with _core.open_nursery() as nursery:
3020+
3021+
async def capture_task() -> None:
3022+
nonlocal completed_task
3023+
completed_task = _core.current_task()
3024+
await _core.checkpoint()
3025+
3026+
nursery.start_soon(capture_task)
3027+
3028+
# Task has completed, so coro.cr_frame should be None, thus no frames
3029+
assert completed_task is not None
3030+
assert completed_task.coro.cr_frame is None
3031+
assert list(completed_task.iter_await_frames()) == []

0 commit comments

Comments
 (0)