Skip to content

Commit cec4ddf

Browse files
[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 a648813) Co-authored-by: Peter Bierma <[email protected]>
1 parent 08bea29 commit cec4ddf

File tree

6 files changed

+114
-39
lines changed

6 files changed

+114
-39
lines changed

Lib/test/test_interpreters/test_api.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from test.support import os_helper
1212
from test.support import script_helper
1313
from test.support import import_helper
14+
from test.support.script_helper import assert_python_ok
1415
# Raise SkipTest if subinterpreters not supported.
1516
_interpreters = import_helper.import_module('_interpreters')
1617
from concurrent import interpreters
@@ -707,6 +708,68 @@ def test_created_with_capi(self):
707708
self.interp_exists(interpid))
708709

709710

711+
def test_remaining_threads(self):
712+
r_interp, w_interp = self.pipe()
713+
714+
FINISHED = b'F'
715+
716+
# It's unlikely, but technically speaking, it's possible
717+
# that the thread could've finished before interp.close() is
718+
# reached, so this test might not properly exercise the case.
719+
# However, it's quite unlikely and probably not worth bothering about.
720+
interp = interpreters.create()
721+
interp.exec(f"""if True:
722+
import os
723+
import threading
724+
import time
725+
726+
def task():
727+
time.sleep(1)
728+
os.write({w_interp}, {FINISHED!r})
729+
730+
threads = (threading.Thread(target=task) for _ in range(3))
731+
for t in threads:
732+
t.start()
733+
""")
734+
interp.close()
735+
736+
self.assertEqual(os.read(r_interp, 1), FINISHED)
737+
738+
def test_remaining_daemon_threads(self):
739+
# Daemon threads leak reference by nature, because they hang threads
740+
# without allowing them to do cleanup (i.e., release refs).
741+
# To prevent that from messing up the refleak hunter and whatnot, we
742+
# run this in a subprocess.
743+
code = '''if True:
744+
import _interpreters
745+
import types
746+
interp = _interpreters.create(
747+
types.SimpleNamespace(
748+
use_main_obmalloc=False,
749+
allow_fork=False,
750+
allow_exec=False,
751+
allow_threads=True,
752+
allow_daemon_threads=True,
753+
check_multi_interp_extensions=True,
754+
gil='own',
755+
)
756+
)
757+
_interpreters.exec(interp, f"""if True:
758+
import threading
759+
import time
760+
761+
def task():
762+
time.sleep(3)
763+
764+
threads = (threading.Thread(target=task, daemon=True) for _ in range(3))
765+
for t in threads:
766+
t.start()
767+
""")
768+
_interpreters.destroy(interp)
769+
'''
770+
assert_python_ok('-c', code)
771+
772+
710773
class TestInterpreterPrepareMain(TestBase):
711774

712775
def test_empty(self):
@@ -815,7 +878,10 @@ def script():
815878
spam.eggs()
816879
817880
interp = interpreters.create()
818-
interp.exec(script)
881+
try:
882+
interp.exec(script)
883+
finally:
884+
interp.close()
819885
""")
820886

821887
stdout, stderr = self.assert_python_failure(scriptfile)
@@ -824,7 +890,7 @@ def script():
824890
# File "{interpreters.__file__}", line 179, in exec
825891
self.assertEqual(stderr, dedent(f"""\
826892
Traceback (most recent call last):
827-
File "{scriptfile}", line 9, in <module>
893+
File "{scriptfile}", line 10, in <module>
828894
interp.exec(script)
829895
~~~~~~~~~~~^^^^^^^^
830896
{interpmod_line.strip()}

Lib/test/test_interpreters/test_lifecycle.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def test_sys_path_0(self):
132132
'sub': sys.path[0],
133133
}}, indent=4), flush=True)
134134
""")
135+
interp.close()
135136
'''
136137
# <tmp>/
137138
# pkg/
@@ -172,7 +173,10 @@ def test_gh_109793(self):
172173
argv = [sys.executable, '-c', '''if True:
173174
from concurrent import interpreters
174175
interp = interpreters.create()
175-
raise Exception
176+
try:
177+
raise Exception
178+
finally:
179+
interp.close()
176180
''']
177181
proc = subprocess.run(argv, capture_output=True, text=True)
178182
self.assertIn('Traceback', proc.stderr)

Lib/test/test_threading.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,10 +1718,7 @@ def f():
17181718
17191719
_testcapi.run_in_subinterp(%r)
17201720
""" % (subinterp_code,)
1721-
with test.support.SuppressCrashReport():
1722-
rc, out, err = assert_python_failure("-c", script)
1723-
self.assertIn("Fatal Python error: Py_EndInterpreter: "
1724-
"not the last thread", err.decode())
1721+
assert_python_ok("-c", script)
17251722

17261723
def _check_allowed(self, before_start='', *,
17271724
allowed=True,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a crash when using threads inside of a subinterpreter.

Programs/_testembed.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,9 +1434,12 @@ static int test_audit_subinterpreter(void)
14341434
PySys_AddAuditHook(_audit_subinterpreter_hook, NULL);
14351435
_testembed_Py_InitializeFromConfig();
14361436

1437-
Py_NewInterpreter();
1438-
Py_NewInterpreter();
1439-
Py_NewInterpreter();
1437+
PyThreadState *tstate = PyThreadState_Get();
1438+
for (int i = 0; i < 3; ++i)
1439+
{
1440+
Py_EndInterpreter(Py_NewInterpreter());
1441+
PyThreadState_Swap(tstate);
1442+
}
14401443

14411444
Py_Finalize();
14421445

Python/pylifecycle.c

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,6 +1999,7 @@ resolve_final_tstate(_PyRuntimeState *runtime)
19991999
}
20002000
else {
20012001
/* Fall back to the current tstate. It's better than nothing. */
2002+
// XXX No it's not
20022003
main_tstate = tstate;
20032004
}
20042005
}
@@ -2044,6 +2045,16 @@ _Py_Finalize(_PyRuntimeState *runtime)
20442045

