From e5c57de641942078487cd384fe3615a36109814b Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 1 Apr 2026 08:34:41 -0500 Subject: [PATCH 01/17] greenlet_refs: Replace PyModule_AddObject with safer alternatives (PyModule_AddObjectRef and PyModule_AddIntConstant). This fixes an improper DECREF on error. --- src/greenlet/greenlet_refs.hpp | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/greenlet/greenlet_refs.hpp b/src/greenlet/greenlet_refs.hpp index 64f07e28..89e7be52 100644 --- a/src/greenlet/greenlet_refs.hpp +++ b/src/greenlet/greenlet_refs.hpp @@ -902,16 +902,10 @@ namespace greenlet { { } - // PyAddObject(): Add a reference to the object to the module. - // On return, the reference count of the object is unchanged. - // - // The docs warn that PyModule_AddObject only steals the - // reference on success, so if it fails after we've incref'd - // or allocated, we're responsible for the decref. + // PyAddObject(): Add a new reference to the object to the module. void PyAddObject(const char* name, const long new_bool) { - OwnedObject p = OwnedObject::consuming(Require(PyBool_FromLong(new_bool))); - this->PyAddObject(name, p); + Require(PyModule_AddIntConstant(this->p, name, new_bool)); } void PyAddObject(const char* name, const OwnedObject& new_object) @@ -932,16 +926,11 @@ namespace greenlet { this->PyAddObject(name, reinterpret_cast(&type)); } + private: + void PyAddObject(const char* name, PyObject* new_object) { - Py_INCREF(new_object); - try { - Require(PyModule_AddObject(this->p, name, new_object)); - } - catch (const PyErrOccurred&) { - Py_DECREF(p); - throw; - } + Require(PyModule_AddObjectRef(this->p, name, new_object)); } }; From 1e7cc48e6bedbf844e86c8132ed46cdd1aa07968 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 1 Apr 2026 12:32:43 -0500 Subject: [PATCH 02/17] TPythonState: Comments around, and more defensive handling of, tstate->delete_later. Also re-enable the trashcan test on 3.13+ as a simple smoke test. --- src/greenlet/TPythonState.cpp | 50 ++++++++++++++++++++--- src/greenlet/tests/test_greenlet_trash.py | 30 +++++++++----- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/greenlet/TPythonState.cpp b/src/greenlet/TPythonState.cpp index 6375922b..154591c6 100644 --- a/src/greenlet/TPythonState.cpp +++ b/src/greenlet/TPythonState.cpp @@ -172,11 +172,28 @@ void PythonState::operator<<(const PyThreadState *const tstate) noexcept this->stackpointer = nullptr; } #endif - #if GREENLET_PY313 +#if GREENLET_PY313 + // By contract of _PyTrash_thread_deposit_object, + // the ``delete_later`` object has a refcount of 0. + // We take a strong reference to it. + // + // Now, ``delete_later`` is managed as a + // linked list whose objects are unconditionally deallocated + // WITHOUT calling DECREF on them, so it's not clear what that is + // actually accomplishing. That is, if another object is pushed on + // the list and then the list is deallocated, this object will + // still be deallocated. This strong reference serves as a form of + // resurrection, meaning that when operator>> DECREFs it, we might + // enter its ``tp_dealloc`` function again. + // + // In practice, it's quite difficult to arrange for this to be + // a non-null value during a greenlet switch. + // ``greenlet.tests.test_greenlet_trash`` tries, but under 3.14, + // at least, fails to do so. this->delete_later = Py_XNewRef(tstate->delete_later); #elif GREENLET_PY312 this->trash_delete_nesting = tstate->trash.delete_nesting; - #else // not 312 + #else // not 312 or 3.13+ this->trash_delete_nesting = tstate->trash_delete_nesting; #endif // GREENLET_PY312 #else // Not 311 @@ -257,9 +274,32 @@ void PythonState::operator>>(PyThreadState *const tstate) noexcept #endif this->_top_frame.relinquish_ownership(); #if GREENLET_PY313 - Py_XDECREF(tstate->delete_later); - tstate->delete_later = this->delete_later; - Py_CLEAR(this->delete_later); + // See comments in operator<<. We own a strong reference to + // this->delete_later, which may or may not be the same object as + // tstate->delete_later (depending if something pushed an object + // onto the trashcan). Again, because ``delete_later`` is managed + // as a linked list, it's not clear that saving and restoring the + // value, especially without ever setting it to NULL, accomplishes + // much...but the code was added by a core dev, so assume correct. + // + // Recall that tstate->delete_later is supposed to have a refcount + // of 0, because objects are added there from their ``tp_dealloc`` + // method. So we should only need to DECREF it if we're the ones + // that INCREF'd it in operator<<. (This is different than the + // core dev's original code which always did this.) + if (this->delete_later == tstate->delete_later) { + Py_XDECREF(tstate->delete_later); + tstate->delete_later = this->delete_later; + this->delete_later = nullptr; + } + else { + // it got switched behind our back. So the reference we own + // needs to be explicitly cleared. + tstate->delete_later = this->delete_later; + Py_CLEAR(this->delete_later); + } + + #elif GREENLET_PY312 tstate->trash.delete_nesting = this->trash_delete_nesting; #else // not 3.12 diff --git a/src/greenlet/tests/test_greenlet_trash.py b/src/greenlet/tests/test_greenlet_trash.py index c1fc1374..66a7ec65 100644 --- a/src/greenlet/tests/test_greenlet_trash.py +++ b/src/greenlet/tests/test_greenlet_trash.py @@ -25,7 +25,6 @@ - If the test fails in that way, the interpreter crashes. """ -from __future__ import print_function, absolute_import, division import unittest @@ -35,21 +34,29 @@ class TestTrashCanReEnter(unittest.TestCase): def test_it(self): try: # pylint:disable-next=no-name-in-module - from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=unused-import + from greenlet._greenlet import get_tstate_trash_delete_nesting except ImportError: import sys - # Python 3.13 has not "trash delete nesting" anymore (but "delete later") + # Python 3.13 has no "trash delete nesting" anymore (but + # "delete later") Our test as written won't be able to check + # the nesting depth anymore, but on debug builds it should + # still be able to trigger a crash if we get things wrong. + # However, at least on 3.14, the greenlet switch in the test + # below never finds an active value for + # ``tstate->delete_later``, meaning this test isn't testing + # what we want it to. assert sys.version_info[:2] >= (3, 13) - self.skipTest("get_tstate_trash_delete_nesting is not available.") + def get_tstate_trash_delete_nesting(): + return 0 # Try several times to trigger it, because it isn't 100% # reliable. - for _ in range(10): - self.check_it() + for i in range(30): + with self.subTest(i=i): + self.check_it(get_tstate_trash_delete_nesting) - def check_it(self): # pylint:disable=too-many-statements + def check_it(self, get_tstate_trash_delete_nesting): # pylint:disable=too-many-statements import greenlet - from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=no-name-in-module main = greenlet.getcurrent() assert get_tstate_trash_delete_nesting() == 0 @@ -57,11 +64,12 @@ def check_it(self): # pylint:disable=too-many-statements # We expect to be in deferred deallocation after this many # deallocations have occurred. TODO: I wish we had a better way to do # this --- that was before get_tstate_trash_delete_nesting; perhaps - # we can use that API to do better? - TRASH_UNWIND_LEVEL = 50 + # we can use that API to do better? (Probably not, it is non-functional in + # 3.13+) + TRASH_UNWIND_LEVEL = 500 # 50 is the nominal default on 3.12 # How many objects to put in a container; it's the container that # queues objects for deferred deallocation. - OBJECTS_PER_CONTAINER = 500 + OBJECTS_PER_CONTAINER = 1000 class Dealloc: # define the class here because we alter class variables each time we run. """ From 4980828c926efca9bbee42f9897c6cda5a3fe350 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 1 Apr 2026 12:39:11 -0500 Subject: [PATCH 03/17] TPythonState set_initial_state: Copy c_stack_refs like we do in operator<<. --- src/greenlet/TPythonState.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/greenlet/TPythonState.cpp b/src/greenlet/TPythonState.cpp index 154591c6..8805b6a1 100644 --- a/src/greenlet/TPythonState.cpp +++ b/src/greenlet/TPythonState.cpp @@ -329,6 +329,11 @@ void PythonState::set_initial_state(const PyThreadState* const tstate) noexcept #if GREENLET_PY314 this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; this->current_executor = tstate->current_executor; + #ifdef Py_GIL_DISABLED + this->c_stack_refs = ((_PyThreadStateImpl*)tstate)->c_stack_refs; + #endif + // this->stackpointer is left null because this->_top_frame is + // null so there is no value to copy. #elif GREENLET_PY312 this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; // XXX: TODO: Comment from a reviewer: From 7d7c9359dd384b8fe5bec249a4839f9a837dd26b Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 1 Apr 2026 13:09:33 -0500 Subject: [PATCH 04/17] TPythonState set_initial_state: Set c_recursion_depth the same way operator<< does. Using Py_C_RECURSION_LIMIT. Previously it was using py_recursion_limit, which doesn't make any sense. --- src/greenlet/TPythonState.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/greenlet/TPythonState.cpp b/src/greenlet/TPythonState.cpp index 8805b6a1..239708a2 100644 --- a/src/greenlet/TPythonState.cpp +++ b/src/greenlet/TPythonState.cpp @@ -336,11 +336,11 @@ void PythonState::set_initial_state(const PyThreadState* const tstate) noexcept // null so there is no value to copy. #elif GREENLET_PY312 this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; - // XXX: TODO: Comment from a reviewer: - // Should this be ``Py_C_RECURSION_LIMIT - tstate->c_recursion_remaining``? - // But to me it looks more like that might not be the right - // initialization either? - this->c_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; +#if GREENLET_314 + this->c_recursion_depth = 0; // unused on 3.14 +#else + this->c_recursion_depth = Py_C_RECURSION_LIMIT - tstate->c_recursion_remaining; +#endif #elif GREENLET_PY311 this->recursion_depth = tstate->recursion_limit - tstate->recursion_remaining; #else From 3f06755661f938f9d15947ecce885aaa4e804031 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 1 Apr 2026 13:19:54 -0500 Subject: [PATCH 05/17] greenlet.cpp: Extra safety creating CLOCKS_PER_SEC and adding items to the greenlet type's dict. --- src/greenlet/greenlet.cpp | 4 ++-- src/greenlet/greenlet_refs.hpp | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/greenlet/greenlet.cpp b/src/greenlet/greenlet.cpp index d1f2c7b0..b9d9236e 100644 --- a/src/greenlet/greenlet.cpp +++ b/src/greenlet/greenlet.cpp @@ -229,7 +229,7 @@ greenlet_internal_mod_init() noexcept m.PyAddObject("GREENLET_USE_CONTEXT_VARS", 1L); m.PyAddObject("GREENLET_USE_STANDARD_THREADING", 1L); - OwnedObject clocks_per_sec = OwnedObject::consuming(PyLong_FromSsize_t(CLOCKS_PER_SEC)); + NewReference clocks_per_sec(Require(PyLong_FromSsize_t(CLOCKS_PER_SEC))); m.PyAddObject("CLOCKS_PER_SEC", clocks_per_sec); /* also publish module-level data as attributes of the greentype. */ @@ -239,7 +239,7 @@ greenlet_internal_mod_init() noexcept // shouldn't be encouraged so don't add new items here. for (const char* const* p = copy_on_greentype; *p; p++) { OwnedObject o = m.PyRequireAttr(*p); - PyDict_SetItemString(PyGreenlet_Type.tp_dict, *p, o.borrow()); + Require(PyDict_SetItemString(PyGreenlet_Type.tp_dict, *p, o.borrow())); } /* diff --git a/src/greenlet/greenlet_refs.hpp b/src/greenlet/greenlet_refs.hpp index 89e7be52..12af0e6d 100644 --- a/src/greenlet/greenlet_refs.hpp +++ b/src/greenlet/greenlet_refs.hpp @@ -37,7 +37,9 @@ namespace greenlet // first). // // Because this is only set from an atexit handler, by which point - // we're single threaded, there should be no need to make it std::atomic. + // we're single threaded, there should be no need to make it + // std::atomic. + // TODO: Move this to the GreenletGlobals object? static int g_greenlet_shutting_down; static inline bool @@ -908,6 +910,8 @@ namespace greenlet { Require(PyModule_AddIntConstant(this->p, name, new_bool)); } + // It is safe to pass a null value to this API because we use + // PyModule_AddObjectRef under the covers which allows null. void PyAddObject(const char* name, const OwnedObject& new_object) { // The caller already owns a reference they will decref From 7bed810c2d3df115abc9c5ea485e7feb7e307244 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 1 Apr 2026 13:30:36 -0500 Subject: [PATCH 06/17] TPythonState: Comment on why we don't visit delete_later. Basically, it's already been done. --- src/greenlet/TPythonState.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/greenlet/TPythonState.cpp b/src/greenlet/TPythonState.cpp index 239708a2..7d7cec0a 100644 --- a/src/greenlet/TPythonState.cpp +++ b/src/greenlet/TPythonState.cpp @@ -364,6 +364,12 @@ int PythonState::tp_traverse(visitproc visit, void* arg, bool own_top_frame) noe // The naive way of looping over c_stack_refs->ref and visiting // those crashes the process (at least with GIL disabled). #endif + // Note that we DO NOT visit ``delete_later``. Even if it's + // non-null and we technically own a reference to it, its + // reference count already went to 0 once and it was in the + // process of being deallocated. The trash can mechanism linked it + // into a list that will be cleaned at some later time, and it has + // become untracked by the GC. return 0; } From 664792ea42eabbcfa8d1c0d9bb99621664f4c237 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Wed, 1 Apr 2026 13:34:23 -0500 Subject: [PATCH 07/17] Comment on use of PyErr_Fetch --- src/greenlet/TGreenlet.cpp | 4 ++++ src/greenlet/greenlet_refs.hpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/greenlet/TGreenlet.cpp b/src/greenlet/TGreenlet.cpp index 1fb056e8..2d1f71da 100644 --- a/src/greenlet/TGreenlet.cpp +++ b/src/greenlet/TGreenlet.cpp @@ -392,6 +392,10 @@ g_handle_exit(const OwnedObject& greenlet_result) if (!greenlet_result && mod_globs->PyExc_GreenletExit.PyExceptionMatches()) { /* catch and ignore GreenletExit */ PyErrFetchParam val; + // TODO: When we run on 3.12+ only (GREENLET_312), switch to the + // ``PyErr_GetRaisedException`` family of functions. The + // ``PyErr_Fetch`` family is deprecated on 3.12+, but is part + // of the stable ABI so it's not going anywhere. PyErr_Fetch(PyErrFetchParam(), val, PyErrFetchParam()); if (!val) { return OwnedObject::None(); diff --git a/src/greenlet/greenlet_refs.hpp b/src/greenlet/greenlet_refs.hpp index 12af0e6d..49778cc8 100644 --- a/src/greenlet/greenlet_refs.hpp +++ b/src/greenlet/greenlet_refs.hpp @@ -1008,6 +1008,10 @@ namespace greenlet { } }; + // TODO: When we run on 3.12+ only (GREENLET_312), switch to the + // ``PyErr_GetRaisedException`` family of functions. The + // ``PyErr_Fetch`` family is deprecated on 3.12+, but is part + // of the stable ABI so it's not going anywhere. class PyErrPieces { private: From 8c0472657b0fef20b9a807fbe951600607658cec Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 07:49:50 -0500 Subject: [PATCH 08/17] TThreadState: On free-threaded builds, protect deleteme list read/write with a mutex. --- src/greenlet/TThreadState.hpp | 134 +++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 60 deletions(-) diff --git a/src/greenlet/TThreadState.hpp b/src/greenlet/TThreadState.hpp index d45c839b..98ee526f 100644 --- a/src/greenlet/TThreadState.hpp +++ b/src/greenlet/TThreadState.hpp @@ -10,6 +10,7 @@ #include "greenlet_refs.hpp" #include "greenlet_thread_support.hpp" +using greenlet::LockGuard; using greenlet::refs::BorrowedObject; using greenlet::refs::BorrowedGreenlet; using greenlet::refs::BorrowedMainGreenlet; @@ -117,6 +118,12 @@ class ThreadState { refcounts are incremented in the copy. */ deleteme_t deleteme; +#ifdef Py_GIL_DISABLED + // On free-threaded builds, we need to protect shared access to + // the deleteme list by a mutex. It can be written from one thread + // while being read in another + Mutex deleteme_lock; +#endif #ifdef GREENLET_NEEDS_EXCEPTION_STATE_SAVED void* exception_state; @@ -297,70 +304,74 @@ class ThreadState { */ inline void clear_deleteme_list(const bool murder=false) { - if (!this->deleteme.empty()) { - // Move the list contents out with swap — a constant-time - // pointer exchange that never allocates. The previous - // code used a copy (deleteme_t copy = this->deleteme) - // which allocated through PythonAllocator / PyMem_Malloc; - // that could SIGSEGV during early Py_FinalizeEx on Python - // < 3.11 when the allocator is partially torn down. - deleteme_t copy; - std::swap(copy, this->deleteme); - - // During Py_FinalizeEx cleanup, the GC or atexit handlers - // may have already collected objects in this list, - // leaving dangling pointers. Attempting Py_DECREF on - // freed memory causes a SIGSEGV. g_greenlet_shutting_down - // covers the early atexit phase; Py_IsFinalizing() covers - // later phases. Thus, we deliberately leak. - if (greenlet::IsShuttingDown()) { - return; - } +#ifdef Py_GIL_DISABLED + LockGuard deleteme_guard(this->deleteme_lock); +#endif + if (this->deleteme.empty()) { + return; + } + // Move the list contents out with swap — a constant-time + // pointer exchange that never allocates. The previous + // code used a copy (deleteme_t copy = this->deleteme) + // which allocated through PythonAllocator / PyMem_Malloc; + // that could SIGSEGV during early Py_FinalizeEx on Python + // < 3.11 when the allocator is partially torn down. + deleteme_t copy; + std::swap(copy, this->deleteme); + + // During Py_FinalizeEx cleanup, the GC or atexit handlers + // may have already collected objects in this list, + // leaving dangling pointers. Attempting Py_DECREF on + // freed memory causes a SIGSEGV. g_greenlet_shutting_down + // covers the early atexit phase; Py_IsFinalizing() covers + // later phases. Thus, we deliberately leak. + if (greenlet::IsShuttingDown()) { + return; + } - // Preserve any pending exception so that cleanup-triggered - // errors don't accidentally swallow an unrelated exception - // (e.g. one set by throw() before a switch). - PyErrPieces incoming_err; - - for(deleteme_t::iterator it = copy.begin(), end = copy.end(); - it != end; - ++it ) { - PyGreenlet* to_del = *it; - if (murder) { - // Force each greenlet to appear dead; we can't raise an - // exception into it anymore anyway. - to_del->pimpl->murder_in_place(); - } + // Preserve any pending exception so that cleanup-triggered + // errors don't accidentally swallow an unrelated exception + // (e.g. one set by throw() before a switch). + PyErrPieces incoming_err; + + for(deleteme_t::iterator it = copy.begin(), end = copy.end(); + it != end; + ++it ) { + PyGreenlet* to_del = *it; + if (murder) { + // Force each greenlet to appear dead; we can't raise an + // exception into it anymore anyway. + to_del->pimpl->murder_in_place(); + } - // The only reference to these greenlets should be in - // this list, decreffing them should let them be - // deleted again, triggering calls to green_dealloc() - // in the correct thread (if we're not murdering). - // This may run arbitrary Python code and switch - // threads or greenlets! - Py_DECREF(to_del); - if (PyErr_Occurred()) { - PyErr_WriteUnraisable(nullptr); - PyErr_Clear(); - } + // The only reference to these greenlets should be in + // this list, decreffing them should let them be + // deleted again, triggering calls to green_dealloc() + // in the correct thread (if we're not murdering). + // This may run arbitrary Python code and switch + // threads or greenlets! + Py_DECREF(to_del); + if (PyErr_Occurred()) { + PyErr_WriteUnraisable(nullptr); + PyErr_Clear(); } - // Not worried about C++ exception safety here in terms of - // making sure we restore the error. Either we'll catch it - // above and establish the error from that exception - // (which, yes, might overwrite something from before we - // entered, but we're in an undefined situation at that - // point) or we won't catch it at all and will crash the - // process. - // - // As for Python exception safety, there's no chance we're - // overwriting an exception (from the loop) with no - // exception (captured NULLs before we entered the loop), - // because there CAN'T BE any exception from the loop --- - // we clear them. So we're either restoring a pre-existing - // exception, or leaving the exception unset (by restoring - // NULL). - incoming_err.PyErrRestore(); } + // Not worried about C++ exception safety here in terms of + // making sure we restore the error. Either we'll catch it + // above and establish the error from that exception + // (which, yes, might overwrite something from before we + // entered, but we're in an undefined situation at that + // point) or we won't catch it at all and will crash the + // process. + // + // As for Python exception safety, there's no chance we're + // overwriting an exception (from the loop) with no + // exception (captured NULLs before we entered the loop), + // because there CAN'T BE any exception from the loop --- + // we clear them. So we're either restoring a pre-existing + // exception, or leaving the exception unset (by restoring + // NULL). + incoming_err.PyErrRestore(); } public: @@ -393,6 +404,9 @@ class ThreadState { inline void delete_when_thread_running(PyGreenlet* to_del) { Py_INCREF(to_del); +#ifdef Py_GIL_DISABLED + LockGuard deleteme_guard(this->deleteme_lock); +#endif this->deleteme.push_back(to_del); } From 5cc092d1dda2c7bf7e23626ea8513e5c1001bbbb Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 08:00:24 -0500 Subject: [PATCH 09/17] PyGreenletUnswitchable: set tp_is_gc the same way PyGreenlet_Type does --- src/greenlet/PyGreenletUnswitchable.cpp | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/greenlet/PyGreenletUnswitchable.cpp b/src/greenlet/PyGreenletUnswitchable.cpp index 1b768ee3..729cb738 100644 --- a/src/greenlet/PyGreenletUnswitchable.cpp +++ b/src/greenlet/PyGreenletUnswitchable.cpp @@ -114,7 +114,7 @@ static PyGetSetDef green_unswitchable_getsets[] = { .name="force_switch_error", .get=(getter)green_unswitchable_getforce, .set=(setter)green_unswitchable_setforce, - .doc=NULL + .doc=nullptr }, { .name="force_slp_switch_error", @@ -126,21 +126,24 @@ static PyGetSetDef green_unswitchable_getsets[] = { }; PyTypeObject PyGreenletUnswitchable_Type = { - .ob_base=PyVarObject_HEAD_INIT(NULL, 0) - .tp_name="greenlet._greenlet.UnswitchableGreenlet", - .tp_dealloc= (destructor)green_dealloc, /* tp_dealloc */ - .tp_flags=G_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - .tp_doc="Undocumented internal class", /* tp_doc */ - .tp_traverse=(traverseproc)green_traverse, /* tp_traverse */ - .tp_clear=(inquiry)green_clear, /* tp_clear */ - - .tp_getset=green_unswitchable_getsets, /* tp_getset */ - .tp_base=&PyGreenlet_Type, /* tp_base */ - .tp_init=(initproc)green_init, /* tp_init */ - .tp_alloc=PyType_GenericAlloc, /* tp_alloc */ - .tp_new=(newfunc)green_unswitchable_new, /* tp_new */ - .tp_free=PyObject_GC_Del, /* tp_free */ - .tp_is_gc=(inquiry)green_is_gc, /* tp_is_gc */ + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "greenlet._greenlet.UnswitchableGreenlet", + .tp_dealloc = (destructor)green_dealloc, + .tp_flags = G_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "Undocumented internal class for testing error conditions", + .tp_traverse = (traverseproc)green_traverse, + .tp_clear = (inquiry)green_clear, + + .tp_getset = green_unswitchable_getsets, + .tp_base = &PyGreenlet_Type, + .tp_init = (initproc)green_init, + .tp_alloc = PyType_GenericAlloc, + .tp_new = (newfunc)green_unswitchable_new, + .tp_free = PyObject_GC_Del, +#ifndef Py_GIL_DISABLED + // See comments in the base type + .tp_is_gc = (inquiry)green_is_gc, +#endif }; From 5d90ded9ba8a040dd6c15d243fd0629191a72dcb Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 08:27:40 -0500 Subject: [PATCH 10/17] PyGreenlet.cpp: _green_dealloc_kill_started_non_main_greenlet: Reference safety when writing an error to sys.stderr. --- src/greenlet/PyGreenlet.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/greenlet/PyGreenlet.cpp b/src/greenlet/PyGreenlet.cpp index c29f74d9..0e0c7233 100644 --- a/src/greenlet/PyGreenlet.cpp +++ b/src/greenlet/PyGreenlet.cpp @@ -240,9 +240,17 @@ _green_dealloc_kill_started_non_main_greenlet(BorrowedGreenlet self) PyObject* f = PySys_GetObject("stderr"); Py_INCREF(self.borrow_o()); /* leak! */ if (f != NULL) { + // PySys_GetObject returns a borrowed ref which could go + // away when we run arbitrary code, as we do for any of + // the ``PyFile_Write`` APIs. + Py_INCREF(f); + // Note that we're not handling errors here. They either + // work or they don't, and any exception they raised will + // be replaced by PyErrRestore. PyFile_WriteString("GreenletExit did not kill ", f); PyFile_WriteObject(self.borrow_o(), f, 0); PyFile_WriteString("\n", f); + Py_DECREF(f); } } /* Restore the saved exception. */ From f053e68f7e44f5dc27e4dabef2b0f07b44576390 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 08:37:35 -0500 Subject: [PATCH 11/17] Remove some compatibility code for legacy versions of Python. --- src/greenlet/TGreenlet.hpp | 6 +- src/greenlet/TPythonState.cpp | 2 +- src/greenlet/__init__.py | 13 +--- src/greenlet/greenlet_cpython_compat.hpp | 59 ++----------------- src/greenlet/tests/leakcheck.py | 2 - src/greenlet/tests/test_contextvars.py | 2 - src/greenlet/tests/test_cpp.py | 3 - .../tests/test_extension_interface.py | 3 - src/greenlet/tests/test_leaks.py | 4 -- src/greenlet/tests/test_tracing.py | 1 - src/greenlet/tests/test_version.py | 2 - 11 files changed, 11 insertions(+), 86 deletions(-) diff --git a/src/greenlet/TGreenlet.hpp b/src/greenlet/TGreenlet.hpp index 32330e9d..e9372c55 100644 --- a/src/greenlet/TGreenlet.hpp +++ b/src/greenlet/TGreenlet.hpp @@ -18,6 +18,7 @@ using greenlet::refs::OwnedMainGreenlet; using greenlet::refs::BorrowedGreenlet; #if PY_VERSION_HEX < 0x30B00A6 + // prior to 3.11.0a6 # define _PyCFrame CFrame # define _PyInterpreterFrame _interpreter_frame #endif @@ -781,9 +782,7 @@ class TracingGuard // Instantiate one on the stack to save the GC state, // and then disable GC. When it goes out of scope, GC will be - // restored to its original state. Sadly, these APIs are only - // available on 3.10+; luckily, we only need them on 3.11+. -#if GREENLET_PY310 + // restored to its original state. class GCDisabledGuard { private: @@ -802,7 +801,6 @@ class TracingGuard } } }; -#endif OwnedObject& operator<<=(OwnedObject& lhs, greenlet::SwitchingArgs& rhs) noexcept; diff --git a/src/greenlet/TPythonState.cpp b/src/greenlet/TPythonState.cpp index 7d7cec0a..7c493b8e 100644 --- a/src/greenlet/TPythonState.cpp +++ b/src/greenlet/TPythonState.cpp @@ -172,7 +172,7 @@ void PythonState::operator<<(const PyThreadState *const tstate) noexcept this->stackpointer = nullptr; } #endif -#if GREENLET_PY313 + #if GREENLET_PY313 // By contract of _PyTrash_thread_deposit_object, // the ``delete_later`` object has a refcount of 0. // We take a strong reference to it. diff --git a/src/greenlet/__init__.py b/src/greenlet/__init__.py index d34235c4..29aa00d3 100644 --- a/src/greenlet/__init__.py +++ b/src/greenlet/__init__.py @@ -2,9 +2,6 @@ """ The root of the greenlet package. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function __all__ = [ '__version__', @@ -43,14 +40,8 @@ ### # tracing ### -try: - from ._greenlet import gettrace - from ._greenlet import settrace -except ImportError: - # Tracing wasn't supported. - # XXX: The option to disable it was removed in 1.0, - # so this branch should be dead code. - pass +from ._greenlet import gettrace +from ._greenlet import settrace ### # Constants diff --git a/src/greenlet/greenlet_cpython_compat.hpp b/src/greenlet/greenlet_cpython_compat.hpp index 161ea4e4..e76f3420 100644 --- a/src/greenlet/greenlet_cpython_compat.hpp +++ b/src/greenlet/greenlet_cpython_compat.hpp @@ -9,13 +9,6 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" - -#if PY_VERSION_HEX >= 0x30A00B1 -# define GREENLET_PY310 1 -#else -# define GREENLET_PY310 0 -#endif - /* Python 3.10 beta 1 changed tstate->use_tracing to a nested cframe member. See https://github.com/python/cpython/pull/25276 @@ -23,7 +16,7 @@ We have to save and restore this as well. Python 3.13 removed PyThreadState.cframe (GH-108035). */ -#if GREENLET_PY310 && PY_VERSION_HEX < 0x30D0000 +#if PY_VERSION_HEX < 0x30D0000 # define GREENLET_USE_CFRAME 1 #else # define GREENLET_USE_CFRAME 0 @@ -73,52 +66,20 @@ Greenlet won't compile on anything older than Python 3.11 alpha 4 (see # define GREENLET_PY315 0 #endif -#ifndef Py_SET_REFCNT -/* Py_REFCNT and Py_SIZE macros are converted to functions -https://bugs.python.org/issue39573 */ -# define Py_SET_REFCNT(obj, refcnt) Py_REFCNT(obj) = (refcnt) -#endif -#ifdef _Py_DEC_REFTOTAL -# define GREENLET_Py_DEC_REFTOTAL _Py_DEC_REFTOTAL -#else /* _Py_DEC_REFTOTAL macro has been removed from Python 3.9 by: https://github.com/python/cpython/commit/49932fec62c616ec88da52642339d83ae719e924 The symbol we use to replace it was removed by at least 3.12. */ -# ifdef Py_REF_DEBUG -# if GREENLET_PY312 -# define GREENLET_Py_DEC_REFTOTAL -# else -# define GREENLET_Py_DEC_REFTOTAL _Py_RefTotal-- -# endif -# else -# define GREENLET_Py_DEC_REFTOTAL -# endif -#endif -// Define these flags like Cython does if we're on an old version. -#ifndef Py_TPFLAGS_CHECKTYPES - #define Py_TPFLAGS_CHECKTYPES 0 -#endif -#ifndef Py_TPFLAGS_HAVE_INDEX - #define Py_TPFLAGS_HAVE_INDEX 0 -#endif -#ifndef Py_TPFLAGS_HAVE_NEWBUFFER - #define Py_TPFLAGS_HAVE_NEWBUFFER 0 -#endif - -#ifndef Py_TPFLAGS_HAVE_VERSION_TAG - #define Py_TPFLAGS_HAVE_VERSION_TAG 0 +#if defined(Py_REF_DEBUG) && !GREENLET_PY312 +# define GREENLET_Py_DEC_REFTOTAL _Py_RefTotal-- +#else +# define GREENLET_Py_DEC_REFTOTAL #endif -#define G_TPFLAGS_DEFAULT Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_VERSION_TAG | Py_TPFLAGS_CHECKTYPES | Py_TPFLAGS_HAVE_NEWBUFFER | Py_TPFLAGS_HAVE_GC - -#if PY_VERSION_HEX < 0x03090000 -// The official version only became available in 3.9 -# define PyObject_GC_IsTracked(o) _PyObject_GC_IS_TRACKED(o) -#endif +#define G_TPFLAGS_DEFAULT Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_VERSION_TAG | Py_TPFLAGS_HAVE_GC // bpo-43760 added PyThreadState_EnterTracing() to Python 3.11.0a2 @@ -126,11 +87,7 @@ Greenlet won't compile on anything older than Python 3.11 alpha 4 (see static inline void PyThreadState_EnterTracing(PyThreadState *tstate) { tstate->tracing++; -#if PY_VERSION_HEX >= 0x030A00A1 tstate->cframe->use_tracing = 0; -#else - tstate->use_tracing = 0; -#endif } #endif @@ -141,11 +98,7 @@ static inline void PyThreadState_LeaveTracing(PyThreadState *tstate) tstate->tracing--; int use_tracing = (tstate->c_tracefunc != NULL || tstate->c_profilefunc != NULL); -#if PY_VERSION_HEX >= 0x030A00A1 tstate->cframe->use_tracing = use_tracing; -#else - tstate->use_tracing = use_tracing; -#endif } #endif diff --git a/src/greenlet/tests/leakcheck.py b/src/greenlet/tests/leakcheck.py index f45361e4..993e3fa8 100644 --- a/src/greenlet/tests/leakcheck.py +++ b/src/greenlet/tests/leakcheck.py @@ -21,8 +21,6 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from __future__ import print_function - import os import sys import gc diff --git a/src/greenlet/tests/test_contextvars.py b/src/greenlet/tests/test_contextvars.py index b0d1ccf3..1508d7cc 100644 --- a/src/greenlet/tests/test_contextvars.py +++ b/src/greenlet/tests/test_contextvars.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import gc import sys import unittest diff --git a/src/greenlet/tests/test_cpp.py b/src/greenlet/tests/test_cpp.py index 2d0cc9c9..2ed057ea 100644 --- a/src/greenlet/tests/test_cpp.py +++ b/src/greenlet/tests/test_cpp.py @@ -1,6 +1,3 @@ -from __future__ import print_function -from __future__ import absolute_import - import subprocess import unittest diff --git a/src/greenlet/tests/test_extension_interface.py b/src/greenlet/tests/test_extension_interface.py index 34b66567..403ffe23 100644 --- a/src/greenlet/tests/test_extension_interface.py +++ b/src/greenlet/tests/test_extension_interface.py @@ -1,6 +1,3 @@ -from __future__ import print_function -from __future__ import absolute_import - import sys import greenlet diff --git a/src/greenlet/tests/test_leaks.py b/src/greenlet/tests/test_leaks.py index 922bf0f8..1d3eb07b 100644 --- a/src/greenlet/tests/test_leaks.py +++ b/src/greenlet/tests/test_leaks.py @@ -2,8 +2,6 @@ """ Testing scenarios that may have leaked. """ -from __future__ import print_function, absolute_import, division - import sys import gc @@ -322,8 +320,6 @@ def _only_test_some_versions(self): # false negatives. At the moment, those false results seem to have # resolved, so we are actually running this on 3.8+ assert sys.version_info[0] >= 3 - if sys.version_info[:2] < (3, 8): - self.skipTest('Only observed on 3.11') if RUNNING_ON_MANYLINUX: self.skipTest("Slow and not worth repeating here") diff --git a/src/greenlet/tests/test_tracing.py b/src/greenlet/tests/test_tracing.py index 235fbcd6..c54a910a 100644 --- a/src/greenlet/tests/test_tracing.py +++ b/src/greenlet/tests/test_tracing.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys import sysconfig import greenlet diff --git a/src/greenlet/tests/test_version.py b/src/greenlet/tests/test_version.py index c3b9ad71..12b55c27 100755 --- a/src/greenlet/tests/test_version.py +++ b/src/greenlet/tests/test_version.py @@ -1,6 +1,4 @@ #! /usr/bin/env python -from __future__ import absolute_import -from __future__ import print_function import sys import os From 7e148c59e747ea1c8db4ef33c0f82d1449db36d1 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 09:11:44 -0500 Subject: [PATCH 12/17] _test_extension.test_switch_kwargs: Better safety for PyArg_ParseTuple --- src/greenlet/tests/_test_extension.c | 4 +++- src/greenlet/tests/test_extension_interface.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/greenlet/tests/_test_extension.c b/src/greenlet/tests/_test_extension.c index 612b7359..457c1612 100644 --- a/src/greenlet/tests/_test_extension.c +++ b/src/greenlet/tests/_test_extension.c @@ -62,7 +62,9 @@ test_switch_kwargs(PyObject* UNUSED(self), PyObject* args, PyObject* kwargs) PyGreenlet* g = NULL; PyObject* result = NULL; - PyArg_ParseTuple(args, "O!", &PyGreenlet_Type, &g); + if (!PyArg_ParseTuple(args, "O!", &PyGreenlet_Type, &g)) { + return NULL; + } if (g == NULL || !PyGreenlet_Check(g)) { PyErr_BadArgument(); diff --git a/src/greenlet/tests/test_extension_interface.py b/src/greenlet/tests/test_extension_interface.py index 403ffe23..aea975cf 100644 --- a/src/greenlet/tests/test_extension_interface.py +++ b/src/greenlet/tests/test_extension_interface.py @@ -17,6 +17,9 @@ def adder(x, y): g = greenlet.greenlet(adder) self.assertEqual(6, _test_extension.test_switch_kwargs(g, x=3, y=2)) + with self.assertRaisesRegex(TypeError, "argument 1 must be greenlet"): + _test_extension.test_switch_kwargs("not a greenlet") + def test_setparent(self): # pylint:disable=disallowed-name def foo(): From 3a260244c3f6d0b70ea8f0cec7f92b69ec3b7dcf Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 09:35:42 -0500 Subject: [PATCH 13/17] Fix improper incref in return value of _test_extension.c:test_switch/test_switch_kwargs/test_new_greenlet; test this. --- docs/c_api.rst | 2 ++ src/greenlet/PyGreenlet.cpp | 1 - src/greenlet/tests/_test_extension.c | 3 --- .../tests/test_extension_interface.py | 26 ++++++++++++++++++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/c_api.rst b/docs/c_api.rst index 65344ec3..487a08b2 100644 --- a/docs/c_api.rst +++ b/docs/c_api.rst @@ -83,6 +83,8 @@ Functions Switches to the greenlet *g*. Besides *g*, the remaining parameters are optional and may be ``NULL``. + Returns a new reference. + :param args: If ``args`` is NULL, an empty tuple is passed to the target greenlet. If given, must be a :class:`tuple`. diff --git a/src/greenlet/PyGreenlet.cpp b/src/greenlet/PyGreenlet.cpp index 0e0c7233..95cf6992 100644 --- a/src/greenlet/PyGreenlet.cpp +++ b/src/greenlet/PyGreenlet.cpp @@ -397,7 +397,6 @@ green_switch(PyGreenlet* self, PyObject* args, PyObject* kwargs) // second byte of the CALL_METHOD op for ``getcurrent()``). try { - //OwnedObject result = single_result(self->pimpl->g_switch()); OwnedObject result(single_result(self->pimpl->g_switch())); #ifndef NDEBUG // Note that the current greenlet isn't necessarily self. If self diff --git a/src/greenlet/tests/_test_extension.c b/src/greenlet/tests/_test_extension.c index 457c1612..f8fcb9dc 100644 --- a/src/greenlet/tests/_test_extension.c +++ b/src/greenlet/tests/_test_extension.c @@ -52,7 +52,6 @@ test_switch(PyObject* UNUSED(self), PyObject* greenlet) } return NULL; } - Py_INCREF(result); return result; } @@ -79,7 +78,6 @@ test_switch_kwargs(PyObject* UNUSED(self), PyObject* args, PyObject* kwargs) } return NULL; } - Py_XINCREF(result); return result; } @@ -138,7 +136,6 @@ test_new_greenlet(PyObject* UNUSED(self), PyObject* callable) return NULL; } - Py_INCREF(result); return result; } diff --git a/src/greenlet/tests/test_extension_interface.py b/src/greenlet/tests/test_extension_interface.py index aea975cf..0015ee01 100644 --- a/src/greenlet/tests/test_extension_interface.py +++ b/src/greenlet/tests/test_extension_interface.py @@ -56,7 +56,7 @@ def test_raise_greenlet_error(self): def test_throw(self): seen = [] - def foo(): # pylint:disable=disallowed-name + def foo(): # pylint:disable=disallowed-name try: greenlet.getcurrent().parent.switch() except ValueError: @@ -109,6 +109,30 @@ def test_not_throwable(self): self.assertEqual(str(exc.exception), "exceptions must be classes, or instances, not str") + def test_leaks(self): + from . import RUNNING_ON_FREETHREAD_BUILD + iters = 100 + if RUNNING_ON_FREETHREAD_BUILD: + expected_refs = [1] * iters + else: + expected_refs = [2] * iters + for name, caller in ( + ("test_switch", + lambda: _test_extension.test_switch(greenlet.greenlet(object))), + ("test_switch_kwargs", + lambda: _test_extension.test_switch_kwargs(greenlet.greenlet(object))), + ("test_new_greenlet", + lambda: _test_extension.test_new_greenlet(object)), + ): + with self.subTest(name): + results = [caller() for _ in range(iters)] + refs = [ + sys.getrefcount(i) - 1 # ignore ref in ``i`` + for i + in results + ] + self.assertEqual(refs, expected_refs) + if __name__ == '__main__': import unittest From 9f1af4db455c43b47ccaeedba512c718bc8ea65d Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 09:44:25 -0500 Subject: [PATCH 14/17] _test_extension test_setparent: Fix a leak on the success path. --- src/greenlet/tests/_test_extension.c | 6 +++++- src/greenlet/tests/test_extension_interface.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/greenlet/tests/_test_extension.c b/src/greenlet/tests/_test_extension.c index f8fcb9dc..91b9fa67 100644 --- a/src/greenlet/tests/_test_extension.c +++ b/src/greenlet/tests/_test_extension.c @@ -100,6 +100,7 @@ test_setparent(PyObject* UNUSED(self), PyObject* arg) { PyGreenlet* current; PyGreenlet* greenlet = NULL; + PyObject* switch_result = NULL; if (arg == NULL || !PyGreenlet_Check(arg)) { PyErr_BadArgument(); @@ -114,9 +115,12 @@ test_setparent(PyObject* UNUSED(self), PyObject* arg) return NULL; } Py_DECREF(current); - if (PyGreenlet_Switch(greenlet, NULL, NULL) == NULL) { + + switch_result = PyGreenlet_Switch(greenlet, NULL, NULL); + if (switch_result == NULL) { return NULL; } + Py_DECREF(switch_result); Py_RETURN_NONE; } diff --git a/src/greenlet/tests/test_extension_interface.py b/src/greenlet/tests/test_extension_interface.py index 0015ee01..5c908f93 100644 --- a/src/greenlet/tests/test_extension_interface.py +++ b/src/greenlet/tests/test_extension_interface.py @@ -3,6 +3,7 @@ import greenlet from . import _test_extension from . import TestCase +from .leakcheck import ignores_leakcheck # pylint:disable=c-extension-no-member @@ -109,6 +110,7 @@ def test_not_throwable(self): self.assertEqual(str(exc.exception), "exceptions must be classes, or instances, not str") + @ignores_leakcheck def test_leaks(self): from . import RUNNING_ON_FREETHREAD_BUILD iters = 100 From e1fe9f1b079da514046bb40acfafa8411bd827a5 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 09:54:10 -0500 Subject: [PATCH 15/17] _test_extension_cpp.test_exception_switch_and_do_in_g2: fix a reference leak if the greenlet throws a python exception on switch. Test this. --- src/greenlet/tests/_test_extension_cpp.cpp | 1 + src/greenlet/tests/test_cpp.py | 25 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/greenlet/tests/_test_extension_cpp.cpp b/src/greenlet/tests/_test_extension_cpp.cpp index bc3f1783..fc7902f6 100644 --- a/src/greenlet/tests/_test_extension_cpp.cpp +++ b/src/greenlet/tests/_test_extension_cpp.cpp @@ -136,6 +136,7 @@ test_exception_switch_and_do_in_g2(PyObject* UNUSED(self), PyObject* args) try { result = PyGreenlet_Switch(g2, NULL, NULL); if (!result) { + Py_DECREF(g2); return NULL; } } diff --git a/src/greenlet/tests/test_cpp.py b/src/greenlet/tests/test_cpp.py index 2ed057ea..6ad5bcc0 100644 --- a/src/greenlet/tests/test_cpp.py +++ b/src/greenlet/tests/test_cpp.py @@ -1,10 +1,14 @@ +import gc import subprocess import unittest import greenlet -from . import _test_extension_cpp -from . import TestCase +import objgraph + from . import WIN +from . import TestCase +from . import _test_extension_cpp + class CPPTests(TestCase): def test_exception_switch(self): @@ -66,5 +70,22 @@ def test_unhandled_exception_in_greenlet_aborts(self): self._do_test_unhandled_exception('run_unhandled_exception_in_greenlet_aborts') + def test_leak_test_exception_switch_and_do_in_g2(self): + def raiser(): + raise ValueError("boom") + + gc.collect() + before = objgraph.count("greenlet") + + for _ in range(1000): + with self.assertRaises(ValueError): + _test_extension_cpp.test_exception_switch_and_do_in_g2(raiser) + + gc.collect() + after = objgraph.count("greenlet") + leaked = after - before + self.assertEqual(0, leaked) + + if __name__ == '__main__': unittest.main() From 43d48f8f6389320a512d3d9ff8aedc4e237dd88a Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 2 Apr 2026 10:01:25 -0500 Subject: [PATCH 16/17] test_extension_interface/test_leaks: It's not free threaded builds that have a different ref count, it's Python 3.14. We've run into that before, I just forgot. --- src/greenlet/tests/test_extension_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/greenlet/tests/test_extension_interface.py b/src/greenlet/tests/test_extension_interface.py index 5c908f93..4261897c 100644 --- a/src/greenlet/tests/test_extension_interface.py +++ b/src/greenlet/tests/test_extension_interface.py @@ -112,9 +112,9 @@ def test_not_throwable(self): @ignores_leakcheck def test_leaks(self): - from . import RUNNING_ON_FREETHREAD_BUILD + from . import PY314 iters = 100 - if RUNNING_ON_FREETHREAD_BUILD: + if PY314: expected_refs = [1] * iters else: expected_refs = [2] * iters From e10507b3277ac89cd1c1c51776d47d63d265c996 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Sat, 4 Apr 2026 09:23:40 -0500 Subject: [PATCH 17/17] Change note for #502; bump minor version because of potential API changes during shutdown (#499) --- CHANGES.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1f3df3b9..6fdc96df 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,15 +2,25 @@ Changes ========= -3.3.3 (unreleased) +3.4.0 (unreleased) ================== - Publish binary wheels for RiscV 64. -- Fix multiple rare crash paths during interpreter shutdown on. +- Fix multiple rare crash paths during interpreter shutdown. + + Note that this now relies on the ``atexit`` module, and introduces + subtle API changes during interpreter shutdown (for example, + ``getcurrent`` is no longer available once the ``atexit`` callback fires). See `PR #499 `_ by Nicolas Bouvrette. +- Address the results of an automated code audit performed by + devdanzin. This includes several minor correctness changes that + theoretically could have been crashing bugs, but typically only in + very rare circumstances. + + See `PR 502 `_. 3.3.2 (2026-02-20) ==================