[3.14] gh-128639: Don't assume one thread in subinterpreter finalization with fixed daemon thread support (GH-134606) (GH-139050)

gh-128639: Don't assume one thread in subinterpreter finalization with fixed daemon thread support (GH-134606)

This reapplies GH-128640.
(cherry picked from commit a64881363b)

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-10-07 19:30:38 +02:00 committed by GitHub
parent 08bea299bf
commit cec4ddf23e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 114 additions and 39 deletions

View file

@ -11,6 +11,7 @@
from test.support import os_helper from test.support import os_helper
from test.support import script_helper from test.support import script_helper
from test.support import import_helper from test.support import import_helper
from test.support.script_helper import assert_python_ok
# Raise SkipTest if subinterpreters not supported. # Raise SkipTest if subinterpreters not supported.
_interpreters = import_helper.import_module('_interpreters') _interpreters = import_helper.import_module('_interpreters')
from concurrent import interpreters from concurrent import interpreters
@ -707,6 +708,68 @@ def test_created_with_capi(self):
self.interp_exists(interpid)) self.interp_exists(interpid))
def test_remaining_threads(self):
r_interp, w_interp = self.pipe()
FINISHED = b'F'
# It's unlikely, but technically speaking, it's possible
# that the thread could've finished before interp.close() is
# reached, so this test might not properly exercise the case.
# However, it's quite unlikely and probably not worth bothering about.
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):
# Daemon threads leak reference by nature, because they hang threads
# without allowing them to do cleanup (i.e., release refs).
# To prevent that from messing up the refleak hunter and whatnot, we
# run this in a subprocess.
code = '''if True:
import _interpreters
import types
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(3)
threads = (threading.Thread(target=task, daemon=True) for _ in range(3))
for t in threads:
t.start()
""")
_interpreters.destroy(interp)
'''
assert_python_ok('-c', code)
class TestInterpreterPrepareMain(TestBase): class TestInterpreterPrepareMain(TestBase):
def test_empty(self): def test_empty(self):
@ -815,7 +878,10 @@ def script():
spam.eggs() spam.eggs()
interp = interpreters.create() interp = interpreters.create()
interp.exec(script) try:
interp.exec(script)
finally:
interp.close()
""") """)
stdout, stderr = self.assert_python_failure(scriptfile) stdout, stderr = self.assert_python_failure(scriptfile)
@ -824,7 +890,7 @@ def script():
# File "{interpreters.__file__}", line 179, in exec # File "{interpreters.__file__}", line 179, in exec
self.assertEqual(stderr, dedent(f"""\ self.assertEqual(stderr, dedent(f"""\
Traceback (most recent call last): Traceback (most recent call last):
File "{scriptfile}", line 9, in <module> File "{scriptfile}", line 10, in <module>
interp.exec(script) interp.exec(script)
~~~~~~~~~~~^^^^^^^^ ~~~~~~~~~~~^^^^^^^^
{interpmod_line.strip()} {interpmod_line.strip()}

View file

@ -132,6 +132,7 @@ def test_sys_path_0(self):
'sub': sys.path[0], 'sub': sys.path[0],
}}, indent=4), flush=True) }}, indent=4), flush=True)
""") """)
interp.close()
''' '''
# <tmp>/ # <tmp>/
# pkg/ # pkg/
@ -172,7 +173,10 @@ def test_gh_109793(self):
argv = [sys.executable, '-c', '''if True: argv = [sys.executable, '-c', '''if True:
from concurrent import interpreters from concurrent import interpreters
interp = interpreters.create() interp = interpreters.create()
raise Exception try:
raise Exception
finally:
interp.close()
'''] ''']
proc = subprocess.run(argv, capture_output=True, text=True) proc = subprocess.run(argv, capture_output=True, text=True)
self.assertIn('Traceback', proc.stderr) self.assertIn('Traceback', proc.stderr)

View file

