Skip to content

Commit e93393c

Browse files
authored
panic on attach if interpreter is shutting down (#5317)
* panic on attach if interpreter is shutting down * fixup for older versions * fix clippy
1 parent 12a1792 commit e93393c

File tree

8 files changed

+49
-44
lines changed

8 files changed

+49
-44
lines changed

newsfragments/5317.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add FFI definitions `Py_Version` and `Py_IsFinalizing`.

newsfragments/5317.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`Python::attach` will now panic if the Python interpreter is in the process of shutting down.

newsfragments/5317.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix FFI definition `Py_Exit` (never returns, was `()` return value, now `!`).

newsfragments/5317.removed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remove private FFI definitions `_Py_IsCoreInitialized` and `_Py_InitializeMain`

pyo3-ffi/src/cpython/pylifecycle.rs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ use crate::{PyConfig, PyPreConfig, PyStatus, Py_ssize_t};
22
use libc::wchar_t;
33
use std::ffi::{c_char, c_int};
44

5-
// "private" functions in cpython/pylifecycle.h accepted in PEP 587
65
extern "C" {
7-
// skipped _Py_SetStandardStreamEncoding;
6+
7+
// skipped Py_FrozenMain
8+
89
pub fn Py_PreInitialize(src_config: *const PyPreConfig) -> PyStatus;
910
pub fn Py_PreInitializeFromBytesArgs(
1011
src_config: *const PyPreConfig,
@@ -16,34 +17,14 @@ extern "C" {
1617
argc: Py_ssize_t,
1718
argv: *mut *mut wchar_t,
1819
) -> PyStatus;
19-
pub fn _Py_IsCoreInitialized() -> c_int;
2020

2121
pub fn Py_InitializeFromConfig(config: *const PyConfig) -> PyStatus;
22-
pub fn _Py_InitializeMain() -> PyStatus;
2322

2423
pub fn Py_RunMain() -> c_int;
2524

2625
pub fn Py_ExitStatusException(status: PyStatus) -> !;
2726

28-
// skipped _Py_RestoreSignals
29-
3027
// skipped Py_FdIsInteractive
31-
// skipped _Py_FdIsInteractive
32-
33-
// skipped _Py_SetProgramFullPath
34-
35-
// skipped _Py_gitidentifier
36-
// skipped _Py_getversion
37-
38-
// skipped _Py_IsFinalizing
39-
40-
// skipped _PyOS_URandom
41-
// skipped _PyOS_URandomNonblock
42-
43-
// skipped _Py_CoerceLegacyLocale
44-
// skipped _Py_LegacyLocaleDetected
45-
// skipped _Py_SetLocaleFromEnv
46-
4728
}
4829

4930
#[cfg(Py_3_12)]
@@ -76,14 +57,19 @@ pub const _PyInterpreterConfig_INIT: PyInterpreterConfig = PyInterpreterConfig {
7657
gil: PyInterpreterConfig_OWN_GIL,
7758
};
7859

60+
// https://github.com/python/cpython/blob/902de283a8303177eb95bf5bc252d2421fcbd758/Include/cpython/pylifecycle.h#L63-L65
61+
#[cfg(Py_3_12)]
62+
const _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS: c_int =
63+
if cfg!(Py_GIL_DISABLED) { 1 } else { 0 };
64+
7965
#[cfg(Py_3_12)]
8066
pub const _PyInterpreterConfig_LEGACY_INIT: PyInterpreterConfig = PyInterpreterConfig {
8167
use_main_obmalloc: 1,
8268
allow_fork: 1,
8369
allow_exec: 1,
8470
allow_threads: 1,
8571
allow_daemon_threads: 1,
86-
check_multi_interp_extensions: 0,
72+
check_multi_interp_extensions: _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS,
8773
gil: PyInterpreterConfig_SHARED_GIL,
8874
};
8975

@@ -96,4 +82,4 @@ extern "C" {
9682
}
9783

9884
// skipped atexit_datacallbackfunc
99-
// skipped _Py_AtExit
85+
// skipped PyUnstable_AtExit

pyo3-ffi/src/pylifecycle.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extern "C" {
1818
#[cfg_attr(PyPy, link_name = "PyPy_AtExit")]
1919
pub fn Py_AtExit(func: Option<extern "C" fn()>) -> c_int;
2020

21-
pub fn Py_Exit(arg1: c_int);
21+
pub fn Py_Exit(arg1: c_int) -> !;
2222

2323
pub fn Py_Main(argc: c_int, argv: *mut *mut wchar_t) -> c_int;
2424
pub fn Py_BytesMain(argc: c_int, argv: *mut *mut c_char) -> c_int;
@@ -89,4 +89,10 @@ type PyOS_sighandler_t = unsafe extern "C" fn(arg1: c_int);
8989
extern "C" {
9090
pub fn PyOS_getsig(arg1: c_int) -> PyOS_sighandler_t;
9191
pub fn PyOS_setsig(arg1: c_int, arg2: PyOS_sighandler_t) -> PyOS_sighandler_t;
92+
93+
#[cfg(Py_3_11)]
94+
pub static Py_Version: std::ffi::c_ulong;
95+
96+
#[cfg(Py_3_13)]
97+
pub fn Py_IsFinalizing() -> c_int;
9298
}

src/internal/state.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ impl AttachGuard {
5757

5858
crate::interpreter_lifecycle::ensure_initialized();
5959

60+
// Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable
61+
// ways, we'll make a best effort attempt here to avoid that. (There's a time of
62+
// check to time-of-use issue, but it's better than nothing.)
63+
assert!(
64+
!is_finalizing(),
65+
"Cannot attach to the Python interpreter while it is finalizing."
66+
);
67+
6068
// SAFETY: We have ensured the Python interpreter is initialized.
6169
unsafe { Self::acquire_unchecked() }
6270
}
@@ -75,8 +83,8 @@ impl AttachGuard {
7583
_ => {}
7684
}
7785

78-
// SAFETY: This API is always sound to call
79-
if unsafe { ffi::Py_IsInitialized() } == 0 {
86+
// SAFETY: These APIs are always sound to call
87+
if unsafe { ffi::Py_IsInitialized() } == 0 || is_finalizing() {
8088
// If the interpreter is not initialized, we cannot attach.
8189
return None;
8290
}
@@ -130,6 +138,18 @@ impl AttachGuard {
130138
}
131139
}
132140

141+
fn is_finalizing() -> bool {
142+
// SAFTETY: always safe to call this
143+
#[cfg(Py_3_13)]
144+
unsafe {
145+
ffi::Py_IsFinalizing() != 0
146+
}
147+
148+
// can't reliably check this before 3.13
149+
#[cfg(not(Py_3_13))]
150+
false
151+
}
152+
133153
/// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach).
134154
impl Drop for AttachGuard {
135155
fn drop(&mut self) {

src/marker.rs

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ impl Python<'_> {
392392
///
393393
/// - If the [`auto-initialize`] feature is not enabled and the Python interpreter is not
394394
/// initialized.
395+
/// - If the Python interpreter is in the process of [shutting down].
395396
///
396397
/// # Examples
397398
///
@@ -409,6 +410,7 @@ impl Python<'_> {
409410
/// ```
410411
///
411412
/// [`auto-initialize`]: https://pyo3.rs/main/features.html#auto-initialize
413+
/// [shutting down]: https://docs.python.org/3/glossary.html#term-interpreter-shutdown
412414
#[inline]
413415
#[track_caller]
414416
pub fn attach<F, R>(f: F) -> R
@@ -422,6 +424,7 @@ impl Python<'_> {
422424
/// Variant of [`Python::attach`] which will do no work if the interpreter is in a
423425
/// state where it cannot be attached to:
424426
/// - in the middle of GC traversal
427+
/// - in the process of shutting down
425428
/// - not initialized
426429
#[inline]
427430
#[track_caller]
@@ -464,27 +467,13 @@ impl Python<'_> {
464467

465468
/// Like [`Python::attach`] except Python interpreter state checking is skipped.
466469
///
467-
/// Normally when the GIL is acquired, we check that the Python interpreter is an
468-
/// appropriate state (e.g. it is fully initialized). This function skips those
469-
/// checks.
470+
/// Normally when the GIL is acquired, PyO3 checks that the Python interpreter is
471+
/// in an appropriate state (e.g. it is fully initialized). This function skips
472+
/// those checks.
470473
///
471474
/// # Safety
472475
///
473476
/// If [`Python::attach`] would succeed, it is safe to call this function.
474-
///
475-
/// In most cases, you should use [`Python::attach`].
476-
///
477-
/// A justified scenario for calling this function is during multi-phase interpreter
478-
/// initialization when [`Python::attach`] would fail before
479-
// this link is only valid on 3.8+not pypy and up.
480-
#[cfg_attr(
481-
all(Py_3_8, not(PyPy)),
482-
doc = "[`_Py_InitializeMain`](crate::ffi::_Py_InitializeMain)"
483-
)]
484-
#[cfg_attr(any(not(Py_3_8), PyPy), doc = "`_Py_InitializeMain`")]
485-
/// is called because the interpreter is only partially initialized.
486-
///
487-
/// Behavior in other scenarios is not documented.
488477
#[inline]
489478
#[track_caller]
490479
pub unsafe fn with_gil_unchecked<F, R>(f: F) -> R

0 commit comments

Comments
 (0)