-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
gh-128639: Don't assume one thread in subinterpreter finalization with fixed daemon thread support #134606
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
base: main
Are you sure you want to change the base?
gh-128639: Don't assume one thread in subinterpreter finalization with fixed daemon thread support #134606
Changes from all commits
b14aa73
91d94d9
a569355
0146aec
f85a291
d287d54
fe7a342
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Fix a crash when using threads inside of a subinterpreter. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1992,6 +1992,7 @@ resolve_final_tstate(_PyRuntimeState *runtime) | |
} | ||
else { | ||
/* Fall back to the current tstate. It's better than nothing. */ | ||
// XXX No it's not | ||
main_tstate = tstate; | ||
} | ||
} | ||
|
@@ -2037,6 +2038,16 @@ _Py_Finalize(_PyRuntimeState *runtime) | |
|
||
_PyAtExit_Call(tstate->interp); | ||
|
||
/* Clean up any lingering subinterpreters. | ||
|
||
Two preconditions need to be met here: | ||
|
||
- This has to happen before _PyRuntimeState_SetFinalizing is | ||
called, or else threads might get prematurely blocked. | ||
- The world must not be stopped, as finalizers can run. | ||
*/ | ||
finalize_subinterpreters(); | ||
|
||
assert(_PyThreadState_GET() == tstate); | ||
|
||
/* Copy the core config, PyInterpreterState_Delete() free | ||
|
@@ -2124,9 +2135,6 @@ _Py_Finalize(_PyRuntimeState *runtime) | |
_PyImport_FiniExternal(tstate->interp); | ||
finalize_modules(tstate); | ||
|
||
/* Clean up any lingering subinterpreters. */ | ||
finalize_subinterpreters(); | ||
|
||
/* Print debug stats if any */ | ||
_PyEval_Fini(); | ||
|
||
|
@@ -2410,9 +2418,8 @@ Py_NewInterpreter(void) | |
return tstate; | ||
} | ||
|
||
/* Delete an interpreter and its last thread. This requires that the | ||
given thread state is current, that the thread has no remaining | ||
frames, and that it is its interpreter's only remaining thread. | ||
/* Delete an interpreter. This requires that the given thread state | ||
is current, and that the thread has no remaining frames. | ||
It is a fatal error to violate these constraints. | ||
|
||
(Py_FinalizeEx() doesn't have these constraints -- it zaps | ||
|
@@ -2442,15 +2449,20 @@ Py_EndInterpreter(PyThreadState *tstate) | |
_Py_FinishPendingCalls(tstate); | ||
|
||
_PyAtExit_Call(tstate->interp); | ||
|
||
if (tstate != interp->threads.head || tstate->next != NULL) { | ||
Py_FatalError("not the last thread"); | ||
} | ||
|
||
_PyRuntimeState *runtime = interp->runtime; | ||
_PyEval_StopTheWorldAll(runtime); | ||
/* Remaining daemon threads will automatically exit | ||
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */ | ||
_PyInterpreterState_SetFinalizing(interp, tstate); | ||
|
||
PyThreadState *list = _PyThreadState_RemoveExcept(tstate); | ||
for (PyThreadState *p = list; p != NULL; p = p->next) { | ||
_PyThreadState_SetShuttingDown(p); | ||
} | ||
|
||
_PyEval_StartTheWorldAll(runtime); | ||
_PyThreadState_DeleteList(list, /*is_after_fork=*/0); | ||
|
||
// XXX Call something like _PyImport_Disable() here? | ||
|
||
_PyImport_FiniExternal(tstate->interp); | ||
|
@@ -2480,6 +2492,8 @@ finalize_subinterpreters(void) | |
PyInterpreterState *main_interp = _PyInterpreterState_Main(); | ||
assert(final_tstate->interp == main_interp); | ||
_PyRuntimeState *runtime = main_interp->runtime; | ||
assert(!runtime->stoptheworld.world_stopped); | ||
assert(_PyRuntimeState_GetFinalizing(runtime) == NULL); | ||
struct pyinterpreters *interpreters = &runtime->interpreters; | ||
|
||
/* Get the first interpreter in the list. */ | ||
|
@@ -2508,27 +2522,17 @@ finalize_subinterpreters(void) | |
|
||
/* Clean up all remaining subinterpreters. */ | ||
while (interp != NULL) { | ||
assert(!_PyInterpreterState_IsRunningMain(interp)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this assert removed? It should definitely still hold. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, couldn't a daemon thread be running the interpreter? They haven't shutdown at this point, because we had to move where |
||
|
||
/* Find the tstate to use for fini. We assume the interpreter | ||
will have at most one tstate at this point. */ | ||
PyThreadState *tstate = interp->threads.head; | ||
if (tstate != NULL) { | ||
/* Ideally we would be able to use tstate as-is, and rely | ||
on it being in a ready state: no exception set, not | ||
running anything (tstate->current_frame), matching the | ||
current thread ID (tstate->thread_id). To play it safe, | ||
we always delete it and use a fresh tstate instead. */ | ||
assert(tstate != final_tstate); | ||
_PyThreadState_Attach(tstate); | ||
PyThreadState_Clear(tstate); | ||
_PyThreadState_Detach(tstate); | ||
PyThreadState_Delete(tstate); | ||
/* Make a tstate for finalization. */ | ||
PyThreadState *tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI); | ||
if (tstate == NULL) { | ||
// XXX Some graceful way to always get a thread state? | ||
Py_FatalError("thread state allocation failed"); | ||
} | ||
tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI); | ||
|
||
/* Destroy the subinterpreter. */ | ||
/* Enter the subinterpreter. */ | ||
_PyThreadState_Attach(tstate); | ||
|
||
/* Destroy the subinterpreter. */ | ||
Py_EndInterpreter(tstate); | ||
assert(_PyThreadState_GET() == NULL); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I've understood correctly, this is the only difference from the original PR, which had the following change in this part:
The difference entails:
_PyThreadState_RemoveExcept()
after calling_PyInterpreterState_SetFinalizing()
, instead of right before_PyThreadState_SetShuttingDown()
on each of the removed thread states, before starting the world againIs that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that looks about right. Take a look at how the main interpreter does this for reference. The particularly important part was
_PyThreadState_SetShuttingDown
, because without it, daemon threads don't hang.