Skip to content

Commit bcb2772

Browse files
committed
gh-137400: Fix a thread-safety issues when profiling all threads
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.
1 parent 470cbe9 commit bcb2772

File tree

8 files changed

+366
-231
lines changed

8 files changed

+366
-231
lines changed

Include/internal/pycore_ceval.h

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

2323
// Export for '_lsprof' shared extension
2424
PyAPI_FUNC(int) _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
25+
extern int _PyEval_SetProfileAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyObject *arg);
2526

2627
extern int _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
28+
extern int _PyEval_SetTraceAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyObject *arg);
2729

2830
extern int _PyEval_SetOpcodeTrace(PyFrameObject *f, bool enable);
2931

Include/internal/pycore_interp_structs.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ struct _ceval_runtime_state {
9898
// For example, we use a preallocated array
9999
// for the list of pending calls.
100100
struct _pending_calls pending_mainthread;
101-
PyMutex sys_trace_profile_mutex;
102101
};
103102

104103

@@ -950,8 +949,8 @@ struct _is {
950949
PyDict_WatchCallback builtins_dict_watcher;
951950

952951
_Py_GlobalMonitors monitors;
953-
bool sys_profile_initialized;
954-
bool sys_trace_initialized;
952+
_PyOnceFlag sys_profile_once_flag;
953+
_PyOnceFlag sys_trace_once_flag;
955954
Py_ssize_t sys_profiling_threads; /* Count of threads with c_profilefunc set */
956955
Py_ssize_t sys_tracing_threads; /* Count of threads with c_tracefunc set */
957956
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
@@ -194,6 +194,32 @@ def during_threads(self):
194194
self.set = not self.set
195195

196196

197+
@threading_helper.requires_working_threading()
198+
class SetProfileAllThreadsMultiThreaded(InstrumentationMultiThreadedMixin, TestCase):
199+
"""Uses threading.setprofile_all_threads and repeatedly toggles instrumentation on and off"""
200+
201+
def setUp(self):
202+
self.set = False
203+
self.called = False
204+
205+
def after_test(self):
206+
self.assertTrue(self.called)
207+
208+
def tearDown(self):
209+
threading.setprofile_all_threads(None)
210+
211+
def trace_func(self, frame, event, arg):
212+
self.called = True
213+
return self.trace_func
214+
215+
def during_threads(self):
216+
if self.set:
217+
threading.setprofile_all_threads(self.trace_func)
218+
else:
219+
threading.setprofile_all_threads(None)
220+
self.set = not self.set
221+
222+
197223
class TraceBuf:
198224
def __init__(self):
199225
self.traces = []

Python/ceval.c

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,18 +2512,9 @@ PyEval_SetProfileAllThreads(Py_tracefunc func, PyObject *arg)
25122512
PyThreadState *this_tstate = _PyThreadState_GET();
25132513
PyInterpreterState* interp = this_tstate->interp;
25142514

2515-
_PyRuntimeState *runtime = &_PyRuntime;
2516-
HEAD_LOCK(runtime);
2517-
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
2518-
HEAD_UNLOCK(runtime);
2519-
2520-
while (ts) {
2521-
if (_PyEval_SetProfile(ts, func, arg) < 0) {
2522-
PyErr_FormatUnraisable("Exception ignored in PyEval_SetProfileAllThreads");
2523-
}
2524-
HEAD_LOCK(runtime);
2525-
ts = PyThreadState_Next(ts);
2526-
HEAD_UNLOCK(runtime);
2515+
if (_PyEval_SetProfileAllThreads(interp, func, arg) < 0) {
2516+
/* Log _PySys_Audit() error */
2517+
PyErr_FormatUnraisable("Exception ignored in PyEval_SetProfileAllThreads");
25272518
}
25282519
}
25292520

@@ -2543,18 +2534,9 @@ PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *arg)
25432534
PyThreadState *this_tstate = _PyThreadState_GET();
25442535
PyInterpreterState* interp = this_tstate->interp;
25452536

