From 733428648e0634caafdb698a9df7cb1204a71f39 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 7 Aug 2026 13:43:02 -0700 Subject: [PATCH 1/2] 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) From dc7d2077992ec4c83d82d93e2e5a615c81f2f2ee Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 10 Aug 2026 10:57:53 -0700 Subject: [PATCH 2/2] Re-factor, move _Py_ReserveTLBCIndex call. By moving the call before the _Py_qsbr_reserve, we don't need the _Py_qsbr_unreserve helper and code is a bit simpler. --- Include/internal/pycore_code.h | 4 ++++ Include/internal/pycore_qsbr.h | 5 ----- ...08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst} | 0 Objects/codeobject.c | 12 +++++++++--- Python/pystate.c | 15 ++++++--------- Python/qsbr.c | 19 ------------------- 6 files changed, 19 insertions(+), 36 deletions(-) rename Misc/NEWS.d/next/Core_and_Builtins/{2026-08-07-13-40-12.gh-issue-999999.Qk3Vt9.rst => 2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst} (100%) diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 5b1fddbe15b98b8..32242f89b812e69 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -582,6 +582,10 @@ PyAPI_FUNC(_Py_CODEUNIT *) _PyCode_GetTLBC(PyCodeObject *co); // Returns the reserved index or -1 on error. extern int32_t _Py_ReserveTLBCIndex(PyInterpreterState *interp); +// Release an index returned by _Py_ReserveTLBCIndex() that was never stored +// in a PyThreadState. +extern void _Py_UnreserveTLBCIndex(PyInterpreterState *interp, int32_t index); + // Release the current thread's index into thread-local bytecode arrays extern void _Py_ClearTLBCIndex(_PyThreadStateImpl *tstate); diff --git a/Include/internal/pycore_qsbr.h b/Include/internal/pycore_qsbr.h index fbc430c6d2044f7..eeca6fc472be37b 100644 --- a/Include/internal/pycore_qsbr.h +++ b/Include/internal/pycore_qsbr.h @@ -152,11 +152,6 @@ _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-155363.Qk3Vt9.rst similarity index 100% rename from Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-999999.Qk3Vt9.rst rename to Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst diff --git a/Objects/codeobject.c b/Objects/codeobject.c index d7955cc7390a7ab..58811d63c7e318c 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -3314,14 +3314,20 @@ _Py_ReserveTLBCIndex(PyInterpreterState *interp) } void -_Py_ClearTLBCIndex(_PyThreadStateImpl *tstate) +_Py_UnreserveTLBCIndex(PyInterpreterState *interp, int32_t index) { - PyInterpreterState *interp = ((PyThreadState *)tstate)->interp; if (interp->config.tlbc_enabled) { - _PyIndexPool_FreeIndex(&interp->tlbc_indices, tstate->tlbc_index); + _PyIndexPool_FreeIndex(&interp->tlbc_indices, index); } } +void +_Py_ClearTLBCIndex(_PyThreadStateImpl *tstate) +{ + PyInterpreterState *interp = ((PyThreadState *)tstate)->interp; + _Py_UnreserveTLBCIndex(interp, tstate->tlbc_index); +} + static _PyCodeArray * _PyCodeArray_New(Py_ssize_t size) { diff --git a/Python/pystate.c b/Python/pystate.c index df1cd0883c01f35..646c157007d4ac1 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1669,24 +1669,21 @@ new_threadstate(PyInterpreterState *interp, int whence) #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. + // tstate. 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) { + int32_t tlbc_idx = _Py_ReserveTLBCIndex(interp); + if (tlbc_idx < 0) { free_threadstate(tstate); return NULL; } - int32_t tlbc_idx = _Py_ReserveTLBCIndex(interp); - if (tlbc_idx < 0) { - // 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); + Py_ssize_t qsbr_idx = _Py_qsbr_reserve(interp); + if (qsbr_idx < 0) { + _Py_UnreserveTLBCIndex(interp, tlbc_idx); free_threadstate(tstate); return NULL; } diff --git a/Python/qsbr.c b/Python/qsbr.c index a3b3530c0b210cd..e9d935bfb40d84e 100644 --- a/Python/qsbr.c +++ b/Python/qsbr.c @@ -217,25 +217,6 @@ _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)