gh-109793: Allow Switching Interpreters During Finalization (gh-109794)

Essentially, we should check the thread ID rather than the thread state pointer.
This commit is contained in:
Eric Snow 2023-09-27 13:41:06 -06:00 committed by GitHub
parent f49958c886
commit 32466c97c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 3 deletions

View file

@ -501,3 +501,20 @@ static inline void _Py_atomic_fence_release(void);
#else #else
# error "no available pyatomic implementation for this platform/compiler" # error "no available pyatomic implementation for this platform/compiler"
#endif #endif
// --- aliases ---------------------------------------------------------------
#if SIZEOF_LONG == 8
# define _Py_atomic_load_ulong _Py_atomic_load_uint64
# define _Py_atomic_load_ulong_relaxed _Py_atomic_load_uint64_relaxed
# define _Py_atomic_store_ulong _Py_atomic_store_uint64
# define _Py_atomic_store_ulong_relaxed _Py_atomic_store_uint64_relaxed
#elif SIZEOF_LONG == 4
# define _Py_atomic_load_ulong _Py_atomic_load_uint32
# define _Py_atomic_load_ulong_relaxed _Py_atomic_load_uint32_relaxed
# define _Py_atomic_store_ulong _Py_atomic_store_uint32
# define _Py_atomic_store_ulong_relaxed _Py_atomic_store_uint32_relaxed
#else
# error "long must be 4 or 8 bytes in size"
#endif // SIZEOF_LONG

View file

@ -93,6 +93,8 @@ struct _is {
and _PyInterpreterState_SetFinalizing() and _PyInterpreterState_SetFinalizing()
to access it, don't access it directly. */ to access it, don't access it directly. */
_Py_atomic_address _finalizing; _Py_atomic_address _finalizing;
/* The ID of the OS thread in which we are finalizing. */
unsigned long _finalizing_id;
struct _gc_runtime_state gc; struct _gc_runtime_state gc;
@ -215,9 +217,23 @@ _PyInterpreterState_GetFinalizing(PyInterpreterState *interp) {
return (PyThreadState*)_Py_atomic_load_relaxed(&interp->_finalizing); return (PyThreadState*)_Py_atomic_load_relaxed(&interp->_finalizing);
} }
static inline unsigned long
_PyInterpreterState_GetFinalizingID(PyInterpreterState *interp) {
return _Py_atomic_load_ulong_relaxed(&interp->_finalizing_id);
}
static inline void static inline void
_PyInterpreterState_SetFinalizing(PyInterpreterState *interp, PyThreadState *tstate) { _PyInterpreterState_SetFinalizing(PyInterpreterState *interp, PyThreadState *tstate) {
_Py_atomic_store_relaxed(&interp->_finalizing, (uintptr_t)tstate); _Py_atomic_store_relaxed(&interp->_finalizing, (uintptr_t)tstate);
if (tstate == NULL) {
_Py_atomic_store_ulong_relaxed(&interp->_finalizing_id, 0);
}
else {
// XXX Re-enable this assert once gh-109860 is fixed.
//assert(tstate->thread_id == PyThread_get_thread_ident());
_Py_atomic_store_ulong_relaxed(&interp->_finalizing_id,
tstate->thread_id);
}
} }

View file

