From 9fc498fdcf774a819b47c2a3271a7a1f322af0b9 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Aug 2026 09:48:19 -0700 Subject: [PATCH] gh-155363: Fix QSBR slot leak on thread state creation failure (gh-155365) 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. Restructure code so failure path doesn't leak. (cherry picked from commit 44e92d4a5922dd8fa61f75834784c2772fdd34ab) Co-authored-by: Neil Schemenauer --- Include/internal/pycore_code.h | 4 ++++ ...026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst | 4 ++++ Objects/codeobject.c | 12 +++++++++--- Python/pystate.c | 16 +++++++++------- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst 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/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.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-155363.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/Objects/codeobject.c b/Objects/codeobject.c index 03036020b1cb1ae..d4c96b36e3c8b5b 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -3313,14 +3313,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 8349df1b573952d..d79006639ab4529 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1638,21 +1638,23 @@ new_threadstate(PyInterpreterState *interp, int whence) return NULL; } -#ifdef Py_GIL_DISABLED - Py_ssize_t qsbr_idx = _Py_qsbr_reserve(interp); - if (qsbr_idx < 0) { +#ifdef Py_STATS + // The PyStats structure is quite large and is allocated separated from + // tstate. + if (!_PyStats_ThreadInit(interp, tstate)) { free_threadstate(tstate); return NULL; } +#endif +#ifdef Py_GIL_DISABLED 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)) { + Py_ssize_t qsbr_idx = _Py_qsbr_reserve(interp); + if (qsbr_idx < 0) { + _Py_UnreserveTLBCIndex(interp, tlbc_idx); free_threadstate(tstate); return NULL; }