2546-
_PyRuntimeState *runtime = &_PyRuntime;
2547-
HEAD_LOCK(runtime);
2548-
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
2549-
HEAD_UNLOCK(runtime);
2550-
2551-
while (ts) {
2552-
if (_PyEval_SetTrace(ts, func, arg) < 0) {
2553-
PyErr_FormatUnraisable("Exception ignored in PyEval_SetTraceAllThreads");
2554-
}
2555-
HEAD_LOCK(runtime);
2556-
ts = PyThreadState_Next(ts);
2557-
HEAD_UNLOCK(runtime);
2537+
if (_PyEval_SetTraceAllThreads(interp, func, arg) < 0) {
2538+
/* Log _PySys_Audit() error */
2539+
PyErr_FormatUnraisable("Exception ignored in PyEval_SetTraceAllThreads");
25582540
}
25592541
}
25602542

Python/instrumentation.c

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,8 @@ set_version_raw(uintptr_t *ptr, uint32_t version)
10401040
static void
10411041
set_global_version(PyThreadState *tstate, uint32_t version)
10421042
{
1043+
ASSERT_WORLD_STOPPED();
1044+
10431045
assert((version & _PY_EVAL_EVENTS_MASK) == 0);
10441046
PyInterpreterState *interp = tstate->interp;
10451047
set_version_raw(&interp->ceval.instrumentation_version, version);
@@ -1939,28 +1941,30 @@ _Py_Instrument(PyCodeObject *code, PyInterpreterState *interp)
19391941

19401942

19411943
static int
1942-
instrument_all_executing_code_objects(PyInterpreterState *interp) {
1944+
instrument_all_executing_code_objects(PyInterpreterState *interp)
1945+
{
19431946
ASSERT_WORLD_STOPPED();
19441947

1948+
int err = 0;
19451949
_PyRuntimeState *runtime = &_PyRuntime;
19461950
HEAD_LOCK(runtime);
1947-
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
1948-
HEAD_UNLOCK(runtime);
1951+
PyThreadState *ts = PyInterpreterState_ThreadHead(interp);
19491952
while (ts) {
19501953
_PyInterpreterFrame *frame = ts->current_frame;
19511954
while (frame) {
19521955
if (frame->owner < FRAME_OWNED_BY_INTERPRETER) {
1953-
if (instrument_lock_held(_PyFrame_GetCode(frame), interp)) {
1954-
return -1;
1956+
err = instrument_lock_held(_PyFrame_GetCode(frame), interp);
1957+
if (err) {
1958+
goto done;
19551959
}
19561960
}
19571961
frame = frame->previous;
19581962
}
1959-
HEAD_LOCK(runtime);
19601963
ts = PyThreadState_Next(ts);
1961-
HEAD_UNLOCK(runtime);
19621964
}
1963-
return 0;
1965+
done:
1966+
HEAD_UNLOCK(runtime);
1967+
return err;
19641968
}
19651969

19661970
static void
@@ -2006,6 +2010,7 @@ check_tool(PyInterpreterState *interp, int tool_id)
20062010
int
20072011
_PyMonitoring_SetEvents(int tool_id, _PyMonitoringEventSet events)
20082012
{
2013+
ASSERT_WORLD_STOPPED();
20092014
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
20102015
PyThreadState *tstate = _PyThreadState_GET();
20112016
PyInterpreterState *interp = tstate->interp;
@@ -2014,33 +2019,28 @@ _PyMonitoring_SetEvents(int tool_id, _PyMonitoringEventSet events)
20142019
return -1;
20152020
}
20162021

2017-
int res;
2018-
_PyEval_StopTheWorld(interp);
20192022
uint32_t existing_events = get_events(&interp->monitors, tool_id);
20202023
if (existing_events == events) {
2021-
res = 0;
2022-
goto done;
2024+
return 0;
20232025
}
20242026
set_events(&interp->monitors, tool_id, events);
20252027
uint32_t new_version = global_version(interp) + MONITORING_VERSION_INCREMENT;
20262028
if (new_version == 0) {
20272029
PyErr_Format(PyExc_OverflowError, "events set too many times");
2028-
res = -1;
2029-
goto done;
2030+
return -1;
20302031
}
20312032
set_global_version(tstate, new_version);
20322033
#ifdef _Py_TIER2
20332034
_Py_Executors_InvalidateAll(interp, 1);
20342035
#endif
2035-
res = instrument_all_executing_code_objects(interp);
2036-
done:
2037-
_PyEval_StartTheWorld(interp);
2038-
return res;
2036+
return instrument_all_executing_code_objects(interp);
20392037
}
20402038

20412039
int
20422040
_PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEventSet events)
20432041
{
2042+
ASSERT_WORLD_STOPPED();
2043+
20442044
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
20452045
PyInterpreterState *interp = _PyInterpreterState_GET();
20462046
assert(events < (1 << _PY_MONITORING_LOCAL_EVENTS));
@@ -2052,28 +2052,20 @@ _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
20522052
return -1;
20532053
}
20542054

2055-
int res;
2056-
_PyEval_StopTheWorld(interp);
20572055
if (allocate_instrumentation_data(code)) {
2058-
res = -1;
2059-
goto done;
2056+
return -1;
20602057
}
20612058

20622059
code->_co_monitoring->tool_versions[tool_id] = interp->monitoring_tool_versions[tool_id];
20632060

20642061
_Py_LocalMonitors *local = &code->_co_monitoring->local_monitors;
20652062
uint32_t existing_events = get_local_events(local, tool_id);
20662063
if (existing_events == events) {
2067-
res = 0;
2068-
goto done;
2064+
return 0;
20692065
}
20702066
set_local_events(local, tool_id, events);
20712067

2072-
res = force_instrument_lock_held(code, interp);
2073-
2074-
done:
2075-
_PyEval_StartTheWorld(interp);
2076-
return res;
2068+
return force_instrument_lock_held(code, interp);
20772069
}
20782070

