You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
PyGILStateEnsure() (Probably) Crashes If Called After Finalization #124619
I'm pretty sure PyGILState_Ensure() will crash if called after finalization, particularly in the case where the thread doesn't already have a thread state active. I noticed this while looking at the function for something else.
Hitting what looks like this same crash in real-world (non-synthetic) code, via PyGObject + GDBus, on Python 3.14.7 — so this is still unfixed on stable 3.14, not just the 3.14.0a7 build from #132948.
Reproducer:powerprofilesctl get (a short-lived Python script shipped with power-profiles-daemon, using PyGObject to talk to power-profiles-daemon over D-Bus). It queries the current profile, prints it, and exits. Invoked frequently (once per status-bar poll interval) it segfaults intermittently with SIGSEGV / SEGV_MAPERR.
coredumpctl info backtrace of the crashing thread (a GDBus worker thread, not the main thread):
Frame #0 is unresolved (no debug symbols on this build), but given PyEval_RestoreThread → PyGILState_Ensure directly above it, it's almost certainly the same _PyThreadState_Attach/bind_gilstate_tstate path as the original report.
At the same instant, the main thread is already inside exit():
Mechanism appears to be: the main thread finishes and calls exit(), kicking off interpreter finalization, while a GDBus-owned worker thread is mid-g_object_unref on a proxy/connection object and its GObject finalizer calls back into Python (PyGObject's _gi), which calls PyGILState_Ensure() on a thread state that finalization has already begun tearing down.
Reproduced twice on this machine so far (identical stack both times), a few days apart, under ordinary desktop use — no synthetic thread-spam required, just a program that talks to D-Bus from a background thread and exits promptly afterward. Given power-profiles-daemon, NetworkManager's Python bindings, and plenty of other GLib/GDBus-using Python tools follow this same "spin up async D-Bus, do one thing, exit" pattern, this is likely to affect more than just this one tool as more distros ship 3.14.
We hit this in a Rust/PyO3 extension (russfellows/s3dlio#302) and narrowed it to a pure C reproducer with no Rust or PyO3 involved. The results add something to this issue: 3.13.8+ survives this pattern and 3.14 does not, so it is a regression in 3.14, not only a long-standing API hazard.
Reproducer
N native (non-Python) threads loop on PyGILState_Ensure() / small Python work / PyGILState_Release(), with a short random sleep between attaches, while the main thread returns and the interpreter finalizes. Many objects with __del__ keep bytecode running during finalization.
23/30 and 25/30 in two batches: double free or corruption (out), SIGSEGV, Fatal Python error: PyInterpreterState_Delete: remaining threads
3.13.18
0/30
3.13.7, 3.12.16
0/30 (from our investigation's runs)
Analysis (3.14.10 vs 3.13.18 sources)
_Py_Finalize marks only the thread states that already exist._PyThreadState_RemoveExcept() takes the list of thread states, and only those get _PyThreadState_SetShuttingDown() (pylifecycle.c ~2092). The comment at ~2118 notes the gap: "XXX Make sure we are preventing the creating of any new thread states".
New thread states are still created. A native thread calling PyGILState_Ensure() during finalization has no thread state, so new_threadstate() creates a fresh one with no finalization check. On a default (GIL) build _PyEval_StopTheWorldAll is a no-op.
In 3.14, take_gil parks only flagged thread states. It decides via _PyThreadState_MustExit(tstate), which checks the thread state's own shutting-down flag. The fresh thread state is not flagged, so the thread proceeds and later races PyInterpreterState_Delete / zapthreads. That race produces the three failure modes above:
a SIGSEGV in _PyThreadState_Attach while zapthreads frees the thread state;
a double free when the thread's own PyGILState_Release frees it too;
"remaining threads" when the thread state is created after zapthreads ran.
The thread-state-based check in 3.14 appears to have lost the "late native thread" case that 3.13.8+ handles. Refusing (or parking) thread-state creation in PyGILState_Ensure once finalization has started would close it.
Workaround on our side: no native thread attaches during shutdown; Python objects are built only on Python threads. Happy to test a patch.
Activity
PyGILState_Ensurecalled from background thread during finalization on Python 3.14.0a7 #132948 as a duplicate of this issueSccucci84 commentedon Sep 1, 2026
Hitting what looks like this same crash in real-world (non-synthetic) code, via PyGObject + GDBus, on Python 3.14.7 — so this is still unfixed on stable 3.14, not just the 3.14.0a7 build from #132948.
Reproducer:
powerprofilesctl get(a short-lived Python script shipped withpower-profiles-daemon, using PyGObject to talk topower-profiles-daemonover D-Bus). It queries the current profile, prints it, and exits. Invoked frequently (once per status-bar poll interval) it segfaults intermittently withSIGSEGV/SEGV_MAPERR.coredumpctl infobacktrace of the crashing thread (a GDBus worker thread, not the main thread):Frame #0 is unresolved (no debug symbols on this build), but given
PyEval_RestoreThread→PyGILState_Ensuredirectly above it, it's almost certainly the same_PyThreadState_Attach/bind_gilstate_tstatepath as the original report.At the same instant, the main thread is already inside
exit():Mechanism appears to be: the main thread finishes and calls
exit(), kicking off interpreter finalization, while a GDBus-owned worker thread is mid-g_object_unrefon a proxy/connection object and its GObject finalizer calls back into Python (PyGObject's_gi), which callsPyGILState_Ensure()on a thread state that finalization has already begun tearing down.Reproduced twice on this machine so far (identical stack both times), a few days apart, under ordinary desktop use — no synthetic thread-spam required, just a program that talks to D-Bus from a background thread and exits promptly afterward. Given
power-profiles-daemon,NetworkManager's Python bindings, and plenty of other GLib/GDBus-using Python tools follow this same "spin up async D-Bus, do one thing, exit" pattern, this is likely to affect more than just this one tool as more distros ship 3.14.Poll the active power profile over D-Bus instead of powerprofilesctl
russfellows commentedon Oct 30, 2026
We hit this in a Rust/PyO3 extension (russfellows/s3dlio#302) and narrowed it to a pure C reproducer with no Rust or PyO3 involved. The results add something to this issue: 3.13.8+ survives this pattern and 3.14 does not, so it is a regression in 3.14, not only a long-standing API hazard.
Reproducer
N native (non-Python) threads loop on
PyGILState_Ensure()/ small Python work /PyGILState_Release(), with a short random sleep between attaches, while the main thread returns and the interpreter finalizes. Many objects with__del__keep bytecode running during finalization.attacher.c (build with
gcc -O2 -shared -fPIC -I$(python3 -c 'import sysconfig;print(sysconfig.get_path("include"))') attacher.c -o attacher.so):drv.py (run:
python3 drv.py ./attacher.so 64, where 64 is the number of native threads):Results (Linux x86_64 glibc, default GIL build, 64 native threads, 30 runs each)
double free or corruption (out), SIGSEGV,Fatal Python error: PyInterpreterState_Delete: remaining threadsAnalysis (3.14.10 vs 3.13.18 sources)
_Py_Finalizemarks only the thread states that already exist._PyThreadState_RemoveExcept()takes the list of thread states, and only those get_PyThreadState_SetShuttingDown()(pylifecycle.c ~2092). The comment at ~2118 notes the gap: "XXX Make sure we are preventing the creating of any new thread states".PyGILState_Ensure()during finalization has no thread state, sonew_threadstate()creates a fresh one with no finalization check. On a default (GIL) build_PyEval_StopTheWorldAllis a no-op.take_gilparks only flagged thread states. It decides via_PyThreadState_MustExit(tstate), which checks the thread state's own shutting-down flag. The fresh thread state is not flagged, so the thread proceeds and later racesPyInterpreterState_Delete/zapthreads. That race produces the three failure modes above:_PyThreadState_Attachwhile zapthreads frees the thread state;PyGILState_Releasefrees it too;_PyThreadState_MustExitchecks globally whether the runtime is finalizing, so the late thread state is parked too (pthread_exit & PyThread_exit_thread from PyEval_RestoreThread etc. are harmful #87135 backport), and there is no crash.The thread-state-based check in 3.14 appears to have lost the "late native thread" case that 3.13.8+ handles. Refusing (or parking) thread-state creation in
PyGILState_Ensureonce finalization has started would close it.Workaround on our side: no native thread attaches during shutdown; Python objects are built only on Python threads. Happy to test a patch.