From 733428648e0634caafdb698a9df7cb1204a71f39 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 7 Aug 2026 13:43:02 -0700 Subject: [PATCH] gh-155363: Fix QSBR slot leak on thread state creation failure In the free-threaded build, new_threadstate() reserves a QSBR thread-state slot before it can still fail for other reasons, but the failure paths only called free_threadstate(), which does not know about the reservation. --- Include/internal/pycore_qsbr.h | 5 +++++ ...-08-07-13-40-12.gh-issue-999999.Qk3Vt9.rst | 4 ++++ Python/pystate.c | 19 ++++++++++++------- Python/qsbr.c | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-999999.Qk3Vt9.rst diff --git a/Include/internal/pycore_qsbr.h b/Include/internal/pycore_qsbr.h index eeca6fc472be37b..fbc430c6d2044f7 100644 --- a/Include/internal/pycore_qsbr.h +++ b/Include/internal/pycore_qsbr.h @@ -152,6 +152,11 @@ _Py_qsbr_detach(struct _qsbr_thread_state *qsbr); extern Py_ssize_t _Py_qsbr_reserve(PyInterpreterState *interp); +// Frees a QSBR state reserved by _Py_qsbr_reserve() that was never associated +// with a PyThreadState by _Py_qsbr_register(). +extern void +_Py_qsbr_unreserve(PyInterpreterState *interp, Py_ssize_t index); + // Associates a PyThreadState with the QSBR state at the given index extern void _Py_qsbr_register(struct _PyThreadStateImpl *tstate, diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-999999.Qk3Vt9.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-999999.Qk3Vt9.rst new file mode 100644 index 000000000000000..52200bb9d2fd59f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-999999.Qk3Vt9.rst @@ -0,0 +1,4 @@ +Fix a leak in the :term:`free-threaded build` when creating a thread state +fails after an internal QSBR slot has been reserved for it. The slot could +never be reclaimed, so the QSBR array grew without bound across repeated +failures. diff --git a/Python/pystate.c b/Python/pystate.c index d10b38def32911d..df1cd0883c01f35 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1667,6 +1667,15 @@ new_threadstate(PyInterpreterState *interp, int whence) return NULL; } +#ifdef Py_STATS + // The PyStats structure is quite large and is allocated separated from + // tstate. This is done before reserving the QSBR and TLBC indices below + // so that a failure here does not have to give them back. + if (!_PyStats_ThreadInit(interp, tstate)) { + free_threadstate(tstate); + return NULL; + } +#endif #ifdef Py_GIL_DISABLED Py_ssize_t qsbr_idx = _Py_qsbr_reserve(interp); if (qsbr_idx < 0) { @@ -1675,13 +1684,9 @@ new_threadstate(PyInterpreterState *interp, int whence) } int32_t tlbc_idx = _Py_ReserveTLBCIndex(interp); if (tlbc_idx < 0) { - free_threadstate(tstate); - return NULL; - } -#endif -#ifdef Py_STATS - // The PyStats structure is quite large and is allocated separated from tstate. - if (!_PyStats_ThreadInit(interp, tstate)) { + // free_threadstate() does not know about the QSBR entry, which is only + // reclaimed by _Py_qsbr_unregister() once _Py_qsbr_register() has run. + _Py_qsbr_unreserve(interp, qsbr_idx); free_threadstate(tstate); return NULL; } diff --git a/Python/qsbr.c b/Python/qsbr.c index e9d935bfb40d84e..a3b3530c0b210cd 100644 --- a/Python/qsbr.c +++ b/Python/qsbr.c @@ -217,6 +217,25 @@ _Py_qsbr_reserve(PyInterpreterState *interp) return index; } +void +_Py_qsbr_unreserve(PyInterpreterState *interp, Py_ssize_t index) +{ + struct _qsbr_shared *shared = &interp->qsbr; + + PyMutex_Lock(&shared->mutex); + // NOTE: we must load shared->array inside the mutex because the array may + // have been resized since the entry was reserved. The index remains + // valid: grow_thread_array() preserves the position of existing entries. + struct _qsbr_thread_state *qsbr = &shared->array[index].qsbr; + + assert(qsbr->allocated && qsbr->tstate == NULL); + + qsbr->allocated = false; + qsbr->freelist_next = shared->freelist; + shared->freelist = qsbr; + PyMutex_Unlock(&shared->mutex); +} + void _Py_qsbr_register(_PyThreadStateImpl *tstate, PyInterpreterState *interp, Py_ssize_t index)