Fix crashes during interpreter shutdown on all Python versions#499
Open
nbouvrette wants to merge 1 commit intopython-greenlet:masterfrom
Open
Fix crashes during interpreter shutdown on all Python versions#499nbouvrette wants to merge 1 commit intopython-greenlet:masterfrom
nbouvrette wants to merge 1 commit intopython-greenlet:masterfrom
Conversation
nbouvrette
added a commit
to nbouvrette/greenlet
that referenced
this pull request
Mar 11, 2026
Made-with: Cursor
17d17e3 to
733a419
Compare
nbouvrette
added a commit
to nbouvrette/greenlet
that referenced
this pull request
Mar 12, 2026
Ports all crash fixes from the main branch (PR python-greenlet#499) to maint/3.2 for a 3.2.6 release targeting Python 3.9 stability. Three root causes of SIGSEGV during Py_FinalizeEx on Python < 3.11: 1. clear_deleteme_list() vector allocation crash: replaced copy with std::swap and switched deleteme_t to std::allocator (system malloc). 2. ThreadState memory corruption: switched from PythonAllocator (PyObject_Malloc) to std::malloc/std::free. 3. getcurrent() crash on invalidated type objects: added atexit handler that sets g_greenlet_shutting_down before _Py_IsFinalizing() is set. Also fixes exception preservation in clear_deleteme_list(), adds Py_IsFinalizing() compat shim for Python < 3.13, Windows USS tolerance for flaky memory test, and additional shutdown tests. Made-with: Cursor
5 tasks
25a4dfa to
e1fdf27
Compare
During Py_FinalizeEx, multiple greenlet code paths accessed partially-destroyed Python state, causing SIGSEGV in production (uWSGI worker recycling on ARM64 and x86_64, Python 3.11). Root cause: _Py_IsFinalizing() is set AFTER atexit handlers complete on ALL Python versions, leaving a window where getcurrent() and type validators reach into torn-down C++ state. Fix: Two independent guards now protect all shutdown phases: 1. g_greenlet_shutting_down — atexit handler registered at module init (LIFO = runs first). Covers the atexit phase where Py_IsFinalizing() is still False. 2. Py_IsFinalizing() — covers the GC collection and later phases. A compatibility shim is provided for Python < 3.13. These guards are checked in mod_getcurrent, PyGreenlet_GetCurrent, GreenletChecker, MainGreenletExactChecker, ContextExactChecker, clear_deleteme_list, ThreadState destructor, _green_dealloc_kill_started_non_main_greenlet, and AddPendingCall. Additional hardening: - clear_deleteme_list() uses std::swap (zero-allocation) - deleteme vector uses std::allocator (system malloc) - ThreadState uses std::malloc/std::free - clear_deleteme_list() preserves pending Python exceptions TDD-certified: tests fail on greenlet 3.3.2 and pass with the fix across Python 3.10-3.14. Test suite: 21 shutdown tests (5 TDD regression, 2 behavioral, 14 smoke with 3 strengthened). Also fixes: - test_dealloc_catches_GreenletExit_throws_other: use sys.unraisablehook for pytest compatibility - test_version: skip gracefully on old setuptools (PEP 639) - test_no_gil_on_free_threaded: use getattr for pylint compatibility - Flaky USS memory test on Windows Made-with: Cursor
a4a6510 to
5745a6c
Compare
nbouvrette
added a commit
to nbouvrette/greenlet
that referenced
this pull request
Mar 24, 2026
Backport of PR python-greenlet#499 (master) to maint/3.2 for greenlet 3.2.6, with all shutdown guards made unconditional across Python 3.9-3.13. The previous backport (3.2.5 / PR python-greenlet#495) only guarded Python < 3.11, but the vulnerability exists on ALL Python versions: Py_IsFinalizing() is set AFTER atexit handlers complete inside Py_FinalizeEx. Two independent guards now protect all shutdown phases: 1. g_greenlet_shutting_down — atexit handler registered at module init (LIFO = runs first). Covers the atexit phase where Py_IsFinalizing() is still False. 2. Py_IsFinalizing() — covers the GC collection and later phases. A compatibility shim maps to _Py_IsFinalizing() on Python < 3.13. These guards are checked in mod_getcurrent, PyGreenlet_GetCurrent, GreenletChecker, MainGreenletExactChecker, ContextExactChecker, clear_deleteme_list, ThreadState destructor, _green_dealloc_kill_started_non_main_greenlet, and AddPendingCall. Additional hardening: - clear_deleteme_list() uses std::swap (zero-allocation) - deleteme vector uses std::allocator (system malloc) - ThreadState uses std::malloc/std::free - clear_deleteme_list() preserves pending Python exceptions TDD-certified: tests fail on greenlet 3.3.2 and pass with the fix across Python 3.10-3.14. Docker verification on Python 3.9 and 3.10 confirms GUARDED on the maint/3.2 branch. Also fixes: - SPDX license identifier: Python-2.0 -> PSF-2.0 - test_dealloc_catches_GreenletExit_throws_other: use sys.unraisablehook for pytest compatibility - test_version: skip gracefully on old setuptools - Flaky USS memory test on Windows Made-with: Cursor
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
Fix multiple SIGSEGV crash paths during
Py_FinalizeExon all Python versions (3.10–3.14). Observed in production on ARM64 (Python 3.11 + uWSGI withmax-requestsworker recycling) where greenlet was installed as a transitive dependency but never explicitly used by application code.Relationship to PR #495
PR #495 partially addressed this class of crashes by adding
murder_in_place()and_Py_IsFinalizing()guards, but only on Python < 3.11 (#if !GREENLET_PY311). Further investigation revealed that:Py_IsFinalizing()is set after atexit handlers complete on every version.getcurrent(), type checkers (GreenletChecker,MainGreenletExactChecker,ContextExactChecker), andclear_deleteme_list()had no shutdown protection.This PR supersedes PR #495 by making all guards unconditional, protecting the remaining crash paths, and adding TDD-certified tests that demonstrably fail on unpatched greenlet 3.3.2.
Design
Two independent guards now protect all shutdown phases:
g_greenlet_shutting_down— an atexit handler registered at module init (LIFO = runs first) sets this flag. Covers the atexit phase ofPy_FinalizeEx, wherePy_IsFinalizing()is stillFalseon all Python versions.Py_IsFinalizing()— covers the GC collection and later phases ofPy_FinalizeEx. A compatibility shim is provided for Python < 3.13 (where only the private_Py_IsFinalizing()existed).These guards are checked in
mod_getcurrent,PyGreenlet_GetCurrent,GreenletChecker,MainGreenletExactChecker,ContextExactChecker,clear_deleteme_list(),ThreadState::~ThreadState(),_green_dealloc_kill_started_non_main_greenlet, andThreadState_DestroyNoGIL::AddPendingCall.Root cause
_Py_IsFinalizing()is only set after atexit handlers complete insidePy_FinalizeExon all Python versions:Without the guards, code running in atexit handlers (e.g. uWSGI plugin cleanup calling
Py_FinalizeEx) or__del__methods could callgreenlet.getcurrent(), reaching into partially-torn-down C++ state and crashing inPyType_IsSubtypeviaGreenletChecker.What changed
C++ shutdown guards (8 files)
PyModule.cppg_greenlet_shutting_down+ atexit handler made unconditional (was#if !GREENLET_PY311)CObjects.cppPyGreenlet_GetCurrentguard made unconditionalPyGreenlet.cppmurder_in_place()guard made unconditionalTThreadState.hppclear_deleteme_list()+ destructor guards made unconditionalTThreadStateDestroy.cppAddPendingCallguard extended withg_greenlet_shutting_downgreenlet.cppgreenlet_refs.hppGreenletChecker+ContextExactCheckergreenlet_internal.hppMainGreenletExactCheckerAdditional hardening
clear_deleteme_list()usesstd::swap(zero-allocation) instead of copying thePythonAllocator-backed vectordeletemevector usesstd::allocator(systemmalloc) instead ofPyMem_MallocThreadStateusesstd::malloc/std::freeinstead ofPyObject_Mallocclear_deleteme_list()preserves pending Python exceptions around its cleanup loopTests (3 files)
test_interpreter_shutdown.py— verified RED on greenlet 3.3.2 (UNGUARDED) and GREEN with fix (GUARDED) across Python 3.10–3.14getcurrent()still returns valid objects when called before greenlet's cleanup (guards against over-blocking)test_dealloc_catches_GreenletExit_throws_other— usesys.unraisablehookinstead of stderr capture (pytest compatibility)test_version— skip gracefully on old setuptools that can't parse PEP 639 SPDX license formatTDD verification
Ran both test types against unpatched greenlet 3.3.2 and the patched code across 6 Python versions:
requires-python >= 3.10)PR #495's tests were also re-evaluated as part of this work: all 9 original tests pass on both greenlet 3.1.1 (pre-#495) and 3.3.2 (post-#495), confirming they are smoke tests that cannot detect regressions. The 5 Group D tests added here are the true regression safety net.
Additionally, the crash reproducer (uWSGI + Flask on ARM64 Python 3.11) ran 45,000 requests with 0 crashes (15 worker recycling cycles) with the patched greenlet.
Test plan
murder_in_place()guard only fires during shutdown, not normal thread exit (Group B tests verify GreenletExit/finally still work)Backport note
These fixes have already been backported to the
maint/3.2branch in PR #500 (targeting 3.2.6), since the previous backport (3.2.5 / PR #495) did not fully stabilize shutdown behavior.