Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

gh-132296: Use thread-state reference counting to fix PyEval_SetTraceAllThreads #132298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions 3 Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ _Py_AssertHoldsTstateFunc(const char *func)
#define _Py_AssertHoldsTstate()
#endif

void _PyThreadState_Decref(PyThreadState *tstate);
void _PyThreadState_Incref(PyThreadState *tstate);

#ifdef __cplusplus
}
#endif
Expand Down
20 changes: 20 additions & 0 deletions 20 Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,26 @@ def checker():
self.assertEqual(threading.gettrace(), old_trace)
self.assertEqual(sys.gettrace(), old_trace)

@unittest.skipUnless(support.Py_GIL_DISABLED, "only meaningful under free-threading")
def test_settrace_all_threads_race(self):
# GH-132296: settrace_all_threads() could be racy on the free-threaded build
#if the threads were concurrently deleted.
old_trace = threading.gettrace()
def dummy(*args):
pass

def do_settrace():
threading.settrace_all_threads(dummy)

try:
with threading_helper.catch_threading_exception() as cm:
with threading_helper.start_threads((threading.Thread(target=do_settrace) for _ in range(8))):
pass

self.assertIsNone(cm.exc_value)
finally:
threading.settrace_all_threads(old_trace)

def test_getprofile(self):
def fn(*args): pass
old_profile = threading.getprofile()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a crash when using :c:func:`PyEval_SetTrace` on the :term:`free threaded
<free threading>` build.
13 changes: 12 additions & 1 deletion 13 Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -2467,15 +2467,26 @@ PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *arg)
_PyRuntimeState *runtime = &_PyRuntime;
HEAD_LOCK(runtime);
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
/* gh-132296: We need to prevent the thread state from being concurrently
deallocated. We can't stop-the-world because _PyEval_SetTrace()
is re-entrant. */
_PyThreadState_Incref(ts);
HEAD_UNLOCK(runtime);

while (ts) {
while (ts != NULL) {
if (_PyEval_SetTrace(ts, func, arg) < 0) {
PyErr_FormatUnraisable("Exception ignored in PyEval_SetTraceAllThreads");
}
HEAD_LOCK(runtime);
PyThreadState *old = ts;
ts = PyThreadState_Next(ts);
/* Drop the reference to the prior thread state
and acquire a reference to the next one. */
if (ts != NULL) {
_PyThreadState_Incref(ts);
}
HEAD_UNLOCK(runtime);
_PyThreadState_Decref(old);
}
}

Expand Down
30 changes: 26 additions & 4 deletions 30 Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,22 @@ decref_threadstate(_PyThreadStateImpl *tstate)
}
}

void
_PyThreadState_Decref(PyThreadState *tstate)
{
assert(tstate != NULL);
_PyThreadStateImpl *impl = (_PyThreadStateImpl *)tstate;
decref_threadstate(impl);
}

void
_PyThreadState_Incref(PyThreadState *tstate)
{
assert(tstate != NULL);
_PyThreadStateImpl *impl = (_PyThreadStateImpl *)tstate;
_Py_atomic_add_ssize(&impl->refcount, 1);
}

/* Get the thread state to a minimal consistent state.
Further init happens in pylifecycle.c before it can be used.
All fields not initialized here are expected to be zeroed out,
Expand Down Expand Up @@ -1880,9 +1896,15 @@ void
PyThreadState_Delete(PyThreadState *tstate)
{
_Py_EnsureTstateNotNULL(tstate);
tstate_verify_not_active(tstate);
tstate_delete_common(tstate, 0);
free_threadstate((_PyThreadStateImpl *)tstate);
_PyThreadStateImpl *impl = (_PyThreadStateImpl *)tstate;
if (_Py_atomic_add_ssize(&impl->refcount, -1) == 2) {
/* Treat the interpreter's reference (via the linked list) as weak,
even though it's technically strong. This is because we want
to free the thread state now, rather than wait for finalization. */
tstate_verify_not_active(tstate);
tstate_delete_common(tstate, 0);
free_threadstate((_PyThreadStateImpl *)tstate);
}
}


Expand All @@ -1895,7 +1917,7 @@ _PyThreadState_DeleteCurrent(PyThreadState *tstate)
#endif
current_fast_clear(tstate->interp->runtime);
tstate_delete_common(tstate, 1); // release GIL as part of call
free_threadstate((_PyThreadStateImpl *)tstate);
decref_threadstate((_PyThreadStateImpl *)tstate);
}

void
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.