Fix debug-mode segfault when the asyncgen finalizer hook runs during frame teardown#756
Open
david-runlayer wants to merge 1 commit into
Open
Conversation
Author
|
CI note: the |
david-runlayer
force-pushed
the
fix-asyncgen-finalizer-upstream
branch
4 times, most recently
from
July 20, 2026 22:47
474e3dd to
3f175ad
Compare
…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 (MagicStack#715).
david-runlayer
force-pushed
the
fix-asyncgen-finalizer-upstream
branch
from
July 20, 2026 23:07
3f175ad to
1a15864
Compare
david-runlayer
marked this pull request as ready for review
July 20, 2026 23:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In debug mode,
Loop._asyncgen_finalizer_hookcan segfault the interpreter. The hook is invoked by the GC from a dealloc — which can happen while CPython is mid-way through clearing the current interpreter frame — and thecall_soon_threadsafeit performs creates aHandlewhose debug source-traceback capture callssys._getframe(), dereferencing the half-cleared frame.We hit this in production on CPython 3.13.14 with uvloop 0.22.1: enabling
loop.set_debug(True)on a Starlette/FastAPI service with streaming request bodies (request.stream()is an async generator; under disconnect/cancellation churn they are GC'd constantly) produced worker SIGSEGVs within seconds. Reproduces on 3.13.14 and 3.14.4, x86_64 and aarch64. It appears to be the same underlying weakness as #715 (there, on 3.14, the capture path surfaces a non-frame object totraceback.walk_stack).Symbolized backtrace (uvloop built
-O0 -g3, CPython 3.13.14)A production core dump shows the same failure from the release wheel: SIGSEGV (SEGV_MAPERR) inside
sys._getframeincref'ingframe->frame_objcontaining a torn value (0x3000000010).Fix
_asyncgen_finalizer_hooksets a per-thread flag (the hook can run on any thread) soextract_stack()skips frame introspection for the handle it creates. The source traceback of a GC point is arbitrary victim code anyway, so nothing meaningful is lost.extract_stack()additionally toleratesAttributeError/ValueError/TypeErrorfromStackSummary.extract/walk_stack, returningNoneinstead of propagating — this also addresses the 3.14 failure mode reported in uvloop debug mode crashes on Python 3.14 during async generator finalization (traceback.walk_stack sees non-frame object) #715 (_asyncio.TaskStepMethWrapperlackingf_back).Reproducer
Crashes in <15 s unpatched (verified end-to-end from a clean directory containing only these files: 271
Fatal Python errorlines in a 60 s window, current PyPI starlette/fastapi/uvicorn); clean with this fix (10 min, ~259k streamed requests, verified on 3.13.14 aarch64 and x86_64).app.py + load.py + docker command
Performance
Stock = master merge base (e8efea4), patched = this branch (including the save/restore hook); both built from the same tree with identical flags (aarch64, CPython 3.13.14, best-of-3):
call_soon+ cancel, non-debugcall_soon+ cancel, debugNon-debug hot paths are untouched by construction (
extract_stackis only reachable underloop._debug); the only non-debug cost is the thread-local save/set/restore per GC'd async generator, on a path whose real cost is dominated byuv_async_sendplus the subsequently scheduledcreate_task/aclose().Behavior change
In debug mode, the handle scheduling
agen.aclose()from the finalizer hook no longer carries_source_traceback. Previously it captured the stack of whichever code happened to be running when GC fired — misleading rather than useful. Task-level debug tracebacks (create_taskruns later in normal loop context) andfirstitercapture are unaffected.Validated with the reproducer plus targeted checks (normal handles still get
_source_tracebackin debug mode; asyncgen finalizer path executes cleanly); relying on CI for the full test matrix.