Skip to content

[3.11] gh-108987: Fix _thread.start_new_thread() race condition (#109135) #109272

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

Merged
merged 1 commit into from
Sep 11, 2023
Merged
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
2 changes: 2 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ _Py_ThreadCanHandlePendingCalls(void)
extern int _PyThreadState_CheckConsistency(PyThreadState *tstate);
#endif

int _PyThreadState_MustExit(PyThreadState *tstate);

/* Variable and macro for in-line access to current thread
and interpreter state */

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :func:`_thread.start_new_thread` race condition. If a thread is created
during Python finalization, the newly spawned thread now exits immediately
instead of trying to access freed memory and lead to a crash. Patch by
Victor Stinner.
47 changes: 31 additions & 16 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1053,21 +1053,21 @@ _localdummy_destroyed(PyObject *localweakref, PyObject *dummyweakref)
/* Module functions */

struct bootstate {
PyInterpreterState *interp;
PyThreadState *tstate;
PyObject *func;
PyObject *args;
PyObject *kwargs;
PyThreadState *tstate;
_PyRuntimeState *runtime;
};


static void
thread_bootstate_free(struct bootstate *boot)
thread_bootstate_free(struct bootstate *boot, int decref)
{
Py_DECREF(boot->func);
Py_DECREF(boot->args);
Py_XDECREF(boot->kwargs);
if (decref) {
Py_DECREF(boot->func);
Py_DECREF(boot->args);
Py_XDECREF(boot->kwargs);
}
PyMem_Free(boot);
}

Expand All @@ -1078,9 +1078,24 @@ thread_run(void *boot_raw)
struct bootstate *boot = (struct bootstate *) boot_raw;
PyThreadState *tstate = boot->tstate;

// gh-104690: If Python is being finalized and PyInterpreterState_Delete()
// was called, tstate becomes a dangling pointer.
assert(_PyThreadState_CheckConsistency(tstate));
// gh-108987: If _thread.start_new_thread() is called before or while
// Python is being finalized, thread_run() can called *after*.
// _PyRuntimeState_SetFinalizing() is called. At this point, all Python
// threads must exit, except of the thread calling Py_Finalize() whch holds
// the GIL and must not exit.
//
// At this stage, tstate can be a dangling pointer (point to freed memory),
// it's ok to call _PyThreadState_MustExit() with a dangling pointer.
if (_PyThreadState_MustExit(tstate)) {
// Don't call PyThreadState_Clear() nor _PyThreadState_DeleteCurrent().
// These functions are called on tstate indirectly by Py_Finalize()
// which calls _PyInterpreterState_Clear().
//
// Py_DECREF() cannot be called because the GIL is not held: leak
// references on purpose. Python is being finalized anyway.
thread_bootstate_free(boot, 0);
goto exit;
}

tstate->thread_id = PyThread_get_thread_ident();
#ifdef PY_HAVE_THREAD_NATIVE_ID
Expand All @@ -1105,20 +1120,22 @@ thread_run(void *boot_raw)
Py_DECREF(res);
}

thread_bootstate_free(boot);
thread_bootstate_free(boot, 1);

tstate->interp->threads.count--;
PyThreadState_Clear(tstate);
_PyThreadState_DeleteCurrent(tstate);

exit:
// bpo-44434: Don't call explicitly PyThread_exit_thread(). On Linux with
// the glibc, pthread_exit() can abort the whole process if dlopen() fails
// to open the libgcc_s.so library (ex: EMFILE error).
return;
}

