Skip to content
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

gh-108987: Fix _thread.start_new_thread() race condition #109135

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 @@ -71,6 +71,8 @@ extern _Py_thread_local PyThreadState *_Py_tss_tstate;
extern int _PyThreadState_CheckConsistency(PyThreadState *tstate);
#endif

int _PyThreadState_MustExit(PyThreadState *tstate);

// Export for most shared extensions, used via _PyThreadState_GET() static
// inline function.
PyAPI_FUNC(PyThreadState *) _PyThreadState_GetCurrent(void);
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 @@ -1051,21 +1051,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 @@ -1076,9 +1076,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;
}

_PyThreadState_Bind(tstate);
PyEval_AcquireThread(tstate);
Expand All @@ -1097,14 +1112,17 @@ 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 *
Expand All @@ -1128,7 +1146,6 @@ and False otherwise.\n");
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 @@ -1171,16 +1188,14 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
if (boot == NULL) {
return PyErr_NoMemory();
}
boot->interp = _PyInterpreterState_GET();
boot->tstate = _PyThreadState_New(boot->interp);
boot->tstate = _PyThreadState_New(interp);
if (boot->tstate == NULL) {
PyMem_Free(boot);
if (!PyErr_Occurred()) {
return PyErr_NoMemory();
}
return NULL;
}
boot->runtime = runtime;
boot->func = Py_NewRef(func);
boot->args = Py_NewRef(args);
boot->kwargs = Py_XNewRef(kwargs);
Expand All @@ -1189,7 +1204,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
28 changes: 3 additions & 25 deletions Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -329,28 +329,6 @@ drop_gil(struct _ceval_state *ceval, PyThreadState *tstate)
}


/* 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);
if (finalizing == NULL) {
finalizing = _PyInterpreterState_GetFinalizing(tstate->interp);
}
return (finalizing != NULL && finalizing != tstate);
}


/* Take the GIL.

The function saves errno at entry and restores its value at exit.
Expand All @@ -366,7 +344,7 @@ take_gil(PyThreadState *tstate)
// XXX It may be more correct to check tstate->_status.finalizing.
// XXX assert(!tstate->_status.cleared);

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 @@ -404,7 +382,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 @@ -444,7 +422,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
29 changes: 29 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,10 @@ PyThreadState_Swap(PyThreadState *newts)
void
_PyThreadState_Bind(PyThreadState *tstate)
{
// gh-104690: If Python is being finalized and PyInterpreterState_Delete()
// was called, tstate becomes a dangling pointer.
assert(_PyThreadState_CheckConsistency(tstate));

bind_tstate(tstate);
// This makes sure there's a gilstate tstate bound
// as soon as possible.
Expand Down Expand Up @@ -2908,6 +2912,31 @@ _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);
if (finalizing == NULL) {
finalizing = _PyInterpreterState_GetFinalizing(tstate->interp);
}
return (finalizing != NULL && finalizing != tstate);
}


#ifdef __cplusplus
}
#endif