Skip to content

Commit 00feede

Browse files
committed
Fix crashes during interpreter shutdown on all Python versions (3.2.6)
Backport of PR #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 #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
1 parent e3759d4 commit 00feede

15 files changed

Lines changed: 739 additions & 79 deletions

CHANGES.rst

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,54 @@
55
3.2.6 (unreleased)
66
==================
77

8-
- Nothing changed yet.
8+
- Fix multiple crash paths during interpreter shutdown on **all Python
9+
versions** (observed with uWSGI worker recycling on ARM64 and x86_64).
10+
Two independent guards now protect all shutdown phases:
11+
12+
1. ``g_greenlet_shutting_down`` — an atexit handler registered at
13+
module init (LIFO = runs first) sets this flag. Covers the atexit
14+
phase of ``Py_FinalizeEx``, where ``Py_IsFinalizing()`` is still
15+
``False`` on all Python versions.
16+
17+
2. ``Py_IsFinalizing()`` — covers the GC collection and later phases
18+
of ``Py_FinalizeEx``, where ``__del__`` methods and C++ destructors
19+
run. A compatibility shim is provided for Python < 3.13 (where
20+
only the private ``_Py_IsFinalizing()`` existed).
21+
22+
These guards are checked in ``mod_getcurrent``,
23+
``PyGreenlet_GetCurrent``, ``GreenletChecker``,
24+
``MainGreenletExactChecker``, ``ContextExactChecker``,
25+
``clear_deleteme_list()``, ``ThreadState::~ThreadState()``,
26+
``_green_dealloc_kill_started_non_main_greenlet``, and
27+
``ThreadState_DestroyNoGIL::AddPendingCall``.
28+
29+
Additional hardening:
30+
31+
- ``clear_deleteme_list()`` uses ``std::swap`` (zero-allocation)
32+
instead of copying the ``PythonAllocator``-backed vector.
33+
- The ``deleteme`` vector uses ``std::allocator`` (system ``malloc``)
34+
instead of ``PyMem_Malloc``.
35+
- ``ThreadState`` uses ``std::malloc`` / ``std::free`` instead of
36+
``PyObject_Malloc``.
37+
- ``clear_deleteme_list()`` preserves any pending Python exception
38+
around its cleanup loop.
39+
40+
Verified via TDD: tests fail on greenlet 3.3.2 (UNGUARDED) and pass
41+
with the fix (GUARDED) across Python 3.10–3.14.
42+
43+
This is distinct from the dealloc crash fixed in 3.2.5
44+
(`PR #495
45+
<https://github.com/python-greenlet/greenlet/pull/495>`_).
46+
Backported from `PR #499
47+
<https://github.com/python-greenlet/greenlet/pull/499>`_ by Nicolas
48+
Bouvrette.
49+
50+
- Fix ``test_dealloc_catches_GreenletExit_throws_other`` to use
51+
``sys.unraisablehook`` instead of stderr capture, making it work
52+
with both pytest and unittest runners.
53+
54+
- Fix ``test_version`` to skip gracefully when the local setuptools
55+
version does not support PEP 639 SPDX license format.
956

1057