static PyObject *
thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
{
_PyRuntimeState *runtime = &_PyRuntime;
PyObject *func, *args, *kwargs = NULL;

if (!PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3,
Expand Down Expand Up @@ -1151,13 +1168,11 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
if (boot == NULL) {
return PyErr_NoMemory();
}
boot->interp = _PyInterpreterState_GET();
boot->tstate = _PyThreadState_Prealloc(boot->interp);
boot->tstate = _PyThreadState_Prealloc(interp);
if (boot->tstate == NULL) {
PyMem_Free(boot);
return PyErr_NoMemory();
}
boot->runtime = runtime;
boot->func = Py_NewRef(func);
boot->args = Py_NewRef(args);
boot->kwargs = Py_XNewRef(kwargs);
Expand All @@ -1166,7 +1181,7 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
if (ident == PYTHREAD_INVALID_THREAD_ID) {
PyErr_SetString(ThreadError, "can't start new thread");
PyThreadState_Clear(boot->tstate);
thread_bootstate_free(boot);
thread_bootstate_free(boot, 1);
return NULL;
}
return PyLong_FromUnsignedLong(ident);
Expand Down
25 changes: 3 additions & 22 deletions Python/ceval_gil.h
Original file line number Diff line number Diff line change
Expand Up @@ -185,25 +185,6 @@ drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
}


/* Check if a Python thread must exit immediately, rather than taking the GIL
if Py_Finalize() has been called.

When this function is called by a daemon thread after Py_Finalize() has been
called, the GIL does no longer exist.

tstate must be non-NULL. */
static inline int
tstate_must_exit(PyThreadState *tstate)
{
/* bpo-39877: Access _PyRuntime directly rather than using
tstate->interp->runtime to support calls from Python daemon threads.
After Py_Finalize() has been called, tstate can be a dangling pointer:
point to PyThreadState freed memory. */
PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime);
return (finalizing != NULL && finalizing != tstate);
}


/* Take the GIL.

The function saves errno at entry and restores its value at exit.
Expand All @@ -216,7 +197,7 @@ take_gil(PyThreadState *tstate)

assert(tstate != NULL);

if (tstate_must_exit(tstate)) {
if (_PyThreadState_MustExit(tstate)) {
/* bpo-39877: If Py_Finalize() has been called and tstate is not the
thread which called Py_Finalize(), exit immediately the thread.

Expand Down Expand Up @@ -255,7 +236,7 @@ take_gil(PyThreadState *tstate)
_Py_atomic_load_relaxed(&gil->locked) &&
gil->switch_number == saved_switchnum)
{
if (tstate_must_exit(tstate)) {
if (_PyThreadState_MustExit(tstate)) {
MUTEX_UNLOCK(gil->mutex);
// gh-96387: If the loop requested a drop request in a previous
// iteration, reset the request. Otherwise, drop_gil() can
Expand Down Expand Up @@ -295,7 +276,7 @@ take_gil(PyThreadState *tstate)
MUTEX_UNLOCK(gil->switch_mutex);
#endif

if (tstate_must_exit(tstate)) {
if (_PyThreadState_MustExit(tstate)) {
/* bpo-36475: If Py_Finalize() has been called and tstate is not
the thread which called Py_Finalize(), exit immediately the
thread.
Expand Down
26 changes: 26 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,10 @@ _PyThreadState_Init(PyThreadState *tstate)
void
_PyThreadState_SetCurrent(PyThreadState *tstate)
{
// gh-104690: If Python is being finalized and PyInterpreterState_Delete()
// was called, tstate becomes a dangling pointer.
assert(_PyThreadState_CheckConsistency(tstate));

_PyGILState_NoteThreadState(&tstate->interp->runtime->gilstate, tstate);
}

Expand Down Expand Up @@ -2255,6 +2259,28 @@ _PyThreadState_CheckConsistency(PyThreadState *tstate)
#endif


// Check if a Python thread must exit immediately, rather than taking the GIL
// if Py_Finalize() has been called.
//
// When this function is called by a daemon thread after Py_Finalize() has been
// called, the GIL does no longer exist.
//
// tstate can be a dangling pointer (point to freed memory): only tstate value
// is used, the pointer is not deferenced.
//
// tstate must be non-NULL.
int
_PyThreadState_MustExit(PyThreadState *tstate)
{
/* bpo-39877: Access _PyRuntime directly rather than using
tstate->interp->runtime to support calls from Python daemon threads.
After Py_Finalize() has been called, tstate can be a dangling pointer:
point to PyThreadState freed memory. */
PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime);
return (finalizing != NULL && finalizing != tstate);
}


#ifdef __cplusplus
}
#endif