diff options
author | Sam Gross <colesbury@gmail.com> | 2025-03-06 10:38:34 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-06 10:38:34 -0500 |
commit | 052cb717f5f97d08d2074f4118fd2c21224d3015 (patch) | |
tree | ccdcdd01ce37cd3ae2530ca54a476486a73a1c65 /Python/pystate.c | |
parent | c6dd2348ca61436fc1444ecc0343cb24932f6fa7 (diff) | |
download | cpython-052cb717f5f97d08d2074f4118fd2c21224d3015.tar.gz cpython-052cb717f5f97d08d2074f4118fd2c21224d3015.zip |
gh-124878: Fix race conditions during interpreter finalization (#130649)
The PyThreadState field gains a reference count field to avoid
issues with PyThreadState being a dangling pointer to freed memory.
The refcount starts with a value of two: one reference is owned by the
interpreter's linked list of thread states and one reference is owned by
the OS thread. The reference count is decremented when the thread state
is removed from the interpreter's linked list and before the OS thread
calls `PyThread_hang_thread()`. The thread that decrements it to zero
frees the `PyThreadState` memory.
The `holds_gil` field is moved out of the `_status` bit field, to avoid
a data race where on thread calls `PyThreadState_Clear()`, modifying the
`_status` bit field while the OS thread reads `holds_gil` when
attempting to acquire the GIL.
The `PyThreadState.state` field now has `_Py_THREAD_SHUTTING_DOWN` as a
possible value. This corresponds to the `_PyThreadState_MustExit()`
check. This avoids race conditions in the free threading build when
checking `_PyThreadState_MustExit()`.
Diffstat (limited to 'Python/pystate.c')
-rw-r--r-- | Python/pystate.c | 96 |
1 files changed, 57 insertions, 39 deletions
diff --git a/Python/pystate.c b/Python/pystate.c index 4b01942f40e..fcd12d1b933 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1474,6 +1474,15 @@ free_threadstate(_PyThreadStateImpl *tstate) } } +static void +decref_threadstate(_PyThreadStateImpl *tstate) +{ + if (_Py_atomic_add_ssize(&tstate->refcount, -1) == 1) { + // The last reference to the thread state is gone. + free_threadstate(tstate); + } +} + /* Get the thread state to a minimal consistent state. Further init happens in pylifecycle.c before it can be used. All fields not initialized here are expected to be zeroed out, @@ -1938,8 +1947,12 @@ _PyThreadState_RemoveExcept(PyThreadState *tstate) // Deletes the thread states in the linked list `list`. // // This is intended to be used in conjunction with _PyThreadState_RemoveExcept. +// +// If `is_after_fork` is true, the thread states are immediately freed. +// Otherwise, they are decref'd because they may still be referenced by an +// OS thread. void -_PyThreadState_DeleteList(PyThreadState *list) +_PyThreadState_DeleteList(PyThreadState *list, int is_after_fork) { // The world can't be stopped because we PyThreadState_Clear() can // call destructors. @@ -1949,7 +1962,12 @@ _PyThreadState_DeleteList(PyThreadState *list) for (p = list; p; p = next) { next = p->next; PyThreadState_Clear(p); - free_threadstate((_PyThreadStateImpl *)p); + if (is_after_fork) { + free_threadstate((_PyThreadStateImpl *)p); + } + else { + decref_threadstate((_PyThreadStateImpl *)p); + } } } @@ -2082,12 +2100,19 @@ static void tstate_wait_attach(PyThreadState *tstate) { do { - int expected = _Py_THREAD_SUSPENDED; - - // Wait until we're switched out of SUSPENDED to DETACHED. - _PyParkingLot_Park(&tstate->state, &expected, sizeof(tstate->state), - /*timeout=*/-1, NULL, /*detach=*/0); - + int state = _Py_atomic_load_int_relaxed(&tstate->state); + if (state == _Py_THREAD_SUSPENDED) { + // Wait until we're switched out of SUSPENDED to DETACHED. + _PyParkingLot_Park(&tstate->state, &state, sizeof(tstate->state), + /*timeout=*/-1, NULL, /*detach=*/0); + } + else if (state == _Py_THREAD_SHUTTING_DOWN) { + // We're shutting down, so we can't attach. + _PyThreadState_HangThread(tstate); + } + else { + assert(state == _Py_THREAD_DETACHED); + } // Once we're back in DETACHED we can re-attach } while (!tstate_try_attach(tstate)); } @@ -2118,7 +2143,7 @@ _PyThreadState_Attach(PyThreadState *tstate) tstate_activate(tstate); #ifdef Py_GIL_DISABLED - if (_PyEval_IsGILEnabled(tstate) && !tstate->_status.holds_gil) { + if (_PyEval_IsGILEnabled(tstate) && !tstate->holds_gil) { // The GIL was enabled between our call to _PyEval_AcquireLock() // and when we attached (the GIL can't go from enabled to disabled // here because only a thread holding the GIL can disable @@ -2201,6 +2226,15 @@ _PyThreadState_Suspend(PyThreadState *tstate) HEAD_UNLOCK(runtime); } +void +_PyThreadState_SetShuttingDown(PyThreadState *tstate) +{ + _Py_atomic_store_int(&tstate->state, _Py_THREAD_SHUTTING_DOWN); +#ifdef Py_GIL_DISABLED + _PyParkingLot_UnparkAll(&tstate->state); +#endif +} + // Decrease stop-the-world counter of remaining number of threads that need to // pause. If we are the final thread to pause, notify the requesting thread. static void @@ -3001,43 +3035,27 @@ _PyThreadState_CheckConsistency(PyThreadState *tstate) #endif -// Check if a Python thread must exit immediately, rather than taking the GIL -// if Py_Finalize() has been called. +// Check if a Python thread must call _PyThreadState_HangThread(), rather than +// taking the GIL or attaching to the interpreter if Py_Finalize() has been +// called. // // When this function is called by a daemon thread after Py_Finalize() has been -// called, the GIL does no longer exist. -// -// tstate can be a dangling pointer (point to freed memory): only tstate value -// is used, the pointer is not deferenced. +// called, the GIL may no longer exist. // // tstate must be non-NULL. int _PyThreadState_MustExit(PyThreadState *tstate) { - /* bpo-39877: Access _PyRuntime directly rather than using - tstate->interp->runtime to support calls from Python daemon threads. - After Py_Finalize() has been called, tstate can be a dangling pointer: - point to PyThreadState freed memory. */ - unsigned long finalizing_id = _PyRuntimeState_GetFinalizingID(&_PyRuntime); - PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime); - if (finalizing == NULL) { - // XXX This isn't completely safe from daemon thraeds, - // since tstate might be a dangling pointer. - finalizing = _PyInterpreterState_GetFinalizing(tstate->interp); - finalizing_id = _PyInterpreterState_GetFinalizingID(tstate->interp); - } - // XXX else check &_PyRuntime._main_interpreter._initial_thread - if (finalizing == NULL) { - return 0; - } - else if (finalizing == tstate) { - return 0; - } - else if (finalizing_id == PyThread_get_thread_ident()) { - /* gh-109793: we must have switched interpreters. */ - return 0; - } - return 1; + int state = _Py_atomic_load_int_relaxed(&tstate->state); + return state == _Py_THREAD_SHUTTING_DOWN; +} + +void +_PyThreadState_HangThread(PyThreadState *tstate) +{ + _PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate; + decref_threadstate(tstate_impl); + PyThread_hang_thread(); } /********************/ |