1158
3.2.5 (2026-02-20)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def get_greenlet_version():
225225
'Documentation': 'https://greenlet.readthedocs.io/',
226226
'Changes': 'https://greenlet.readthedocs.io/en/latest/changes.html',
227227
},
228-
license="MIT AND Python-2.0",
228+
license="MIT AND PSF-2.0",
229229
license_files=[
230230
'LICENSE',
231231
'LICENSE.PSF',

src/greenlet/CObjects.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ extern "C" {
2929
static PyGreenlet*
3030
PyGreenlet_GetCurrent(void)
3131
{
32+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
33+
return nullptr;
34+
}
3235
return GET_THREAD_STATE().state().get_current().relinquish_ownership();
3336
}
3437

src/greenlet/PyGreenlet.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,10 @@ _green_dealloc_kill_started_non_main_greenlet(BorrowedGreenlet self)
203203
//
204204
// See: https://github.com/python-greenlet/greenlet/issues/411
205205
// https://github.com/python-greenlet/greenlet/issues/351
206-
#if !GREENLET_PY311
207-
if (_Py_IsFinalizing()) {
206+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
208207
self->murder_in_place();
209208
return 1;
210209
}
211-
#endif
212210

213211
/* Hacks hacks hacks copied from instance_dealloc() */
214212
/* Temporarily resurrect the greenlet. */

src/greenlet/PyModule.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,28 @@ using greenlet::ThreadState;
1717
# pragma clang diagnostic ignored "-Wunused-variable"
1818
#endif
1919

20+
// _Py_IsFinalizing() is only set AFTER atexit handlers complete
21+
// inside Py_FinalizeEx on ALL Python versions (including 3.11+).
22+
// Code running in atexit handlers (e.g. uWSGI plugin cleanup
23+
// calling Py_FinalizeEx, New Relic agent shutdown) can still call
24+
// greenlet.getcurrent(), but by that time type objects or
25+
// internal state may have been invalidated. This flag is set by
26+
// an atexit handler registered at module init (LIFO = runs first).
27+
int g_greenlet_shutting_down = 0;
28+
29+
static PyObject*
30+
_greenlet_atexit_callback(PyObject* UNUSED(self), PyObject* UNUSED(args))
31+
{
32+
g_greenlet_shutting_down = 1;
33+
Py_RETURN_NONE;
34+
}
35+
36+
static PyMethodDef _greenlet_atexit_method = {
37+
"_greenlet_cleanup", _greenlet_atexit_callback,
38+
METH_NOARGS, NULL
39+
};
40+
41+
2042
PyDoc_STRVAR(mod_getcurrent_doc,
2143
"getcurrent() -> greenlet\n"
2244
"\n"
@@ -26,6 +48,9 @@ PyDoc_STRVAR(mod_getcurrent_doc,
2648
static PyObject*
2749
mod_getcurrent(PyObject* UNUSED(module))
2850
{
51+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
52+
Py_RETURN_NONE;
53+
}
2954
return GET_THREAD_STATE().state().get_current().relinquish_ownership_o();
3055
}
3156

src/greenlet/TThreadState.hpp

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef GREENLET_THREAD_STATE_HPP
22
#define GREENLET_THREAD_STATE_HPP
33

4+
#include <cstdlib>
45
#include <ctime>
56
#include <stdexcept>
67

@@ -22,6 +23,14 @@ using greenlet::refs::CreatedModule;
2223
using greenlet::refs::PyErrPieces;
2324
using greenlet::refs::NewReference;
2425

26+
// Defined in PyModule.cpp; set by an atexit handler to signal
27+
// that the interpreter is shutting down. Needed on ALL Python
28+
// versions because _Py_IsFinalizing() is only set AFTER atexit
29+
// handlers complete inside Py_FinalizeEx. Code running in
30+
// atexit handlers (e.g. uWSGI plugin cleanup) needs this early
31+
// flag to avoid accessing partially-torn-down greenlet state.
32+
extern int g_greenlet_shutting_down;
33+
2534
namespace greenlet {
2635
/**
2736
* Thread-local state of greenlets.
@@ -104,7 +113,13 @@ class ThreadState {
104113
/* Strong reference to the trace function, if any. */
105114
OwnedObject tracefunc;
106115

107-
typedef std::vector<PyGreenlet*, PythonAllocator<PyGreenlet*> > deleteme_t;
116+
// Use std::allocator (malloc/free) instead of PythonAllocator
117+
// (PyMem_Malloc) for the deleteme list. During Py_FinalizeEx on
118+
// Python < 3.11, the PyObject_Malloc pool that holds ThreadState
119+
// can be disrupted, corrupting any PythonAllocator-backed
120+
// containers. Using std::allocator makes this vector independent
121+
// of Python's allocator lifecycle.
122+
typedef std::vector<PyGreenlet*> deleteme_t;
108123
/* A vector of raw PyGreenlet pointers representing things that need
109124
deleted when this thread is running. The vector owns the
110125
references, but you need to manually INCREF/DECREF as you use
@@ -120,7 +135,6 @@ class ThreadState {
120135

121136
static std::clock_t _clocks_used_doing_gc;
122137
static ImmortalString get_referrers_name;
123-
static PythonAllocator<ThreadState> allocator;
124138

125139
G_NO_COPIES_OF_CLS(ThreadState);
126140

@@ -146,15 +160,21 @@ class ThreadState {
146160

147161

148162
public:
149-
static void* operator new(size_t UNUSED(count))
163+
// Allocate ThreadState with malloc/free rather than Python's object
164+
// allocator. ThreadState outlives many Python objects and must
165+
// remain valid throughout Py_FinalizeEx. On Python < 3.11,
166+
// PyObject_Malloc pools can be disrupted during early finalization,
167+
// corrupting any C++ objects stored in them.
168+
static void* operator new(size_t count)
150169
{
151-
return ThreadState::allocator.allocate(1);
170+
void* p = std::malloc(count);
171+
if (!p) throw std::bad_alloc();
172+
return p;
152173
}
153174

154175
static void operator delete(void* ptr)
155176
{
156-
return ThreadState::allocator.deallocate(static_cast<ThreadState*>(ptr),
157-
1);
177+
std::free(ptr);
158178
}
159179

160180
static void init()
@@ -283,33 +303,43 @@ class ThreadState {
283303
inline void clear_deleteme_list(const bool murder=false)
284304
{
285305
if (!this->deleteme.empty()) {
286-
// It's possible we could add items to this list while
287-
// running Python code if there's a thread switch, so we
288-
// need to defensively copy it before that can happen.
289-
deleteme_t copy = this->deleteme;
290-
this->deleteme.clear(); // in case things come back on the list
306+
// Move the list contents out with swap — a constant-time
307+
// pointer exchange that never allocates. The previous code
308+
// used a copy (deleteme_t copy = this->deleteme) which
309+
// allocated through PythonAllocator / PyMem_Malloc; that
310+
// could SIGSEGV during early Py_FinalizeEx on Python < 3.11
311+
// when the allocator is partially torn down.
312+
deleteme_t copy;
313+
std::swap(copy, this->deleteme);
314+
315+
// During Py_FinalizeEx cleanup, the GC or atexit handlers
316+
// may have already collected objects in this list, leaving
317+
// dangling pointers. Attempting Py_DECREF on freed memory
318+
// causes a SIGSEGV. g_greenlet_shutting_down covers the
319+
// early atexit phase; Py_IsFinalizing() covers later phases.
320+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
321+
return;
322+
}
323+
324+
// Preserve any pending exception so that cleanup-triggered
325+
// errors don't accidentally swallow an unrelated exception
326+
// (e.g. one set by throw() before a switch).
327+
PyErrPieces incoming_err;
328+
291329
for(deleteme_t::iterator it = copy.begin(), end = copy.end();
292330
it != end;
293331
++it ) {
294332
PyGreenlet* to_del = *it;
295333
if (murder) {
296-
// Force each greenlet to appear dead; we can't raise an
297-
// exception into it anymore anyway.
298334
to_del->pimpl->murder_in_place();
299335
}
300-
301-
// The only reference to these greenlets should be in
302-
// this list, decreffing them should let them be
303-
// deleted again, triggering calls to green_dealloc()
304-
// in the correct thread (if we're not murdering).
305-
// This may run arbitrary Python code and switch
306-
// threads or greenlets!
307336
Py_DECREF(to_del);
308337
if (PyErr_Occurred()) {
309338
PyErr_WriteUnraisable(nullptr);
310339
PyErr_Clear();
311340
}
312341
}
342+
incoming_err.PyErrRestore();
313343
}
314344
}
315345

@@ -370,8 +400,7 @@ class ThreadState {
370400
//
371401
// Python 3.11+ restructured interpreter finalization so that
372402
// these APIs remain safe during shutdown.
373-
#if !GREENLET_PY311
374-
if (_Py_IsFinalizing()) {
403+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
375404
this->tracefunc.CLEAR();
376405
if (this->current_greenlet) {
377406
this->current_greenlet->murder_in_place();
@@ -380,7 +409,6 @@ class ThreadState {
380409
this->main_greenlet.CLEAR();
381410
return;
382411
}
383-
#endif
384412

385413
// We should not have an "origin" greenlet; that only exists
386414
// for the temporary time during a switch, which should not
@@ -505,7 +533,6 @@ class ThreadState {
505533
};
506534

507535
ImmortalString ThreadState::get_referrers_name(nullptr);
508-
PythonAllocator<ThreadState> ThreadState::allocator;
509536
std::clock_t ThreadState::_clocks_used_doing_gc(0);
510537

511538

src/greenlet/TThreadStateDestroy.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,7 @@ struct ThreadState_DestroyNoGIL
177177
// segfault if we happen to get context switched, and maybe we should
178178
// just always implement our own AddPendingCall, but I'd like to see if
179179
// this works first
180-
#if GREENLET_PY313
181-
if (Py_IsFinalizing()) {
182-
#else
183-
if (_Py_IsFinalizing()) {
184-
#endif
180+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
185181
#ifdef GREENLET_DEBUG
186182
// No need to log in the general case. Yes, we'll leak,
187183
// but we're shutting down so it should be ok.

src/greenlet/greenlet.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,39 @@ greenlet_internal_mod_init() noexcept
232232
OwnedObject clocks_per_sec = OwnedObject::consuming(PyLong_FromSsize_t(CLOCKS_PER_SEC));
233233
m.PyAddObject("CLOCKS_PER_SEC", clocks_per_sec);
234234

235+
// Register an atexit handler that sets g_greenlet_shutting_down.
236+
// Python's atexit is LIFO: registered last = called first. By
237+
// registering here (at import time, after most other libraries),
238+
// our handler runs before their cleanup code, which may try to
239+
// call greenlet.getcurrent() on objects whose type has been
240+
// invalidated. _Py_IsFinalizing() alone is insufficient on ALL
241+
// Python versions because it is only set AFTER atexit handlers
242+
// complete inside Py_FinalizeEx.
243+
{
244+
PyObject* atexit_mod = PyImport_ImportModule("atexit");
245+
if (atexit_mod) {
246+
PyObject* register_fn = PyObject_GetAttrString(atexit_mod, "register");
247+
if (register_fn) {
248+
extern PyMethodDef _greenlet_atexit_method;
249+
PyObject* callback = PyCFunction_New(&_greenlet_atexit_method, NULL);
250+
if (callback) {
251+
PyObject* args = PyTuple_Pack(1, callback);
252+
if (args) {
253+
PyObject* result = PyObject_Call(register_fn, args, NULL);
254+
Py_XDECREF(result);
255+
Py_DECREF(args);
256+
}
257+
Py_DECREF(callback);
258+
}
259+
Py_DECREF(register_fn);
260+
}
261+
Py_DECREF(atexit_mod);
262+
}
263+
// Non-fatal: if atexit registration fails, we still have
264+
// the _Py_IsFinalizing() fallback.
265+
PyErr_Clear();
266+
}
267+
235268
/* also publish module-level data as attributes of the greentype. */
236269
// XXX: This is weird, and enables a strange pattern of
237270
// confusing the class greenlet with the module greenlet; with

src/greenlet/greenlet_cpython_compat.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,12 @@ static inline void PyThreadState_LeaveTracing(PyThreadState *tstate)
147147
# define Py_C_RECURSION_LIMIT C_RECURSION_LIMIT
148148
#endif
149149

150+
// Py_IsFinalizing() became a public API in Python 3.13.
151+
// Map it to the private _Py_IsFinalizing() on older versions so all
152+
// call sites can use the standard name. Remove this once greenlet
153+
// drops support for Python < 3.13.
154+
#if !GREENLET_PY313
155+
# define Py_IsFinalizing() _Py_IsFinalizing()
156+
#endif
157+
150158
#endif /* GREENLET_CPYTHON_COMPAT_H */

src/greenlet/greenlet_internal.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ greenlet::refs::MainGreenletExactChecker(void *p)
4646
if (!p) {
4747
return;
4848
}
49+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
50+
return;
51+
}
4952
// We control the class of the main greenlet exactly.
5053
if (Py_TYPE(p) != &PyGreenlet_Type) {
5154
std::string err("MainGreenlet: Expected exactly a greenlet, not a ");

0 commit comments

Comments
 (0)