diff --git a/pyo3-ffi/src/acquire_gil.c b/pyo3-ffi/src/acquire_gil.c index 52732694828..3becd0bc70b 100644 --- a/pyo3-ffi/src/acquire_gil.c +++ b/pyo3-ffi/src/acquire_gil.c @@ -1,6 +1,4 @@ #if defined(_WIN32) -#include -#include #else #include #include @@ -19,18 +17,9 @@ int gil_func_name(void); #if defined(_WIN32) int wrapped_func_name(void) { - // Do the equivalent of https://github.com/python/cpython/issues/87135 (included - // in Python 3.14) to avoid pthread_exit unwinding the current thread, which tends - // to cause undefined behavior in Rust. - // - // Unfortunately, I don't know of a way to do a catch(...) from Rust. - __try { - return gil_func_name(); - } __catch(void) { - while(1) { - SleepEx(INFINITE, TRUE); - } - } + // In MSVC, PyThread_exit_thread calls _endthreadex(0), which does not use SEH. This can + // cause Rust-level UB if there is pinned memory, but AFAICT there's not much we can do about it. + return gil_func_name(); } #else static void hang_thread(void *ignore) { diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index e44d1aa0ecf..2fa43bea971 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -9,6 +9,25 @@ fn issue_219() { Python::with_gil(|_| {}); } +#[pyclass] +struct LockHolder { + #[allow(unused)] + sender: std::sync::mpsc::Sender<()>, +} + +// This will hammer the GIL once the retyrbed LockHolder is dropped. +#[pyfunction] +fn hammer_gil_in_thread() -> PyResult { + let (sender, receiver) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + receiver.recv().ok(); + loop { + Python::with_gil(|_py| ()); + } + }); + Ok(LockHolder { sender }) +} + #[pyfunction] fn get_type_fully_qualified_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { obj.get_type().fully_qualified_name() @@ -35,6 +54,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny> #[pymodule(gil_used = false)] pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; + m.add_function(wrap_pyfunction!(hammer_gil_in_thread, m)?)?; m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?; m.add_function(wrap_pyfunction!(accepts_bool, m)?)?; m.add_function(wrap_pyfunction!(get_item_and_run_callback, m)?)?; diff --git a/pytests/tests/test_hammer_gil_in_thread.py b/pytests/tests/test_hammer_gil_in_thread.py new file mode 100644 index 00000000000..cee1bb6b828 --- /dev/null +++ b/pytests/tests/test_hammer_gil_in_thread.py @@ -0,0 +1,20 @@ +from pyo3_pytests import misc + + +def make_loop(): + # create a reference loop that will only be destroyed when the GC is called at the end + # of execution + start = [] + cur = [start] + for _ in range(1000 * 1000 * 10): + cur = [cur] + start.append(cur) + return start + + +# set a bomb that will explode when modules are cleaned up +loopy = [make_loop()] + + +def test_hammer_gil(): + loopy.append(misc.hammer_gil_in_thread())