Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions include/pybind11/detail/internals-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ PYBIND11_INLINE void translate_local_exception(std::exception_ptr p) {

PYBIND11_INLINE void check_internals_local_exception_translator(internals *internals_ptr) {
if (internals_ptr) {
# 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
for (auto et : internals_ptr->registered_exception_translators) {
if (et == &translate_local_exception) {
return;
Expand Down
3 changes: 2 additions & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions tests/mod_concurrent_import.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) 2026 The pybind Community.

#include <pybind11/pybind11.h>

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()) {}
4 changes: 4 additions & 0 deletions tests/test_exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(); });
}
30 changes: 30 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import importlib
import re
import sys
import threading

import pytest
from custom_exceptions import PythonMyException7
Expand Down Expand Up @@ -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("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++).
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]
Loading