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-128639: Don't assume one thread in subinterpreter finalization #128640

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
56 changes: 54 additions & 2 deletions Lib/test/test_interpreters/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,55 @@ def test_created_with_capi(self):
self.interp_exists(interpid))


def test_remaining_threads(self):
r_interp, w_interp = self.pipe()

FINISHED = b'F'

interp = interpreters.create()
interp.exec(f"""if True:
import os
import threading
import time

def task():
time.sleep(1)
os.write({w_interp}, {FINISHED!r})

threads = [threading.Thread(target=task) for _ in range(3)]
for t in threads:
t.start()
""")
interp.close()

self.assertEqual(os.read(r_interp, 1), FINISHED)

def test_remaining_daemon_threads(self):
interp = _interpreters.create(
types.SimpleNamespace(
use_main_obmalloc=False,
allow_fork=False,
allow_exec=False,
allow_threads=True,
allow_daemon_threads=True,
check_multi_interp_extensions=True,
gil='own',
)
)
_interpreters.exec(interp, f"""if True:
import threading
import time

def task():
time.sleep(100)

threads = [threading.Thread(target=task, daemon=True) for _ in range(3)]
for t in threads:
t.start()
""")
_interpreters.destroy(interp)


class TestInterpreterPrepareMain(TestBase):

def test_empty(self):
Expand Down Expand Up @@ -756,7 +805,10 @@ def script():
spam.eggs()

interp = interpreters.create()
interp.exec(script)
try:
interp.exec(script)
finally:
interp.close()
""")

stdout, stderr = self.assert_python_failure(scriptfile)
Expand All @@ -765,7 +817,7 @@ def script():
# File "{interpreters.__file__}", line 179, in exec
self.assertEqual(stderr, dedent(f"""\
Traceback (most recent call last):
File "{scriptfile}", line 9, in <module>
File "{scriptfile}", line 10, in <module>
interp.exec(script)
~~~~~~~~~~~^^^^^^^^
{interpmod_line.strip()}
Expand Down
6 changes: 5 additions & 1 deletion Lib/test/test_interpreters/test_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def test_sys_path_0(self):
'sub': sys.path[0],
}}, indent=4), flush=True)
""")
interp.close()
'''
# <tmp>/
# pkg/
Expand Down Expand Up @@ -172,7 +173,10 @@ def test_gh_109793(self):
argv = [sys.executable, '-c', '''if True:
from test.support import interpreters
interp = interpreters.create()
raise Exception
try:
raise Exception
finally:
interp.close()
''']
proc = subprocess.run(argv, capture_output=True, text=True)
self.assertIn('Traceback', proc.stderr)
Expand Down
6 changes: 2 additions & 4 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,7 @@ def task():
self.assertEqual(os.read(r_interp, 1), DONE)

@cpython_only
@unittest.skipIf(True, "only crashes some of the time now that Py_EndInterpreter() can handle more threads")
def test_daemon_threads_fatal_error(self):
import_module("_testcapi")
subinterp_code = f"""if 1:
Expand All @@ -1612,10 +1613,7 @@ def f():

_testcapi.run_in_subinterp(%r)
""" % (subinterp_code,)
with test.support.SuppressCrashReport():
rc, out, err = assert_python_failure("-c", script)
self.assertIn("Fatal Python error: Py_EndInterpreter: "
"not the last thread", err.decode())
assert_python_ok("-c", script)

def _check_allowed(self, before_start='', *,
allowed=True,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a crash when using threads inside of a subinterpreter.
9 changes: 6 additions & 3 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -1424,9 +1424,12 @@ static int test_audit_subinterpreter(void)
PySys_AddAuditHook(_audit_subinterpreter_hook, NULL);
_testembed_Py_InitializeFromConfig();

Py_NewInterpreter();
Py_NewInterpreter();
Py_NewInterpreter();
PyThreadState *tstate = PyThreadState_Get();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test working before was a fluke. Py_NewInterpreter changes the thread state, and I don't think we support calling Py_Finalize from a subinterpreter.

for (int i = 0; i < 3; ++i)
{
Py_EndInterpreter(Py_NewInterpreter());
PyThreadState_Swap(tstate);
}

Py_Finalize();

Expand Down
57 changes: 29 additions & 28 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -2054,12 +2054,23 @@ _Py_Finalize(_PyRuntimeState *runtime)
int malloc_stats = tstate->interp->config.malloc_stats;
#endif

/* 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();

/* Ensure that remaining threads are detached */
_PyEval_StopTheWorldAll(runtime);

/* Remaining daemon threads will be trapped in PyThread_hang_thread
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
_PyInterpreterState_SetFinalizing(tstate->interp, tstate);

_PyRuntimeState_SetFinalizing(runtime, tstate);
runtime->initialized = 0;
runtime->core_initialized = 0;
Expand Down Expand Up @@ -2121,9 +2132,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();

Expand Down Expand Up @@ -2406,9 +2414,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
Expand Down Expand Up @@ -2438,14 +2445,15 @@ 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);
PyThreadState *list = _PyThreadState_RemoveExcept(tstate);

/* Remaining daemon threads will automatically exit
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
_PyInterpreterState_SetFinalizing(interp, tstate);
_PyEval_StartTheWorldAll(runtime);
_PyThreadState_DeleteList(list);

// XXX Call something like _PyImport_Disable() here?

Expand Down Expand Up @@ -2476,6 +2484,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. */
Expand Down Expand Up @@ -2504,27 +2514,18 @@ finalize_subinterpreters(void)

/* Clean up all remaining subinterpreters. */
while (interp != NULL) {
assert(!_PyInterpreterState_IsRunningMain(interp));

/* 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);

Expand Down
Loading