Skip to content

Commit 32466c9

Browse files
gh-109793: Allow Switching Interpreters During Finalization (gh-109794)
Essentially, we should check the thread ID rather than the thread state pointer.
1 parent f49958c commit 32466c9

File tree

7 files changed

+96
-3
lines changed

7 files changed

+96
-3
lines changed

Include/cpython/pyatomic.h

+17
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,20 @@ static inline void _Py_atomic_fence_release(void);
501501
#else
502502
# error "no available pyatomic implementation for this platform/compiler"
503503
#endif
504+
505+
506+
// --- aliases ---------------------------------------------------------------
507+
508+
#if SIZEOF_LONG == 8
509+
# define _Py_atomic_load_ulong _Py_atomic_load_uint64
510+
# define _Py_atomic_load_ulong_relaxed _Py_atomic_load_uint64_relaxed
511+
# define _Py_atomic_store_ulong _Py_atomic_store_uint64
512+
# define _Py_atomic_store_ulong_relaxed _Py_atomic_store_uint64_relaxed
513+
#elif SIZEOF_LONG == 4
514+
# define _Py_atomic_load_ulong _Py_atomic_load_uint32
515+
# define _Py_atomic_load_ulong_relaxed _Py_atomic_load_uint32_relaxed
516+
# define _Py_atomic_store_ulong _Py_atomic_store_uint32
517+
# define _Py_atomic_store_ulong_relaxed _Py_atomic_store_uint32_relaxed
518+
#else
519+
# error "long must be 4 or 8 bytes in size"
520+
#endif // SIZEOF_LONG

Include/internal/pycore_interp.h

