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
5 changes: 5 additions & 0 deletions Include/internal/pycore_qsbr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 12 additions & 7 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
19 changes: 19 additions & 0 deletions Python/qsbr.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading