Skip to content

Commit 4c8fa53

Browse files
committed
[3.13] pythongh-137400: Fix thread-safety issues when profiling all threads (pythongh-137518)
There were a few thread-safety issues when profiling or tracing all threads via PyEval_SetProfileAllThreads or PyEval_SetTraceAllThreads: * The loop over thread states could crash if a thread exits concurrently (in both the free threading and default build) * The modification of `c_profilefunc` and `c_tracefunc` wasn't thread-safe on the free threading build. (cherry picked from commit a10152f) Co-authored-by: Sam Gross <[email protected]>
1 parent 9417ea5 commit 4c8fa53

File tree

10 files changed

+379
-228
lines changed

10 files changed

+379
-228
lines changed

Include/internal/pycore_ceval.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ struct _ceval_runtime_state;
1919

2020
// Export for '_lsprof' shared extension
2121
PyAPI_FUNC(int) _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
22+
extern int _PyEval_SetProfileAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyObject *arg);
2223

2324
extern int _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
25+
extern int _PyEval_SetTraceAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyObject *arg);
2426

2527
extern int _PyEval_SetOpcodeTrace(PyFrameObject *f, bool enable);
2628

Include/internal/pycore_interp.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,8 @@ struct _is {
262262
PyDict_WatchCallback builtins_dict_watcher;
263263

264264
_Py_GlobalMonitors monitors;
265-
bool sys_profile_initialized;
266-
bool sys_trace_initialized;
265+
_PyOnceFlag sys_profile_once_flag;
266+
_PyOnceFlag sys_trace_once_flag;
267267
Py_ssize_t sys_profiling_threads; /* Count of threads with c_profilefunc set */
268268
Py_ssize_t sys_tracing_threads; /* Count of threads with c_tracefunc set */
269269
PyObject *monitoring_callables[PY_MONITORING_TOOL_IDS][_PY_MONITORING_EVENTS];

Lib/test/test_free_threading/test_monitoring.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,32 @@ def during_threads(self):
193193
self.set = not self.set
194194

195195

196+
@threading_helper.requires_working_threading()
197+
class SetProfileAllThreadsMultiThreaded(InstrumentationMultiThreadedMixin, TestCase):
198+
"""Uses threading.setprofile_all_threads and repeatedly toggles instrumentation on and off"""
199+
200+
def setUp(self):
201+
self.set = False
202+
self.called = False
203+
204+
def after_test(self):
205+
self.assertTrue(self.called)
206+
207+
def tearDown(self):
208+
threading.setprofile_all_threads(None)
209+
210+
def trace_func(self, frame, event, arg):
211+
self.called = True
212+
return self.trace_func
213+
214+
def during_threads(self):
215+
if self.set:
216+
threading.setprofile_all_threads(self.trace_func)
217+
else:
218+
threading.setprofile_all_threads(None)
219+
self.set = not self.set
220+
221+
196222
@threading_helper.requires_working_threading()
197223
class SetProfileAllMultiThreaded(TestCase):
198224
def test_profile_all_threads(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a crash in the :term:`free threading` build when disabling profiling or
2+
tracing across all threads with :c:func:`PyEval_SetProfileAllThreads` or
3+
:c:func:`PyEval_SetTraceAllThreads` or their Python equivalents
4+
:func:`threading.settrace_all_threads` and
5+
:func:`threading.setprofile_all_threads`.

Python/bytecodes.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,15 @@ dummy_func(
148148

149149
tier1 inst(RESUME, (--)) {
150150
assert(frame == tstate->current_frame);
151-
if (tstate->tracing == 0) {
151+
#ifdef Py_GIL_DISABLED
152+
// For thread-safety, we need to check instrumentation version
153+
// even when tracing. Otherwise, another thread may concurrently
154+
// re-write the bytecode while we are executing this function.
155+
int check_instrumentation = 1;
156+
#else
157+
int check_instrumentation = (tstate->tracing == 0);
158+
#endif
159+
if (check_instrumentation) {
152160
uintptr_t global_version =
153161
_Py_atomic_load_uintptr_relaxed(&tstate->eval_breaker) &
154162
~_PY_EVAL_EVENTS_MASK;

Python/ceval.c

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2334,21 +2334,10 @@ PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
23342334
void
23352335
PyEval_SetProfileAllThreads(Py_tracefunc func, PyObject *arg)
23362336
{
2337-
PyThreadState *this_tstate = _PyThreadState_GET();
2338-
PyInterpreterState* interp = this_tstate->interp;
2339-
2340-
_PyRuntimeState *runtime = &_PyRuntime;
2341-
HEAD_LOCK(runtime);
2342-
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
2343-
HEAD_UNLOCK(runtime);
2344-
2345-
while (ts) {
2346-
if (_PyEval_SetProfile(ts, func, arg) < 0) {
2347-
PyErr_FormatUnraisable("Exception ignored in PyEval_SetProfileAllThreads");
2348-
}
2349-
HEAD_LOCK(runtime);
2350-
ts = PyThreadState_Next(ts);
2351-
HEAD_UNLOCK(runtime);
2337+
PyInterpreterState *interp = _PyInterpreterState_GET();
2338+
if (_PyEval_SetProfileAllThreads(interp, func, arg) < 0) {
2339+
/* Log _PySys_Audit() error */
2340+
PyErr_FormatUnraisable("Exception ignored in PyEval_SetProfileAllThreads");
23522341
}
23532342
}
23542343

@@ -2365,21 +2354,10 @@ PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
23652354
void
23662355
PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *arg)
23672356
{
2368-
PyThreadState *this_tstate = _PyThreadState_GET();
2369-
PyInterpreterState* interp = this_tstate->interp;
2370-
2371-
_PyRuntimeState *runtime = &_PyRuntime;
2372-
HEAD_LOCK(runtime);
2373-
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
2374-
HEAD_UNLOCK(runtime);
2375-
2376-
while (ts) {
2377-
if (_PyEval_SetTrace(ts, func, arg) < 0) {
2378-
PyErr_FormatUnraisable("Exception ignored in PyEval_SetTraceAllThreads");
2379-
}
2380-
HEAD_LOCK(runtime);
2381-
ts = PyThreadState_Next(ts);
2382-
HEAD_UNLOCK(runtime);
2357+
PyInterpreterState *interp = _PyInterpreterState_GET();
2358+
if (_PyEval_SetTraceAllThreads(interp, func, arg) < 0) {
2359+
/* Log _PySys_Audit() error */
2360+
PyErr_FormatUnraisable("Exception ignored in PyEval_SetTraceAllThreads");
23832361
}
23842362
}
23852363

Python/instrumentation.c

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,8 @@ set_version_raw(uintptr_t *ptr, uint32_t version)
10121012
static void
10131013
set_global_version(PyThreadState *tstate, uint32_t version)
10141014
{
1015+
ASSERT_WORLD_STOPPED();
1016+
10151017
assert((version & _PY_EVAL_EVENTS_MASK) == 0);
10161018
PyInterpreterState *interp = tstate->interp;
10171019
set_version_raw(&interp->ceval.instrumentation_version, version);
@@ -1908,28 +1910,27 @@ _Py_Instrument(PyCodeObject *code, PyInterpreterState *interp)
19081910

19091911

19101912
static int
1911-
instrument_all_executing_code_objects(PyInterpreterState *interp) {
1913+
instrument_all_executing_code_objects(PyInterpreterState *interp)
1914+
{
19121915
ASSERT_WORLD_STOPPED();
19131916

1914-
_PyRuntimeState *runtime = &_PyRuntime;
1915-
HEAD_LOCK(runtime);
1916-
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
1917-
HEAD_UNLOCK(runtime);
1918-
while (ts) {
1917+
int err = 0;
1918+
HEAD_LOCK(&_PyRuntime);
1919+
for (PyThreadState *ts = interp->threads.head; ts != NULL; ts = ts->next) {
19191920
_PyInterpreterFrame *frame = ts->current_frame;
19201921
while (frame) {
19211922
if (frame->owner != FRAME_OWNED_BY_CSTACK) {
1922-
if (instrument_lock_held(_PyFrame_GetCode(frame), interp)) {
1923-
return -1;
1923+
err = instrument_lock_held(_PyFrame_GetCode(frame), interp);
1924+
if (err) {
1925+
goto done;
19241926
}
19251927
}
19261928
frame = frame->previous;
19271929
}
1928-
HEAD_LOCK(runtime);
1929-
ts = PyThreadState_Next(ts);
1930-
HEAD_UNLOCK(runtime);
19311930
}
1932-
return 0;
1931+
done:
1932+
HEAD_UNLOCK(&_PyRuntime);
1933+
return err;
19331934
}
19341935

19351936
static void
@@ -1975,6 +1976,7 @@ check_tool(PyInterpreterState *interp, int tool_id)
19751976
int
19761977
_PyMonitoring_SetEvents(int tool_id, _PyMonitoringEventSet events)
19771978
{
1979+
ASSERT_WORLD_STOPPED();
19781980
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
19791981
PyThreadState *tstate = _PyThreadState_GET();
19801982
PyInterpreterState *interp = tstate->interp;
@@ -1983,33 +1985,28 @@ _PyMonitoring_SetEvents(int tool_id, _PyMonitoringEventSet events)
19831985
return -1;
19841986
}
19851987

1986-
int res;
1987-
_PyEval_StopTheWorld(interp);
19881988
uint32_t existing_events = get_events(&interp->monitors, tool_id);
19891989
if (existing_events == events) {
1990-
res = 0;
1991-
goto done;
1990+
return 0;
19921991
}
19931992
set_events(&interp->monitors, tool_id, events);
19941993
uint32_t new_version = global_version(interp) + MONITORING_VERSION_INCREMENT;
19951994
if (new_version == 0) {
19961995
PyErr_Format(PyExc_OverflowError, "events set too many times");
1997-
res = -1;
1998-
goto done;
1996+
return -1;
19991997
}
20001998
set_global_version(tstate, new_version);
20011999
#ifdef _Py_TIER2
20022000
_Py_Executors_InvalidateAll(interp, 1);
20032001
#endif
2004-
res = instrument_all_executing_code_objects(interp);
2005-
done:
2006-
_PyEval_StartTheWorld(interp);
2007-
return res;
2002+
return instrument_all_executing_code_objects(interp);
20082003
}
20092004

20102005
int
20112006
_PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEventSet events)
20122007
{
2008+
ASSERT_WORLD_STOPPED();
2009+
20132010
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
20142011
PyInterpreterState *interp = _PyInterpreterState_GET();
20152012
assert(events < (1 << _PY_MONITORING_LOCAL_EVENTS));
@@ -2021,26 +2018,18 @@ _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
20212018
return -1;
20222019
}
20232020

2024-
int res;
2025-
_PyEval_StopTheWorld(interp);
20262021
if (allocate_instrumentation_data(code)) {
2027-
res = -1;
2028-
goto done;
2022+
return -1;
20292023
}
20302024

20312025
_Py_LocalMonitors *local = &code->_co_monitoring->local_monitors;
20322026
uint32_t existing_events = get_local_events(local, tool_id);
20332027
if (existing_events == events) {
2034-
res = 0;
2035-
goto done;
2028+
return 0;
20362029
}
20372030
set_local_events(local, tool_id, events);
20382031

2039-
res = force_instrument_lock_held(code, interp);
2040-
2041-
done:
2042-
_PyEval_StartTheWorld(interp);
2043-
return res;
2032+
return force_instrument_lock_held(code, interp);
20442033
}
20452034

20462035
int
@@ -2238,7 +2227,11 @@ monitoring_set_events_impl(PyObject *module, int tool_id, int event_set)
22382227
return NULL;
22392228
}
22402229
event_set &= ~C_RETURN_EVENTS;
2241-
if (_PyMonitoring_SetEvents(tool_id, event_set)) {
2230+
PyInterpreterState *interp = _PyInterpreterState_GET();
2231+
_PyEval_StopTheWorld(interp);
2232+
int err = _PyMonitoring_SetEvents(tool_id, event_set);
2233+
_PyEval_StartTheWorld(interp);
2234+
if (err) {
22422235
return NULL;
22432236
}
22442237
Py_RETURN_NONE;
@@ -2315,7 +2308,11 @@ monitoring_set_local_events_impl(PyObject *module, int tool_id,
23152308
return NULL;
23162309
}
23172310

2318-
if (_PyMonitoring_SetLocalEvents((PyCodeObject*)code, tool_id, event_set)) {
2311+
PyInterpreterState *interp = _PyInterpreterState_GET();
2312+
_PyEval_StopTheWorld(interp);
2313+
int err = _PyMonitoring_SetLocalEvents((PyCodeObject*)code, tool_id, event_set);
2314+
_PyEval_StartTheWorld(interp);
2315+
if (err) {
23192316
return NULL;
23202317
}
23212318
Py_RETURN_NONE;

0 commit comments

Comments
 (0)