@ -36,8 +36,12 @@ _Py_IsMainInterpreter(PyInterpreterState *interp)
static inline int static inline int
_Py_IsMainInterpreterFinalizing(PyInterpreterState *interp) _Py_IsMainInterpreterFinalizing(PyInterpreterState *interp)
{ {
return (_PyRuntimeState_GetFinalizing(interp->runtime) != NULL && /* bpo-39877: Access _PyRuntime directly rather than using
interp == &interp->runtime->_main_interpreter); tstate->interp->runtime to support calls from Python daemon threads.
After Py_Finalize() has been called, tstate can be a dangling pointer:
point to PyThreadState freed memory. */
return (_PyRuntimeState_GetFinalizing(&_PyRuntime) != NULL &&
interp == &_PyRuntime._main_interpreter);
} }

View file

@ -171,6 +171,8 @@ typedef struct pyruntimestate {
Use _PyRuntimeState_GetFinalizing() and _PyRuntimeState_SetFinalizing() Use _PyRuntimeState_GetFinalizing() and _PyRuntimeState_SetFinalizing()
to access it, don't access it directly. */ to access it, don't access it directly. */
_Py_atomic_address _finalizing; _Py_atomic_address _finalizing;
/* The ID of the OS thread in which we are finalizing. */
unsigned long _finalizing_id;
struct pyinterpreters { struct pyinterpreters {
PyThread_type_lock mutex; PyThread_type_lock mutex;
@ -303,9 +305,23 @@ _PyRuntimeState_GetFinalizing(_PyRuntimeState *runtime) {
return (PyThreadState*)_Py_atomic_load_relaxed(&runtime->_finalizing); return (PyThreadState*)_Py_atomic_load_relaxed(&runtime->_finalizing);
} }
static inline unsigned long
_PyRuntimeState_GetFinalizingID(_PyRuntimeState *runtime) {
return _Py_atomic_load_ulong_relaxed(&runtime->_finalizing_id);
}
static inline void static inline void
_PyRuntimeState_SetFinalizing(_PyRuntimeState *runtime, PyThreadState *tstate) { _PyRuntimeState_SetFinalizing(_PyRuntimeState *runtime, PyThreadState *tstate) {
_Py_atomic_store_relaxed(&runtime->_finalizing, (uintptr_t)tstate); _Py_atomic_store_relaxed(&runtime->_finalizing, (uintptr_t)tstate);
if (tstate == NULL) {
_Py_atomic_store_ulong_relaxed(&runtime->_finalizing_id, 0);
}
else {
// XXX Re-enable this assert once gh-109860 is fixed.
//assert(tstate->thread_id == PyThread_get_thread_ident());
_Py_atomic_store_ulong_relaxed(&runtime->_finalizing_id,
tstate->thread_id);
}
} }
#ifdef __cplusplus #ifdef __cplusplus

View file

@ -1,5 +1,6 @@
import contextlib import contextlib
import os import os
import sys
import threading import threading
from textwrap import dedent from textwrap import dedent
import unittest import unittest
@ -487,6 +488,26 @@ class StressTests(TestBase):
pass pass
class FinalizationTests(TestBase):
def test_gh_109793(self):
import subprocess
argv = [sys.executable, '-c', '''if True:
import _xxsubinterpreters as _interpreters
interpid = _interpreters.create()
raise Exception
''']
proc = subprocess.run(argv, capture_output=True, text=True)
self.assertIn('Traceback', proc.stderr)
if proc.returncode == 0 and support.verbose:
print()
print("--- cmd unexpected succeeded ---")
print(f"stdout:\n{proc.stdout}")
print(f"stderr:\n{proc.stderr}")
print("------")
self.assertEqual(proc.returncode, 1)
class TestIsShareable(TestBase): class TestIsShareable(TestBase):
def test_default_shareables(self): def test_default_shareables(self):

View file

@ -0,0 +1,4 @@
The main thread no longer exits prematurely when a subinterpreter
is cleaned up during runtime finalization. The bug was a problem
particularly because, when triggered, the Python process would
always return with a 0 exitcode, even if it failed.

View file

@ -2964,11 +2964,26 @@ _PyThreadState_MustExit(PyThreadState *tstate)
tstate->interp->runtime to support calls from Python daemon threads. tstate->interp->runtime to support calls from Python daemon threads.
After Py_Finalize() has been called, tstate can be a dangling pointer: After Py_Finalize() has been called, tstate can be a dangling pointer:
point to PyThreadState freed memory. */ point to PyThreadState freed memory. */
unsigned long finalizing_id = _PyRuntimeState_GetFinalizingID(&_PyRuntime);
PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime); PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime);
if (finalizing == NULL) { if (finalizing == NULL) {
// XXX This isn't completely safe from daemon thraeds,
// since tstate might be a dangling pointer.
finalizing = _PyInterpreterState_GetFinalizing(tstate->interp); finalizing = _PyInterpreterState_GetFinalizing(tstate->interp);
finalizing_id = _PyInterpreterState_GetFinalizingID(tstate->interp);
} }
return (finalizing != NULL && finalizing != tstate); // XXX else check &_PyRuntime._main_interpreter._initial_thread
if (finalizing == NULL) {
return 0;
}
else if (finalizing == tstate) {
return 0;
}
else if (finalizing_id == PyThread_get_thread_ident()) {
/* gh-109793: we must have switched interpreters. */
return 0;
}
return 1;
} }