Skip to content

Commit 1a98554

Browse files
committed
Backport crash fixes from PR python-greenlet#499 for greenlet 3.2.6
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
1 parent e3759d4 commit 1a98554

File tree

10 files changed

+400
-29
lines changed

10 files changed

+400
-29
lines changed

CHANGES.rst

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

8-
- Nothing changed yet.
8+
- Fix multiple crash paths during interpreter shutdown on Python < 3.11
9+
(observed with uWSGI worker recycling). Three root causes were
10+
identified and fixed:
11+
12+
1. ``clear_deleteme_list()`` used a ``PythonAllocator``-backed vector
13+
copy (``PyMem_Malloc``), which could SIGSEGV during early
14+
``Py_FinalizeEx`` when Python's allocator pools are partially torn
15+
down. Replaced with ``std::swap`` (zero-allocation,
16+
constant-time) and switched the ``deleteme`` vector to
17+
``std::allocator`` (system ``malloc``).
18+
19+
2. ``ThreadState`` objects were allocated via ``PyObject_Malloc``,
20+
placing them in ``pymalloc`` pools that can be disrupted during
21+
finalization. Switched to ``std::malloc`` / ``std::free`` so
22+
``ThreadState`` memory remains valid throughout ``Py_FinalizeEx``.
23+
24+
3. ``_Py_IsFinalizing()`` is only set *after* ``call_py_exitfuncs``
25+
and ``_PyGC_CollectIfEnabled`` complete inside ``Py_FinalizeEx``,
26+
so code in atexit handlers or ``__del__`` methods could still call
27+
``greenlet.getcurrent()`` when type objects had already been
28+
invalidated, crashing in ``PyType_IsSubtype``. An atexit handler
29+
is now registered at module init (LIFO = runs first) that sets a
30+
shutdown flag checked by ``getcurrent()``,
31+
``PyGreenlet_GetCurrent()``, and ``clear_deleteme_list()``.
32+
33+
Additionally, ``clear_deleteme_list()`` now preserves any pending
34+
Python exception around its cleanup loop, fixing a latent bug where
35+
an unrelated exception (e.g. one set by ``throw()``) could be
36+
swallowed by ``PyErr_WriteUnraisable`` / ``PyErr_Clear`` inside the
37+
loop.
38+
39+
This is distinct from the dealloc crash fixed in 3.2.5
40+
(`PR #495
41+
<https://github.com/python-greenlet/greenlet/pull/495>`_).
42+
Backported from `PR #499
43+
<https://github.com/python-greenlet/greenlet/pull/499>`_ by Nicolas
44+
Bouvrette.
945

1046

1147
3.2.5 (2026-02-20)

src/greenlet/CObjects.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ extern "C" {
2929
static PyGreenlet*
3030
PyGreenlet_GetCurrent(void)
3131
{
32+
#if !GREENLET_PY311
33+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
34+
return nullptr;
35+
}
36+
#endif
3237
return GET_THREAD_STATE().state().get_current().relinquish_ownership();
3338
}
3439

