From 5b161af2149bec2f11ba52f9c3365ec808581e61 Mon Sep 17 00:00:00 2001 From: Yuka Sadaoka Date: Thu, 22 Oct 2026 21:07:27 +0000 Subject: [PATCH 1/3] Initial commit for fixing a data race in the local exception translator --- include/pybind11/detail/internals-inl.h | 4 ++++ tests/CMakeLists.txt | 3 ++- tests/mod_concurrent_import.cpp | 10 +++++++++ tests/test_exceptions.cpp | 4 ++++ tests/test_exceptions.py | 30 +++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/mod_concurrent_import.cpp diff --git a/include/pybind11/detail/internals-inl.h b/include/pybind11/detail/internals-inl.h index a7d6450ee7..b4416c4583 100644 --- a/include/pybind11/detail/internals-inl.h +++ b/include/pybind11/detail/internals-inl.h @@ -143,6 +143,10 @@ PYBIND11_INLINE void translate_local_exception(std::exception_ptr p) { PYBIND11_INLINE void check_internals_local_exception_translator(internals *internals_ptr) { if (internals_ptr) { +# ifdef Py_GIL_DISABLED + // Not with_exception_translators(): that calls get_internals(), which runs this hook. + pycritical_section lock(internals_ptr->exception_translator_mutex); +# endif for (auto et : internals_ptr->registered_exception_translators) { if (et == &translate_local_exception) { return; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c23e7668e1..4cbc364e79 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -257,7 +257,8 @@ tests_extra_targets( "pybind11_cross_module_tests") # And add additional targets for other tests. -tests_extra_targets("test_exceptions.py" "cross_module_interleaved_error_already_set") +tests_extra_targets("test_exceptions.py" + "cross_module_interleaved_error_already_set;mod_concurrent_import") tests_extra_targets("test_gil_scoped.py" "cross_module_gil_utils") tests_extra_targets("test_cpp_conduit.py" "exo_planet_pybind11;exo_planet_c_api;home_planet_very_lonely_traveler") diff --git a/tests/mod_concurrent_import.cpp b/tests/mod_concurrent_import.cpp new file mode 100644 index 0000000000..327fff4e76 --- /dev/null +++ b/tests/mod_concurrent_import.cpp @@ -0,0 +1,10 @@ +// Copyright (c) 2026 The pybind Community. + +#include + +namespace py = pybind11; + +// test_exceptions.py needs a module whose *first* import happens inside a test, so nothing else +// may import this one. + +PYBIND11_MODULE(mod_concurrent_import, , py::mod_gil_not_used()) {} diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 0a970065b4..b313b8a86d 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -424,4 +424,8 @@ TEST_SUBMODULE(exceptions, m) { PyErr_SetObject(PyExc_Exception, exc_inst.ptr()); } }); + + // The on-fetch hook (check_internals_local_exception_translator) only runs when a module is + // imported; test_local_translator_registration_vs_internals_fetch needs to keep running it. + m.def("ensure_internals", []() { py::detail::ensure_internals(); }); } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b0135d050e..3a65240d9b 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,7 +1,9 @@ from __future__ import annotations +import importlib import re import sys +import threading import pytest from custom_exceptions import PythonMyException7 @@ -442,3 +444,31 @@ def test_return_exception_void(): with pytest.raises(TypeError) as excinfo: m.return_exception_void() assert "Exception" in str(excinfo.value) + + +@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") +def test_local_translator_registration_vs_internals_fetch(): + # Nothing to assert: the race only shows up under TSan, and only with libc++ or + # MSVC (the hook isn't built with libstdc++). + if "mod_concurrent_import" in sys.modules: + pytest.skip("The first import can only be exercised once per process") + + stop = threading.Event() + errors = [] + + def ensure_internals_loop(): + try: + while not stop.is_set(): + m.ensure_internals() + except Exception as e: + errors.append(e) + + reader = threading.Thread(target=ensure_internals_loop) + reader.start() + try: + importlib.import_module("mod_concurrent_import") + finally: + stop.set() + reader.join() + if errors: + raise errors[0] From 6e6bda70b12379505f549d564ad8631f6f94a8cd Mon Sep 17 00:00:00 2001 From: Yuka Sadaoka Date: Fri, 23 Oct 2026 11:12:56 +0000 Subject: [PATCH 2/3] only run the translator race test on free-threaded builds --- tests/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 3a65240d9b..ec4f8e4e1d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -446,7 +446,7 @@ def test_return_exception_void(): assert "Exception" in str(excinfo.value) -@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") +@pytest.mark.skipif("not env.PY_GIL_DISABLED", reason="Only relevant without the GIL") def test_local_translator_registration_vs_internals_fetch(): # Nothing to assert: the race only shows up under TSan, and only with libc++ or # MSVC (the hook isn't built with libstdc++). From a19ef4f4ea586312e3e90e219513088923512b02 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 23 Oct 2026 11:03:41 -0700 Subject: [PATCH 3/3] fix: guard translator locking on older free-threaded Python --- include/pybind11/detail/internals-inl.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/pybind11/detail/internals-inl.h b/include/pybind11/detail/internals-inl.h index b4416c4583..586f78ad50 100644 --- a/include/pybind11/detail/internals-inl.h +++ b/include/pybind11/detail/internals-inl.h @@ -143,7 +143,8 @@ PYBIND11_INLINE void translate_local_exception(std::exception_ptr p) { PYBIND11_INLINE void check_internals_local_exception_translator(internals *internals_ptr) { if (internals_ptr) { -# ifdef Py_GIL_DISABLED +# if defined(Py_GIL_DISABLED) && PY_VERSION_HEX >= 0x030E00C1 + // The pre-3.14rc1 fallback mutex can deadlock on reentrant imports. // Not with_exception_translators(): that calls get_internals(), which runs this hook. pycritical_section lock(internals_ptr->exception_translator_mutex); # endif