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.
| 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