@ -1718,10 +1718,7 @@ def f():
_testcapi.run_in_subinterp(%r) _testcapi.run_in_subinterp(%r)
""" % (subinterp_code,) """ % (subinterp_code,)
with test.support.SuppressCrashReport(): assert_python_ok("-c", script)
rc, out, err = assert_python_failure("-c", script)
self.assertIn("Fatal Python error: Py_EndInterpreter: "
"not the last thread", err.decode())
def _check_allowed(self, before_start='', *, def _check_allowed(self, before_start='', *,
allowed=True, allowed=True,

View file

@ -0,0 +1 @@
Fix a crash when using threads inside of a subinterpreter.

View file

@ -1434,9 +1434,12 @@ static int test_audit_subinterpreter(void)
PySys_AddAuditHook(_audit_subinterpreter_hook, NULL); PySys_AddAuditHook(_audit_subinterpreter_hook, NULL);
_testembed_Py_InitializeFromConfig(); _testembed_Py_InitializeFromConfig();
Py_NewInterpreter(); PyThreadState *tstate = PyThreadState_Get();
Py_NewInterpreter(); for (int i = 0; i < 3; ++i)
Py_NewInterpreter(); {
Py_EndInterpreter(Py_NewInterpreter());
PyThreadState_Swap(tstate);
}
Py_Finalize(); Py_Finalize();

View file

@ -1999,6 +1999,7 @@ resolve_final_tstate(_PyRuntimeState *runtime)
} }
else { else {
/* Fall back to the current tstate. It's better than nothing. */ /* Fall back to the current tstate. It's better than nothing. */
// XXX No it's not
main_tstate = tstate; main_tstate = tstate;
} }
} }
@ -2044,6 +2045,16 @@ _Py_Finalize(_PyRuntimeState *runtime)
_PyAtExit_Call(tstate->interp); _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); assert(_PyThreadState_GET() == tstate);
/* Copy the core config, PyInterpreterState_Delete() free /* Copy the core config, PyInterpreterState_Delete() free
@ -2131,9 +2142,6 @@ _Py_Finalize(_PyRuntimeState *runtime)
_PyImport_FiniExternal(tstate->interp); _PyImport_FiniExternal(tstate->interp);
finalize_modules(tstate); finalize_modules(tstate);
/* Clean up any lingering subinterpreters. */
finalize_subinterpreters();
/* Print debug stats if any */ /* Print debug stats if any */
_PyEval_Fini(); _PyEval_Fini();
@ -2414,9 +2422,8 @@ Py_NewInterpreter(void)
return tstate; return tstate;
} }
/* Delete an interpreter and its last thread. This requires that the /* Delete an interpreter. This requires that the given thread state
given thread state is current, that the thread has no remaining is current, and that the thread has no remaining frames.
frames, and that it is its interpreter's only remaining thread.
It is a fatal error to violate these constraints. It is a fatal error to violate these constraints.
(Py_FinalizeEx() doesn't have these constraints -- it zaps (Py_FinalizeEx() doesn't have these constraints -- it zaps
@ -2446,15 +2453,20 @@ Py_EndInterpreter(PyThreadState *tstate)
_Py_FinishPendingCalls(tstate); _Py_FinishPendingCalls(tstate);
_PyAtExit_Call(tstate->interp); _PyAtExit_Call(tstate->interp);
_PyRuntimeState *runtime = interp->runtime;
if (tstate != interp->threads.head || tstate->next != NULL) { _PyEval_StopTheWorldAll(runtime);
Py_FatalError("not the last thread");
}
/* Remaining daemon threads will automatically exit /* Remaining daemon threads will automatically exit
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */ when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
_PyInterpreterState_SetFinalizing(interp, tstate); _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? // XXX Call something like _PyImport_Disable() here?
_PyImport_FiniExternal(tstate->interp); _PyImport_FiniExternal(tstate->interp);
@ -2484,6 +2496,8 @@ finalize_subinterpreters(void)
PyInterpreterState *main_interp = _PyInterpreterState_Main(); PyInterpreterState *main_interp = _PyInterpreterState_Main();
assert(final_tstate->interp == main_interp); assert(final_tstate->interp == main_interp);
_PyRuntimeState *runtime = main_interp->runtime; _PyRuntimeState *runtime = main_interp->runtime;
assert(!runtime->stoptheworld.world_stopped);
assert(_PyRuntimeState_GetFinalizing(runtime) == NULL);
struct pyinterpreters *interpreters = &runtime->interpreters; struct pyinterpreters *interpreters = &runtime->interpreters;
/* Get the first interpreter in the list. */ /* Get the first interpreter in the list. */
@ -2512,27 +2526,17 @@ finalize_subinterpreters(void)
/* Clean up all remaining subinterpreters. */ /* Clean up all remaining subinterpreters. */
while (interp != NULL) { while (interp != NULL) {
assert(!_PyInterpreterState_IsRunningMain(interp)); /* Make a tstate for finalization. */
PyThreadState *tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI);
/* Find the tstate to use for fini. We assume the interpreter if (tstate == NULL) {
will have at most one tstate at this point. */ // XXX Some graceful way to always get a thread state?
PyThreadState *tstate = interp->threads.head; Py_FatalError("thread state allocation failed");
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);
} }
tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI);
/* Enter the subinterpreter. */
_PyThreadState_Attach(tstate);
/* Destroy the subinterpreter. */ /* Destroy the subinterpreter. */
_PyThreadState_Attach(tstate);
Py_EndInterpreter(tstate); Py_EndInterpreter(tstate);
assert(_PyThreadState_GET() == NULL); assert(_PyThreadState_GET() == NULL);