+16
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ struct _is {
9393
and _PyInterpreterState_SetFinalizing()
9494
to access it, don't access it directly. */
9595
_Py_atomic_address _finalizing;
96+
/* The ID of the OS thread in which we are finalizing. */
97+
unsigned long _finalizing_id;
9698

9799
struct _gc_runtime_state gc;
98100

@@ -215,9 +217,23 @@ _PyInterpreterState_GetFinalizing(PyInterpreterState *interp) {
215217
return (PyThreadState*)_Py_atomic_load_relaxed(&interp->_finalizing);
216218
}
217219

220+
static inline unsigned long
221+
_PyInterpreterState_GetFinalizingID(PyInterpreterState *interp) {
222+
return _Py_atomic_load_ulong_relaxed(&interp->_finalizing_id);
223+
}
224+
218225
static inline void
219226
_PyInterpreterState_SetFinalizing(PyInterpreterState *interp, PyThreadState *tstate) {
220227
_Py_atomic_store_relaxed(&interp->_finalizing, (uintptr_t)tstate);
228+
if (tstate == NULL) {
229+
_Py_atomic_store_ulong_relaxed(&interp->_finalizing_id, 0);
230+
}
231+
else {
232+
// XXX Re-enable this assert once gh-109860 is fixed.
233+
//assert(tstate->thread_id == PyThread_get_thread_ident());
234+
_Py_atomic_store_ulong_relaxed(&interp->_finalizing_id,
235+
tstate->thread_id);
236+
}
221237
}
222238

223239

Include/internal/pycore_pystate.h

+6-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ _Py_IsMainInterpreter(PyInterpreterState *interp)
3636
static inline int
3737
_Py_IsMainInterpreterFinalizing(PyInterpreterState *interp)
3838
{
39-
return (_PyRuntimeState_GetFinalizing(interp->runtime) != NULL &&
40-
interp == &interp->runtime->_main_interpreter);
39+
/* bpo-39877: Access _PyRuntime directly rather than using
40+
tstate->interp->runtime to support calls from Python daemon threads.
41+
After Py_Finalize() has been called, tstate can be a dangling pointer:
42+
point to PyThreadState freed memory. */
43+
return (_PyRuntimeState_GetFinalizing(&_PyRuntime) != NULL &&
44+
interp == &_PyRuntime._main_interpreter);
4145
}
4246

4347

Include/internal/pycore_runtime.h

+16
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ typedef struct pyruntimestate {
171171
Use _PyRuntimeState_GetFinalizing() and _PyRuntimeState_SetFinalizing()
172172
to access it, don't access it directly. */
173173
_Py_atomic_address _finalizing;
174+
/* The ID of the OS thread in which we are finalizing. */
175+
unsigned long _finalizing_id;
174176

175177
struct pyinterpreters {
176178
PyThread_type_lock mutex;
@@ -303,9 +305,23 @@ _PyRuntimeState_GetFinalizing(_PyRuntimeState *runtime) {
303305
return (PyThreadState*)_Py_atomic_load_relaxed(&runtime->_finalizing);
304306
}
305307

308+
static inline unsigned long
309+
_PyRuntimeState_GetFinalizingID(_PyRuntimeState *runtime) {
310+
return _Py_atomic_load_ulong_relaxed(&runtime->_finalizing_id);
311+
}
312+
306313
static inline void
307314
_PyRuntimeState_SetFinalizing(_PyRuntimeState *runtime, PyThreadState *tstate) {
308315
_Py_atomic_store_relaxed(&runtime->_finalizing, (uintptr_t)tstate);
316+
if (tstate == NULL) {
317+
_Py_atomic_store_ulong_relaxed(&runtime->_finalizing_id, 0);
318+
}
319+
else {
320+
// XXX Re-enable this assert once gh-109860 is fixed.
321+
//assert(tstate->thread_id == PyThread_get_thread_ident());
322+
_Py_atomic_store_ulong_relaxed(&runtime->_finalizing_id,
323+
tstate->thread_id);
324+
}
309325
}
310326

311327
#ifdef __cplusplus

Lib/test/test_interpreters.py

+21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import os
3+
import sys
34
import threading
45
from textwrap import dedent
56
import unittest
@@ -487,6 +488,26 @@ def task():
487488
pass
488489

489490

491+
class FinalizationTests(TestBase):
492+
493+
def test_gh_109793(self):
494+
import subprocess
495+
argv = [sys.executable, '-c', '''if True:
496+
import _xxsubinterpreters as _interpreters
497+
interpid = _interpreters.create()
498+
raise Exception
499+
''']
500+
proc = subprocess.run(argv, capture_output=True, text=True)
501+
self.assertIn('Traceback', proc.stderr)
502+
if proc.returncode == 0 and support.verbose:
503+
print()
504+
print("--- cmd unexpected succeeded ---")
505+
print(f"stdout:\n{proc.stdout}")
506+
print(f"stderr:\n{proc.stderr}")
507+
print("------")
508+
self.assertEqual(proc.returncode, 1)
509+
510+
490511
class TestIsShareable(TestBase):
491512

492513
def test_default_shareables(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The main thread no longer exits prematurely when a subinterpreter
2+
is cleaned up during runtime finalization. The bug was a problem
3+
particularly because, when triggered, the Python process would
4+
always return with a 0 exitcode, even if it failed.

Python/pystate.c

+16-1
Original file line numberDiff line numberDiff line change
@@ -2964,11 +2964,26 @@ _PyThreadState_MustExit(PyThreadState *tstate)
29642964
tstate->interp->runtime to support calls from Python daemon threads.
29652965
After Py_Finalize() has been called, tstate can be a dangling pointer:
29662966
point to PyThreadState freed memory. */
2967+
unsigned long finalizing_id = _PyRuntimeState_GetFinalizingID(&_PyRuntime);
29672968
PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime);
29682969
if (finalizing == NULL) {
2970+
// XXX This isn't completely safe from daemon thraeds,
2971+
// since tstate might be a dangling pointer.
29692972
finalizing = _PyInterpreterState_GetFinalizing(tstate->interp);
2973+
finalizing_id = _PyInterpreterState_GetFinalizingID(tstate->interp);
29702974
}
2971-
return (finalizing != NULL && finalizing != tstate);
2975+
// XXX else check &_PyRuntime._main_interpreter._initial_thread
2976+
if (finalizing == NULL) {
2977+
return 0;
2978+
}
2979+
else if (finalizing == tstate) {
2980+
return 0;
2981+
}
2982+
else if (finalizing_id == PyThread_get_thread_ident()) {
2983+
/* gh-109793: we must have switched interpreters. */
2984+
return 0;
2985+
}
2986+
return 1;
29722987
}
29732988

29742989

0 commit comments

Comments
 (0)