fix: only cache the internals pp after a successful lookup (#6190)
* fix: only cache the internals pp after a successful lookup
In get_pp(), last_istate_tls was set before get_or_create_pp_in_state_dict().
If that call threw, the thread cache matched the interpreter but held a null
pp, so later calls returned nullptr ("get_internals: get_pp() returned
nullptr"). Seen intermittently on MinGW in
test_import_in_subinterpreter_concurrently.
Assisted-by: ClaudeCode:claude-opus-5-5
* test: cover internals cache retries after failed lookup
---------
Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h
index 164da39..d755b28 100644
--- a/include/pybind11/detail/internals.h
+++ b/include/pybind11/detail/internals.h
@@ -650,8 +650,11 @@
if (!tstate) {
tstate = get_thread_state_unchecked();
}
+ // Update the cache only on success; a stale interp with a null pp would make
+ // later calls return nullptr.
+ auto *pp = get_or_create_pp_in_state_dict();
last_istate_tls() = tstate->interp;
- internals_p_tls() = get_or_create_pp_in_state_dict();
+ internals_p_tls() = pp;
}
return internals_p_tls();
}
diff --git a/tests/test_with_catch/test_subinterpreter.cpp b/tests/test_with_catch/test_subinterpreter.cpp
index 570519e..52b18aa 100644
--- a/tests/test_with_catch/test_subinterpreter.cpp
+++ b/tests/test_with_catch/test_subinterpreter.cpp
@@ -13,6 +13,8 @@
# include <cstdlib>
# include <fstream>
# include <functional>
+# include <memory>
+# include <new>
# include <thread>
# include <utility>
@@ -42,6 +44,53 @@
py::detail::get_local_internals();
}
+TEST_CASE("Internals cache retries after a failed lookup") {
+ struct test_internals {
+ bool fail_next_fetch = true;
+ };
+ using manager_type = py::detail::internals_pp_manager<test_internals>;
+ constexpr const char *key = "_pybind11_test_internals_cache_lookup_retry";
+ auto &manager = manager_type::get_instance(key, [](test_internals *internals) {
+ if (internals && internals->fail_next_fetch) {
+ internals->fail_next_fetch = false;
+ throw std::bad_alloc();
+ }
+ });
+ struct reset_guard {
+ manager_type &manager;
+ ~reset_guard() {
+ manager.unref();
+ unsafe_reset_internals_for_single_interpreter();
+ }
+ } reset{manager};
+
+ // Creating a subinterpreter enables the per-thread interpreter cache.
+ auto sub = py::subinterpreter::create();
+ manager.unref();
+
+ auto check_failed_lookup = [&]() {
+ py::subinterpreter_scoped_activate activate(sub);
+ // Prepopulate the capsule so that the lookup invokes on_fetch instead of creating it.
+ auto *expected_pp
+ = py::detail::atomic_get_or_create_in_state_dict<std::unique_ptr<test_internals>>(key)
+ .first;
+ expected_pp->reset(new test_internals());
+ REQUIRE_THROWS_AS(manager.get_pp(), std::bad_alloc);
+
+ // A failed lookup must neither cache nullptr nor retain another interpreter's pointer.
+ REQUIRE(manager.get_pp() == expected_pp);
+ };
+
+ SECTION("Initially empty cache") { check_failed_lookup(); }
+ SECTION("Cached pointer from another interpreter") {
+ auto *main_pp
+ = py::detail::atomic_get_or_create_in_state_dict<std::unique_ptr<test_internals>>(key)
+ .first;
+ REQUIRE(manager.get_pp() == main_pp);
+ check_failed_lookup();
+ }
+}
+
py::object &get_dict_type_object() {
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage;
return storage