Repository navigation
sys._setprofileallthreads race condition #137400
Copy link
Copy link
Closed
Labels
3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixes3.15bugs and security fixesbugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
There's a race on tstate->c_profilefunc if profiling is disable concurrently via sys._setprofileallthreads or threading.setprofile_all_threads or PyEval_SetProfileAllThreads.
cpython/Python/legacy_tracing.c
Lines 37 to 57 in 001461a
| static PyObject * | |
| call_profile_func(_PyLegacyEventHandler *self, PyObject *arg) | |
| { | |
| PyThreadState *tstate = _PyThreadState_GET(); | |
| if (tstate->c_profilefunc == NULL) { | |
| Py_RETURN_NONE; | |
| } | |
| PyFrameObject *frame = PyEval_GetFrame(); | |
| if (frame == NULL) { | |
| PyErr_SetString(PyExc_SystemError, | |
| "Missing frame when calling profile function."); | |
| return NULL; | |
| } | |
| Py_INCREF(frame); | |
| int err = tstate->c_profilefunc(tstate->c_profileobj, frame, self->event, arg); | |
| Py_DECREF(frame); | |
| if (err) { | |
| return NULL; | |
| } | |
| Py_RETURN_NONE; | |
| } |
Repro
import sys
import threading
done = threading.Event()
def foo():
pass
def my_profile(frame, event, arg):
return None
def bg_thread():
while not done.is_set():
foo()
foo()
foo()
foo()
foo()
foo()
foo()
foo()
foo()
foo()
def main():
bg_threads = []
for i in range(10):
t = threading.Thread(target=bg_thread)
t.start()
bg_threads.append(t)
for i in range(100):
print(f"Iteration {i}")
sys._setprofileallthreads(my_profile)
sys._setprofileallthreads(None)
done.set()
for t in bg_threads:
t.join()
if __name__ == "__main__":
main()Linked PRs
- gh-137400: Fix a crash when disabling profiling across all threads #137471
- gh-137400: Stop the world when swapping profile functions #137472
- gh-137400: Fix thread-safety issues when profiling all threads #137518
- [3.14] gh-137400: Fix a crash when disabling profiling across all threads (gh-137471) #137648
- [3.13] gh-137400: Fix a crash when disabling profiling across all threads (gh-137471) #137649
- [3.14] gh-137400: Fix thread-safety issues when profiling all threads (gh-137518) #137730
- [3.13] gh-137400: Fix thread-safety issues when profiling all threads (gh-137518) #137733
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixes3.15bugs and security fixesbugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
colesbury commentedon Aug 5, 2025
cc @pablogsal
xuantengh commentedon Aug 5, 2025
Seems like this could be fixed by adding
LOCK_SETUPandUNLOCK_SETUPbefore and after C profile function calls to prevent concurrent modifications. If this approach is acceptable, I may raise an PR for that.colesbury commentedon Aug 5, 2025
No, we don't want to lock before every C profile function call. We need to refactor how we install/uninstall trace hooks.
ZeroIntensity commentedon Aug 5, 2025
I see two options that shouldn't kill performance:
c_profilefuncandc_profileobjreads and writes to atomic operations, and then load them each a single time incall_profile_func.call_profile_func, where_PyEval_SetProfilehas exclusive access.Which do you think makes the most sense?
colesbury commentedon Aug 5, 2025
I think the eventual goal should be to perform all the setup under:
HEAD_LOCK(runtime)for things likePyEval_SetProfileAllThreadsthat currently iterate over all threads. Currently the iteration is unsafe (even with the GIL) and may crash if a thread concurrently terminates.That means we would need to refactor things so that:
_PySys_Auditcalls happen outside the stop the world and lockNear term, we may want to consider a smaller change where we just add another stop-the-world pause around the modifications to
tstate->c_tracefuncand similar.godlygeek commentedon Aug 5, 2025
This small patch seems to be enough to fix the crash in your reproducer (and in Memray, too!):
But that pays the cost of a stop-the-world even if we're setting the profile function for our own attached thread state, and still accesses
tstate->interp->sys_profile_initializedfrom a different thread without holding a lock... Maybe we need something more like this?colesbury commentedon Aug 6, 2025
I think the simpler change is probably okay for now, even at the cost of an extra stop-the-world pause.
Later on, I'll refactor it so that
PyEval_SetProfileAllThreadsstops the world once instead of 1xNTHREADS or 2xNTHREADS.It's currently within a
LOCK_SETUP()UNLOCK_SETUP()call, so I think it's okay.pablogsal commentedon Aug 6, 2025
Yeah I think this is fine as this happens only on setting/unsetting and is simpler to reason about. Maybe a bit trickier at finalization....
pythongh-137400: Fix a crash when disabling profiling across all threads
11 remaining items
Merge branch 'main' into pythongh-137400-now-with-fewer-crashes
[3.13] gh-137400: Fix a crash when disabling profiling across all thr…
[3.14] gh-137400: Fix a crash when disabling profiling across all thr…
hugovk commentedon Aug 12, 2025
Yep, backport merged: #137648
Merge branch 'main' into csv-sniffer-counter-set
gh-137400: Fix thread-safety issues when profiling all threads (gh-13…
[3.14] pythongh-137400: Fix thread-safety issues when profiling all t…
[3.13] pythongh-137400: Fix thread-safety issues when profiling all t…
pythongh-137400: Fix a crash when disabling profiling across all thre…
pythongh-137400: Fix thread-safety issues when profiling all threads (p…
[3.14] pythongh-137400: Fix a crash when disabling profiling across a…
[3.14] gh-137400: Fix thread-safety issues when profiling all threads (…
[3.13] gh-137400: Fix thread-safety issues when profiling all threads (…