20792071
int
@@ -2105,11 +2097,12 @@ int _PyMonitoring_ClearToolId(int tool_id)
21052097
}
21062098
}
21072099

2100+
_PyEval_StopTheWorld(interp);
21082101
if (_PyMonitoring_SetEvents(tool_id, 0) < 0) {
2102+
_PyEval_StartTheWorld(interp);
21092103
return -1;
21102104
}
21112105

2112-
_PyEval_StopTheWorld(interp);
21132106
uint32_t version = global_version(interp) + MONITORING_VERSION_INCREMENT;
21142107
if (version == 0) {
21152108
PyErr_Format(PyExc_OverflowError, "events set too many times");
@@ -2346,7 +2339,11 @@ monitoring_set_events_impl(PyObject *module, int tool_id, int event_set)
23462339
event_set &= ~(1 << PY_MONITORING_EVENT_BRANCH);
23472340
event_set |= (1 << PY_MONITORING_EVENT_BRANCH_RIGHT) | (1 << PY_MONITORING_EVENT_BRANCH_LEFT);
23482341
}
2349-
if (_PyMonitoring_SetEvents(tool_id, event_set)) {
2342+
PyInterpreterState *interp = _PyInterpreterState_GET();
2343+
_PyEval_StopTheWorld(interp);
2344+
int err = _PyMonitoring_SetEvents(tool_id, event_set);
2345+
_PyEval_StartTheWorld(interp);
2346+
if (err) {
23502347
return NULL;
23512348
}
23522349
Py_RETURN_NONE;
@@ -2427,7 +2424,11 @@ monitoring_set_local_events_impl(PyObject *module, int tool_id,
24272424
return NULL;
24282425
}
24292426

2430-
if (_PyMonitoring_SetLocalEvents((PyCodeObject*)code, tool_id, event_set)) {
2427+
PyInterpreterState *interp = _PyInterpreterState_GET();
2428+
_PyEval_StopTheWorld(interp);
2429+
int err = _PyMonitoring_SetLocalEvents((PyCodeObject*)code, tool_id, event_set);
2430+
_PyEval_StartTheWorld(interp);
2431+
if (err) {
24312432
return NULL;
24322433
}
24332434
Py_RETURN_NONE;

0 commit comments

Comments
 (0)