Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -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},
Expand Down
Loading