20452046
_PyAtExit_Call(tstate->interp);
20462047

2048+
/* Clean up any lingering subinterpreters.
2049+
2050+
Two preconditions need to be met here:
2051+
2052+
- This has to happen before _PyRuntimeState_SetFinalizing is
2053+
called, or else threads might get prematurely blocked.
2054+
- The world must not be stopped, as finalizers can run.
2055+
*/
2056+
finalize_subinterpreters();
2057+
20472058
assert(_PyThreadState_GET() == tstate);
20482059

20492060
/* Copy the core config, PyInterpreterState_Delete() free
@@ -2131,9 +2142,6 @@ _Py_Finalize(_PyRuntimeState *runtime)
21312142
_PyImport_FiniExternal(tstate->interp);
21322143
finalize_modules(tstate);
21332144

2134-
/* Clean up any lingering subinterpreters. */
2135-
finalize_subinterpreters();
2136-
21372145
/* Print debug stats if any */
21382146
_PyEval_Fini();
21392147

@@ -2414,9 +2422,8 @@ Py_NewInterpreter(void)
24142422
return tstate;
24152423
}
24162424

2417-
/* Delete an interpreter and its last thread. This requires that the
2418-
given thread state is current, that the thread has no remaining
2419-
frames, and that it is its interpreter's only remaining thread.
2425+
/* Delete an interpreter. This requires that the given thread state
2426+
is current, and that the thread has no remaining frames.
24202427
It is a fatal error to violate these constraints.
24212428
24222429
(Py_FinalizeEx() doesn't have these constraints -- it zaps
@@ -2446,15 +2453,20 @@ Py_EndInterpreter(PyThreadState *tstate)
24462453
_Py_FinishPendingCalls(tstate);
24472454

24482455
_PyAtExit_Call(tstate->interp);
2449-
2450-
if (tstate != interp->threads.head || tstate->next != NULL) {
2451-
Py_FatalError("not the last thread");
2452-
}
2453-
2456+
_PyRuntimeState *runtime = interp->runtime;
2457+
_PyEval_StopTheWorldAll(runtime);
24542458
/* Remaining daemon threads will automatically exit
24552459
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
24562460
_PyInterpreterState_SetFinalizing(interp, tstate);
24572461

2462+
PyThreadState *list = _PyThreadState_RemoveExcept(tstate);
2463+
for (PyThreadState *p = list; p != NULL; p = p->next) {
2464+
_PyThreadState_SetShuttingDown(p);
2465+
}
2466+
2467+
_PyEval_StartTheWorldAll(runtime);
2468+
_PyThreadState_DeleteList(list, /*is_after_fork=*/0);
2469+
24582470
// XXX Call something like _PyImport_Disable() here?
24592471

24602472
_PyImport_FiniExternal(tstate->interp);
@@ -2484,6 +2496,8 @@ finalize_subinterpreters(void)
24842496
PyInterpreterState *main_interp = _PyInterpreterState_Main();
24852497
assert(final_tstate->interp == main_interp);
24862498
_PyRuntimeState *runtime = main_interp->runtime;
2499+
assert(!runtime->stoptheworld.world_stopped);
2500+
assert(_PyRuntimeState_GetFinalizing(runtime) == NULL);
24872501
struct pyinterpreters *interpreters = &runtime->interpreters;
24882502

24892503
/* Get the first interpreter in the list. */
@@ -2512,27 +2526,17 @@ finalize_subinterpreters(void)
25122526

25132527
/* Clean up all remaining subinterpreters. */
25142528
while (interp != NULL) {
2515-
assert(!_PyInterpreterState_IsRunningMain(interp));
2516-
2517-
/* Find the tstate to use for fini. We assume the interpreter
2518-
will have at most one tstate at this point. */
2519-
PyThreadState *tstate = interp->threads.head;
2520-
if (tstate != NULL) {
2521-
/* Ideally we would be able to use tstate as-is, and rely
2522-
on it being in a ready state: no exception set, not
2523-
running anything (tstate->current_frame), matching the
2524-
current thread ID (tstate->thread_id). To play it safe,
2525-
we always delete it and use a fresh tstate instead. */
2526-
assert(tstate != final_tstate);
2527-
_PyThreadState_Attach(tstate);
2528-
PyThreadState_Clear(tstate);
2529-
_PyThreadState_Detach(tstate);
2530-
PyThreadState_Delete(tstate);
2529+
/* Make a tstate for finalization. */
2530+
PyThreadState *tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI);
2531+
if (tstate == NULL) {
2532+
// XXX Some graceful way to always get a thread state?
2533+
Py_FatalError("thread state allocation failed");
25312534
}
2532-
tstate = _PyThreadState_NewBound(interp, _PyThreadState_WHENCE_FINI);
25332535

2534-
/* Destroy the subinterpreter. */
2536+
/* Enter the subinterpreter. */
25352537
_PyThreadState_Attach(tstate);
2538+
2539+
/* Destroy the subinterpreter. */
25362540
Py_EndInterpreter(tstate);
25372541
assert(_PyThreadState_GET() == NULL);
25382542

0 commit comments

Comments
 (0)