From dee85ffc8d5c2a9e410dae4ffa8bdec28a243535 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Mon, 14 Sep 2026 21:24:47 +0200 Subject: [PATCH] gh-157519: Don't defer deallocation on an unknown stack `_Py_RecursionLimit_GetMargin()` now reports plenty of margin when the stack pointer is far outside the stack limits, the same rule `_Py_CheckRecursiveCall()` uses to detect stack switching. Previously `_Py_Dealloc()` deferred every GC object to the trash queue on such a stack and the chain was never destroyed. --- Include/internal/pycore_pystate.h | 9 ++++ ...-09-14-18-30-00.gh-issue-157519.Kq7Tsz.rst | 5 ++ Modules/_testinternalcapi.c | 46 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-14-18-30-00.gh-issue-157519.Kq7Tsz.rst diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 253d26fe3a3cd8..ed6fb51e5e951c 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -339,8 +339,17 @@ _Py_RecursionLimit_GetMargin(PyThreadState *tstate) assert(_tstate->c_stack_hard_limit != 0); intptr_t here_addr = _Py_get_machine_stack_pointer(); #if _Py_STACK_GROWS_DOWN + if (here_addr < (intptr_t)(_tstate->c_stack_hard_limit - _PyOS_STACK_MARGIN_BYTES)) { + // Far out of bounds -> assume stack switching has occurred. + // Report plenty of margin so that _Py_Dealloc() does not defer + // objects to a chain that would never be destroyed. + return _PyOS_STACK_MARGIN; + } return Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, here_addr - (intptr_t)_tstate->c_stack_soft_limit, _PyOS_STACK_MARGIN_SHIFT); #else + if (here_addr > (intptr_t)(_tstate->c_stack_hard_limit + _PyOS_STACK_MARGIN_BYTES)) { + return _PyOS_STACK_MARGIN; + } return Py_ARITHMETIC_RIGHT_SHIFT(intptr_t, (intptr_t)_tstate->c_stack_soft_limit - here_addr, _PyOS_STACK_MARGIN_SHIFT); #endif } diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-14-18-30-00.gh-issue-157519.Kq7Tsz.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-14-18-30-00.gh-issue-157519.Kq7Tsz.rst new file mode 100644 index 00000000000000..391a1b7724e400 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-14-18-30-00.gh-issue-157519.Kq7Tsz.rst @@ -0,0 +1,5 @@ +Fix a memory leak when Python runs on a C stack the interpreter does not know +about, such as user-space threads, fibers or coroutines. :c:func:`Py_DECREF` +deferred the deallocation of every garbage-collected object to the trashcan +and never freed them. Objects are now deallocated immediately on such stacks, +matching how the C recursion check already treats them. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 38e56ae7042098..51a85c51cb847a 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -3082,6 +3082,51 @@ test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } + +// gh-157519: _Py_Dealloc() must not defer objects to tstate->delete_later +// when the stack pointer is outside the known stack (user-space threads), +// since the chain would never be destroyed. +static PyObject * +test_dealloc_on_unknown_stack(PyObject *self, PyObject *Py_UNUSED(args)) +{ + PyThreadState *tstate = PyThreadState_GET(); + assert(!PyErr_Occurred()); + if (tstate->delete_later != NULL) { + PyErr_SetString(PyExc_AssertionError, + "delete_later is not empty before the test"); + return NULL; + } + + // Set the stack limits far away from the actual stack pointer + size_t size = _PyOS_MIN_STACK_SIZE; + uintptr_t here_addr = _Py_get_machine_stack_pointer(); +#if _Py_STACK_GROWS_DOWN + void *start = (void *)(here_addr + 64 * _PyOS_STACK_MARGIN_BYTES); +#else + void *start = (void *)(here_addr - 64 * _PyOS_STACK_MARGIN_BYTES - size); +#endif + if (PyUnstable_ThreadState_SetStackProtection(tstate, start, size) < 0) { + return NULL; + } + + PyObject *result = NULL; + PyObject *list = PyList_New(0); + if (list == NULL) { + goto done; + } + Py_DECREF(list); + if (tstate->delete_later != NULL) { + PyErr_SetString(PyExc_AssertionError, + "_Py_Dealloc() deferred an object on an unknown stack"); + goto done; + } + result = Py_NewRef(Py_None); + +done: + PyUnstable_ThreadState_ResetStackProtection(tstate); + return result; +} + #define NUM_GUARDS 100 static PyObject * @@ -3389,6 +3434,7 @@ static PyMethodDef module_functions[] = { {"module_get_gc_hooks", module_get_gc_hooks, METH_O}, {"test_threadstate_set_stack_protection", test_threadstate_set_stack_protection, METH_NOARGS}, + {"test_dealloc_on_unknown_stack", test_dealloc_on_unknown_stack, METH_NOARGS}, {"_pyerr_setkeyerror", _pyerr_setkeyerror, METH_O}, {"test_interp_guard_countdown", test_interp_guard_countdown, METH_NOARGS}, {"test_interp_view_countdown", test_interp_view_countdown, METH_NOARGS},