aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Python/pylifecycle.c
diff options
context:
space:
mode:
authorSam Gross <colesbury@gmail.com>2025-03-06 10:38:34 -0500
committerGitHub <noreply@github.com>2025-03-06 10:38:34 -0500
commit052cb717f5f97d08d2074f4118fd2c21224d3015 (patch)
treeccdcdd01ce37cd3ae2530ca54a476486a73a1c65 /Python/pylifecycle.c
parentc6dd2348ca61436fc1444ecc0343cb24932f6fa7 (diff)
downloadcpython-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/pylifecycle.c')
-rw-r--r--Python/pylifecycle.c19
1 files changed, 12 insertions, 7 deletions
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index 1a7f312a604..4007e43c98d 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -2036,18 +2036,23 @@ _Py_Finalize(_PyRuntimeState *runtime)
// XXX Call something like _PyImport_Disable() here?
- /* Destroy the state of all threads of the interpreter, except of the
+ /* Remove the state of all threads of the interpreter, except for the
current thread. In practice, only daemon threads should still be alive,
except if wait_for_thread_shutdown() has been cancelled by CTRL+C.
- Clear frames of other threads to call objects destructors. Destructors
- will be called in the current Python thread. Since
- _PyRuntimeState_SetFinalizing() has been called, no other Python thread
- can take the GIL at this point: if they try, they will exit
- immediately. We start the world once we are the only thread state left,
+ We start the world once we are the only thread state left,
before we call destructors. */
PyThreadState *list = _PyThreadState_RemoveExcept(tstate);
+ for (PyThreadState *p = list; p != NULL; p = p->next) {
+ _PyThreadState_SetShuttingDown(p);
+ }
_PyEval_StartTheWorldAll(runtime);
- _PyThreadState_DeleteList(list);
+
+ /* Clear frames of other threads to call objects destructors. Destructors
+ will be called in the current Python thread. Since
+ _PyRuntimeState_SetFinalizing() has been called, no other Python thread
+ can take the GIL at this point: if they try, they will hang in
+ _PyThreadState_HangThread. */
+ _PyThreadState_DeleteList(list, /*is_after_fork=*/0);
/* At this point no Python code should be running at all.
The only thread state left should be the main thread of the main