Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,60 @@ async def foo():
self.loop.run_until_complete(foo())
self.loop.run_until_complete(asyncio.sleep(0.01))

def test_asyncgen_finalizer_hook_no_stack_capture(self):
# The finalizer hook can be invoked from a dealloc while the
# interpreter is mid-way through clearing a frame, where debug-mode
# source-traceback capture would walk a half-cleared frame and
# crash. Handles created while the suppression flag is set must not
# capture a traceback; normal handles must keep capturing.
if self.implementation != 'uvloop':
raise unittest.SkipTest('uvloop only')

from uvloop.loop import _extract_stack_disabled

self.loop.set_debug(True)
try:
handle = self.loop.call_soon(lambda: None)
self.assertIsNotNone(handle._source_traceback)
handle.cancel()

_extract_stack_disabled.flag = True
try:
handle = self.loop.call_soon(lambda: None)
self.assertIsNone(handle._source_traceback)
handle.cancel()
finally:
_extract_stack_disabled.flag = False
finally:
self.loop.set_debug(False)

def test_asyncgen_finalizer_hook_flag_reentrant(self):
# The hook must save/restore the suppression flag, not set/clear it:
# allocations inside the hook can trigger a GC that re-enters it on
# the same thread, and a clobbering reset would reopen the unsafe
# window for the outer invocation.
if self.implementation != 'uvloop':
raise unittest.SkipTest('uvloop only')

from uvloop.loop import _extract_stack_disabled

async def waiter():
yield 1

async def make_agen():
agen = waiter()
await agen.asend(None)
return agen

agen = self.loop.run_until_complete(make_agen())
_extract_stack_disabled.flag = True # simulate an outer invocation
try:
self.loop._asyncgen_finalizer_hook(agen)
self.assertTrue(_extract_stack_disabled.flag)
finally:
_extract_stack_disabled.flag = False
self.loop.run_until_complete(asyncio.sleep(0.01))

def test_inf_wait_for(self):
async def foo():
await asyncio.sleep(0.1)
Expand Down
17 changes: 17 additions & 0 deletions uvloop/cbhandles.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -416,10 +416,23 @@ cdef new_MethodHandle3(Loop loop, str name, method3_t callback, object context,
return handle


class _ExtractStackDisabled(threading_local):
# Per-thread: True while handles are created from the asyncgen finalizer
# hook, which a dealloc can invoke while CPython is mid-way through
# clearing the current frame; sys._getframe() would then dereference the
# half-cleared frame and crash the interpreter.
flag = False


_extract_stack_disabled = _ExtractStackDisabled()


cdef extract_stack():
"""Replacement for traceback.extract_stack() that only does the
necessary work for asyncio debug mode.
"""
if _extract_stack_disabled.flag:
return None
try:
f = sys_getframe()
# sys._getframe() might raise ValueError if being called without a frame, e.g.
Expand All @@ -433,6 +446,10 @@ cdef extract_stack():
stack = tb_StackSummary.extract(tb_walk_stack(f),
limit=DEBUG_STACK_DEPTH,
lookup_lines=False)
except (AttributeError, ValueError, TypeError):
# walk_stack can encounter non-frame objects, e.g. py3.14 surfaces
# _asyncio.TaskStepMethWrapper, which has no f_back (#715).
return None
finally:
f = None

Expand Down
1 change: 1 addition & 0 deletions uvloop/includes/stdlib.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ cdef int ssl_SSL_ERROR_WANT_WRITE = ssl.SSL_ERROR_WANT_WRITE
cdef int ssl_SSL_ERROR_SYSCALL = ssl.SSL_ERROR_SYSCALL

cdef threading_Thread = threading.Thread
cdef threading_local = threading.local
cdef threading_main_thread = threading.main_thread

cdef int subprocess_PIPE = subprocess.PIPE
Expand Down
11 changes: 10 additions & 1 deletion uvloop/loop.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3195,7 +3195,16 @@ cdef class Loop:
def _asyncgen_finalizer_hook(self, agen):
self._asyncgens.discard(agen)
if not self.is_closed():
self.call_soon_threadsafe(self.create_task, agen.aclose())
# Frame walking is unsafe in this hook; see
# _ExtractStackDisabled in cbhandles.pyx. Save/restore (not
# set/clear): an allocation below can trigger a GC that
# re-enters this hook on the same thread.
prev = _extract_stack_disabled.flag
_extract_stack_disabled.flag = True
try:
self.call_soon_threadsafe(self.create_task, agen.aclose())
finally:
_extract_stack_disabled.flag = prev

def _asyncgen_firstiter_hook(self, agen):
if self._asyncgens_shutdown_called:
Expand Down
Loading