src/greenlet/PyGreenlet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ _green_dealloc_kill_started_non_main_greenlet(BorrowedGreenlet self)
204204
// See: https://github.com/python-greenlet/greenlet/issues/411
205205
// https://github.com/python-greenlet/greenlet/issues/351
206206
#if !GREENLET_PY311
207-
if (_Py_IsFinalizing()) {
207+
if (Py_IsFinalizing()) {
208208
self->murder_in_place();
209209
return 1;
210210
}

src/greenlet/PyModule.cpp

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

20+
// On Python < 3.11, _Py_IsFinalizing() is only set AFTER
21+
// call_py_exitfuncs and _PyGC_CollectIfEnabled finish inside
22+
// Py_FinalizeEx. Code running in atexit handlers or __del__
23+
// methods can still call greenlet.getcurrent(), but by that
24+
// time type objects may have been invalidated, causing
25+
// SIGSEGV in PyType_IsSubtype. This flag is set by an atexit
26+
// handler registered at module init (LIFO = runs first).
27+
#if !GREENLET_PY311
28+
int g_greenlet_shutting_down = 0;
29+
30+
static PyObject*
31+
_greenlet_atexit_callback(PyObject* UNUSED(self), PyObject* UNUSED(args))
32+
{
33+
g_greenlet_shutting_down = 1;
34+
Py_RETURN_NONE;
35+
}
36+
37+
static PyMethodDef _greenlet_atexit_method = {
38+
"_greenlet_cleanup", _greenlet_atexit_callback,
39+
METH_NOARGS, NULL
40+
};
41+
#endif
42+
2043
PyDoc_STRVAR(mod_getcurrent_doc,
2144
"getcurrent() -> greenlet\n"
2245
"\n"
@@ -26,6 +49,11 @@ PyDoc_STRVAR(mod_getcurrent_doc,
2649
static PyObject*
2750
mod_getcurrent(PyObject* UNUSED(module))
2851
{
52+
#if !GREENLET_PY311
53+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
54+
Py_RETURN_NONE;
55+
}
56+
#endif
2957
return GET_THREAD_STATE().state().get_current().relinquish_ownership_o();
3058
}
3159

src/greenlet/TThreadState.hpp

Lines changed: 57 additions & 22 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,13 @@ 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. Only needed on
28+
// Python < 3.11 where _Py_IsFinalizing() is set too late.
29+
#if !GREENLET_PY311
30+
extern int g_greenlet_shutting_down;
31+
#endif
32+
2533
namespace greenlet {
2634
/**
2735
* Thread-local state of greenlets.
@@ -104,7 +112,13 @@ class ThreadState {
104112
/* Strong reference to the trace function, if any. */
105113
OwnedObject tracefunc;
106114

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

121135
static std::clock_t _clocks_used_doing_gc;
122136
static ImmortalString get_referrers_name;
123-
static PythonAllocator<ThreadState> allocator;
124137

125138
G_NO_COPIES_OF_CLS(ThreadState);
126139

@@ -146,15 +159,21 @@ class ThreadState {
146159

147160

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

154174
static void operator delete(void* ptr)
155175
{
156-
return ThreadState::allocator.deallocate(static_cast<ThreadState*>(ptr),
157-
1);
176+
std::free(ptr);
158177
}
159178

160179
static void init()
@@ -283,33 +302,50 @@ class ThreadState {
283302
inline void clear_deleteme_list(const bool murder=false)
284303
{
285304
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
305+
// Move the list contents out with swap — a constant-time
306+
// pointer exchange that never allocates. The previous code
307+
// used a copy (deleteme_t copy = this->deleteme) which
308+
// allocated through PythonAllocator / PyMem_Malloc; that
309+
// could SIGSEGV during early Py_FinalizeEx on Python < 3.11
310+
// when the allocator is partially torn down.
311+
deleteme_t copy;
312+
std::swap(copy, this->deleteme);
313+
314+
// During Py_FinalizeEx cleanup, the GC or atexit handlers
315+
// may have already collected objects in this list, leaving
316+
// dangling pointers. Attempting Py_DECREF on freed memory
317+
// causes a SIGSEGV. On Python < 3.11,
318+
// g_greenlet_shutting_down covers the early stages
319+
// (before Py_IsFinalizing() is set).
320+
#if !GREENLET_PY311
321+
if (g_greenlet_shutting_down || Py_IsFinalizing()) {
322+
return;
323+
}
324+
#else
325+
if (Py_IsFinalizing()) {
326+
return;
327+
}
328+
#endif
329+
330+
// Preserve any pending exception so that cleanup-triggered
331+
// errors don't accidentally swallow an unrelated exception
332+
// (e.g. one set by throw() before a switch).
333+
PyErrPieces incoming_err;
334+
291335
for(deleteme_t::iterator it = copy.begin(), end = copy.end();
292336
it != end;
293337
++it ) {
294338
PyGreenlet* to_del = *it;
295339
if (murder) {
296-
// Force each greenlet to appear dead; we can't raise an
297-
// exception into it anymore anyway.
298340
to_del->pimpl->murder_in_place();
299341
}
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!
307342
Py_DECREF(to_del);
308343
if (PyErr_Occurred()) {
309344
PyErr_WriteUnraisable(nullptr);
310345
PyErr_Clear();
311346
}
312347
}
348+
incoming_err.PyErrRestore();
313349
}
314350
}
315351

@@ -371,7 +407,7 @@ class ThreadState {
371407
// Python 3.11+ restructured interpreter finalization so that
372408
// these APIs remain safe during shutdown.
373409
#if !GREENLET_PY311
374-
if (_Py_IsFinalizing()) {
410+
if (Py_IsFinalizing()) {
375411
this->tracefunc.CLEAR();
376412
if (this->current_greenlet) {
377413
this->current_greenlet->murder_in_place();
@@ -505,7 +541,6 @@ class ThreadState {
505541
};
506542

507543
ImmortalString ThreadState::get_referrers_name(nullptr);
508-
PythonAllocator<ThreadState> ThreadState::allocator;
509544
std::clock_t ThreadState::_clocks_used_doing_gc(0);
510545

511546

src/greenlet/TThreadStateDestroy.cpp

Lines changed: 0 additions & 4 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
181180
if (Py_IsFinalizing()) {
182-
#else
183-
if (_Py_IsFinalizing()) {
184-
#endif
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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,45 @@ 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+
#if !GREENLET_PY311
236+
// Register an atexit handler that sets g_greenlet_shutting_down.
237+
// Python's atexit is LIFO: registered last = called first. By
238+
// registering here (at import time, after most other libraries),
239+
// our handler runs before their cleanup code, which may try to
240+
// call greenlet.getcurrent() on objects whose type has been
241+
// invalidated. _Py_IsFinalizing() alone is insufficient
242+
// because it is only set AFTER call_py_exitfuncs completes.
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+
// Non-fatal: if atexit registration fails, we still have
262+
// the _Py_IsFinalizing() fallback.
263+
if (PyErr_Occurred()) {
264+
PyErr_Clear();
265+
}
266+
Py_DECREF(atexit_mod);
267+
}
268+
else {
269+
PyErr_Clear();
270+
}
271+
}
272+
#endif
273+
235274
/* also publish module-level data as attributes of the greentype. */
236275
// XXX: This is weird, and enables a strange pattern of
237276
// 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 */

0 commit comments

Comments
 (0)