Skip to content

Commit 7d99f9e

Browse files
David SacerdoteDavid Sacerdote
authored andcommitted
Fix debug-mode segfault when the asyncgen finalizer hook runs during frame teardown
The asyncgen finalizer hook can be invoked by a dealloc while CPython is mid-way through clearing the current interpreter frame (_PyEval_FrameClearAndPop -> PyObject_CallFinalizerFromDealloc -> _asyncgen_finalizer_hook). In debug mode the hook's call_soon_threadsafe creates a Handle whose source-traceback capture calls sys._getframe(), which dereferences the half-cleared frame (garbage frame_obj) and segfaults the interpreter. Observed in production on CPython 3.13.14 under streaming HTTP load (starlette request.stream() async generators being GC'd under cancellation churn): worker SIGSEGV within seconds of enabling loop.set_debug(True). Reproduces on 3.13 and 3.14, x86_64 and aarch64. Fix: skip source-traceback capture (per-thread flag) for handles created from the finalizer hook -- the traceback of a GC point is meaningless for debugging anyway -- and make extract_stack() tolerate AttributeError/ ValueError/TypeError from walk_stack, which py3.14 can raise when non-frame objects (e.g. _asyncio.TaskStepMethWrapper) appear in the stack (#715).
1 parent e8efea4 commit 7d99f9e

3 files changed

Lines changed: 27 additions & 1 deletion

File tree

uvloop/cbhandles.pyx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,21 @@ cdef new_MethodHandle3(Loop loop, str name, method3_t callback, object context,
416416
return handle
417417

418418

419+
# Set (per-thread) while handles are created from interpreter finalization
420+
# contexts, where walking the frame stack is not safe: the asyncgen
421+
# finalizer hook can be invoked by a dealloc while CPython is mid-way
422+
# through clearing the current frame (_PyEval_FrameClearAndPop ->
423+
# PyObject_CallFinalizerFromDealloc), and sys._getframe() then dereferences
424+
# the half-cleared frame and crashes the interpreter.
425+
_extract_stack_disabled = threading_local()
426+
427+
419428
cdef extract_stack():
420429
"""Replacement for traceback.extract_stack() that only does the
421430
necessary work for asyncio debug mode.
422431
"""
432+
if getattr(_extract_stack_disabled, 'flag', False):
433+
return None
423434
try:
424435
f = sys_getframe()
425436
# sys._getframe() might raise ValueError if being called without a frame, e.g.
@@ -433,6 +444,11 @@ cdef extract_stack():
433444
stack = tb_StackSummary.extract(tb_walk_stack(f),
434445
limit=DEBUG_STACK_DEPTH,
435446
lookup_lines=False)
447+
except (AttributeError, ValueError, TypeError):
448+
# The stack may contain non-frame objects (py3.14 can surface
449+
# _asyncio.TaskStepMethWrapper to walk_stack; see #715) or be torn
450+
# when invoked from finalization contexts.
451+
return None
436452
finally:
437453
f = None
438454

uvloop/includes/stdlib.pxi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ cdef int ssl_SSL_ERROR_WANT_WRITE = ssl.SSL_ERROR_WANT_WRITE
139139
cdef int ssl_SSL_ERROR_SYSCALL = ssl.SSL_ERROR_SYSCALL
140140

141141
cdef threading_Thread = threading.Thread
142+
cdef threading_local = threading.local
142143
cdef threading_main_thread = threading.main_thread
143144

144145
cdef int subprocess_PIPE = subprocess.PIPE

uvloop/loop.pyx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3195,7 +3195,16 @@ cdef class Loop:
31953195
def _asyncgen_finalizer_hook(self, agen):
31963196
self._asyncgens.discard(agen)
31973197
if not self.is_closed():
3198-
self.call_soon_threadsafe(self.create_task, agen.aclose())
3198+
# This hook can run from a dealloc while the interpreter is
3199+
# mid-way through clearing a frame; debug-mode source-traceback
3200+
# capture must not walk frames in that window (it segfaults;
3201+
# see extract_stack). The traceback of a GC point is
3202+
# meaningless for debugging anyway.
3203+
_extract_stack_disabled.flag = True
3204+
try:
3205+
self.call_soon_threadsafe(self.create_task, agen.aclose())
3206+
finally:
3207+
_extract_stack_disabled.flag = False
31993208

32003209
def _asyncgen_firstiter_hook(self, agen):
32013210
if self._asyncgens_shutdown_called:

0 commit comments

Comments
 (0)