Skip to content

Commit cd96310

Browse files
committed
pythongh-104690: thread_run() checks for tstate dangling pointer (python#109056)
thread_run() of _threadmodule.c now calls _PyThreadState_CheckConsistency() to check if tstate is a dangling pointer when Python is built in debug mode. Rename ceval_gil.c is_tstate_valid() to _PyThreadState_CheckConsistency() to reuse it in _threadmodule.c. (cherry picked from commit f63d378)
1 parent 9dd28d2 commit cd96310

File tree

5 files changed

+37
-26
lines changed

5 files changed

+37
-26
lines changed

Include/internal/pycore_pystate.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ _Py_ThreadCanHandlePendingCalls(void)
6161
}
6262

6363

64+
#ifndef NDEBUG
65+
extern int _PyThreadState_CheckConsistency(PyThreadState *tstate);
66+
#endif
67+
6468
/* Variable and macro for in-line access to current thread
6569
and interpreter state */
6670

Modules/_threadmodule.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,9 +1076,12 @@ static void
10761076
thread_run(void *boot_raw)
10771077
{
10781078
struct bootstate *boot = (struct bootstate *) boot_raw;
1079-
PyThreadState *tstate;
1079+
PyThreadState *tstate = boot->tstate;
1080+
1081+
// gh-104690: If Python is being finalized and PyInterpreterState_Delete()
1082+
// was called, tstate becomes a dangling pointer.
1083+
assert(_PyThreadState_CheckConsistency(tstate));
10801084

1081-
tstate = boot->tstate;
10821085
tstate->thread_id = PyThread_get_thread_ident();
10831086
#ifdef PY_HAVE_THREAD_NATIVE_ID
10841087
tstate->native_thread_id = PyThread_get_thread_native_id();

Python/ceval.c

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -216,20 +216,6 @@ _PyEvalFrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame);
216216
"cannot access free variable '%s' where it is not associated with a" \
217217
" value in enclosing scope"
218218

219-
#ifndef NDEBUG
220-
/* Ensure that tstate is valid: sanity check for PyEval_AcquireThread() and
221-
PyEval_RestoreThread(). Detect if tstate memory was freed. It can happen
222-
when a thread continues to run after Python finalization, especially
223-
daemon threads. */
224-
static int
225-
is_tstate_valid(PyThreadState *tstate)
226-
{
227-
assert(!_PyMem_IsPtrFreed(tstate));
228-
assert(!_PyMem_IsPtrFreed(tstate->interp));
229-
return 1;
230-
}
231-
#endif
232-
233219

234220
/* This can set eval_breaker to 0 even though gil_drop_request became
235221
1. We believe this is all right because the eval loop will release
@@ -464,7 +450,7 @@ PyEval_AcquireThread(PyThreadState *tstate)
464450
void
465451
PyEval_ReleaseThread(PyThreadState *tstate)
466452
{
467-
assert(is_tstate_valid(tstate));
453+
assert(_PyThreadState_CheckConsistency(tstate));
468454

469455
_PyRuntimeState *runtime = tstate->interp->runtime;
470456
PyThreadState *new_tstate = _PyThreadState_Swap(&runtime->gilstate, NULL);
@@ -671,7 +657,7 @@ Py_AddPendingCall(int (*func)(void *), void *arg)
671657
static int
672658
handle_signals(PyThreadState *tstate)
673659
{
674-
assert(is_tstate_valid(tstate));
660+
assert(_PyThreadState_CheckConsistency(tstate));
675661
if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
676662
return 0;
677663
}
@@ -739,7 +725,7 @@ void
739725
_Py_FinishPendingCalls(PyThreadState *tstate)
740726
{
741727
assert(PyGILState_Check());
742-
assert(is_tstate_valid(tstate));
728+
assert(_PyThreadState_CheckConsistency(tstate));
743729

744730
struct _pending_calls *pending = &tstate->interp->ceval.pending;
745731

@@ -764,7 +750,7 @@ Py_MakePendingCalls(void)
764750
assert(PyGILState_Check());
765751

766752
PyThreadState *tstate = _PyThreadState_GET();
767-
assert(is_tstate_valid(tstate));
753+
assert(_PyThreadState_CheckConsistency(tstate));
768754

769755
/* Python signal handler doesn't really queue a callback: it only signals
770756
that a signal was received, see _PyEval_SignalReceived(). */
@@ -6947,7 +6933,7 @@ maybe_call_line_trace(Py_tracefunc func, PyObject *obj,
69476933
int
69486934
_PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
69496935
{
6950-
assert(is_tstate_valid(tstate));
6936+
assert(_PyThreadState_CheckConsistency(tstate));
69516937
/* The caller must hold the GIL */
69526938
assert(PyGILState_Check());
69536939

@@ -6999,7 +6985,7 @@ PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
69996985
int
70006986
_PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
70016987
{
7002-
assert(is_tstate_valid(tstate));
6988+
assert(_PyThreadState_CheckConsistency(tstate));
70036989
/* The caller must hold the GIL */
70046990
assert(PyGILState_Check());
70056991

Python/ceval_gil.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
171171
/* Not switched yet => wait */
172172
if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
173173
{
174-
assert(is_tstate_valid(tstate));
174+
assert(_PyThreadState_CheckConsistency(tstate));
175175
RESET_GIL_DROP_REQUEST(tstate->interp);
176176
/* NOTE: if COND_WAIT does not atomically start waiting when
177177
releasing the mutex, another thread can run through, take
@@ -226,7 +226,7 @@ take_gil(PyThreadState *tstate)
226226
PyThread_exit_thread();
227227
}
228228

229-
assert(is_tstate_valid(tstate));
229+
assert(_PyThreadState_CheckConsistency(tstate));
230230
PyInterpreterState *interp = tstate->interp;
231231
struct _ceval_runtime_state *ceval = &interp->runtime->ceval;
232232
struct _ceval_state *ceval2 = &interp->ceval;
@@ -268,7 +268,7 @@ take_gil(PyThreadState *tstate)
268268
}
269269
PyThread_exit_thread();
270270
}
271-
assert(is_tstate_valid(tstate));
271+
assert(_PyThreadState_CheckConsistency(tstate));
272272

273273
SET_GIL_DROP_REQUEST(interp);
274274
drop_requested = 1;
@@ -307,7 +307,7 @@ take_gil(PyThreadState *tstate)
307307
drop_gil(ceval, ceval2, tstate);
308308
PyThread_exit_thread();
309309
}
310-
assert(is_tstate_valid(tstate));
310+
assert(_PyThreadState_CheckConsistency(tstate));
311311

312312
if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) {
313313
RESET_GIL_DROP_REQUEST(interp);

Python/pystate.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,6 +2237,24 @@ _PyThreadState_PopFrame(PyThreadState *tstate, _PyInterpreterFrame * frame)
22372237
}
22382238

22392239

2240+
#ifndef NDEBUG
2241+
// Check that a Python thread state valid. In practice, this function is used
2242+
// on a Python debug build to check if 'tstate' is a dangling pointer, if the
2243+
// PyThreadState memory has been freed.
2244+
//
2245+
// Usage:
2246+
//
2247+
// assert(_PyThreadState_CheckConsistency(tstate));
2248+
int
2249+
_PyThreadState_CheckConsistency(PyThreadState *tstate)
2250+
{
2251+
assert(!_PyMem_IsPtrFreed(tstate));
2252+
assert(!_PyMem_IsPtrFreed(tstate->interp));
2253+
return 1;
2254+
}
2255+
#endif
2256+
2257+
22402258
#ifdef __cplusplus
22412259
}
22422260
#endif

0 commit comments

Comments
 (0)