You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The scheduled master CI intermittently reached a state that internals_pp_manager::get_pp() documents as impossible while importing a pybind11 module concurrently in multiple Python subinterpreters on Windows:
ImportError: get_internals: get_pp() returned nullptr
The failure occurred once during the existing stress test and the complete job passed when rerun. This issue records the expiring CI evidence and the relevant implementation history so the race can be investigated later; it is not yet a proven root cause or proposed fix.
The failed job otherwise reported 1,295 passed tests, 32 skipped tests, and this single failure. The rerun on 2026-09-12 used the same commit, OS image, Python, compiler, configuration, and test code; it passed the Python tests and the rest of the job. That makes this look scheduling-sensitive rather than environment-version-sensitive.
Environment preserved from the log
GitHub Actions windows-latest (Windows Server 2025, 10.0.26100)
Runner image: windows-2025-vs2026, version 20260907.229.1
The test is already a meaningful stress test, rather than a single opportunistic import:
check_script_success_in_subprocess() runs its subprocess eight times by default.
Each subprocess creates an InterpreterPoolExecutor(max_workers=16).
It submits 32 calls which concurrently import mod_per_interpreter_gil_with_singleton and inspect objects registered in its singleton. The module is explicitly declared with py::multiple_interpreters::per_interpreter_gil().
The pytest locals showed the helper loop variable as _ = 3, so three complete subprocess repetitions succeeded and the fourth failed.
Here test() imports mod_per_interpreter_gil_with_singleton; the import failed in one worker with the diagnostic above. A useful initial reproduction approach is therefore to build the normal test modules on 64-bit Windows with CPython 3.14 and repeatedly run only:
Increasing the helper's rerun value provides a straightforward way to raise the stress level without changing the concurrency pattern that produced the failure.
Why this is significant
At the failing commit, get_pp() is explicitly documented with "Will never return nullptr":
The null guard was added in #6018 as diagnostic hardening motivated by the unexplained Windows crashes in #5993. It turned this occurrence into a useful ImportError; without the guard, dereferencing the null pointer could instead have crashed. The context in #5993 was different, so this issue should not assume the two problems have the same cause.
This also overlaps directly with #5947, "Fix concurrency consistency for internals_pp_manager under multiple-interpreters." During development of that PR, commit 4ef8b0c disabled the manager's caches and always retrieved the state-dictionary entry. That experiment was reverted in 49952a8 before the final solution was merged. Repeating that experiment is a useful diagnostic comparison, although it is not necessarily the final design.
Initial code analysis and a leading hypothesis
The following is a plausible race to investigate, not a demonstrated root cause.
Before has_seen_non_main_interpreter() becomes true, get_pp() and unref() both use the shared, non-atomic internals_singleton_pp_ member. With independent interpreter GILs, two module-initialization threads may execute those paths concurrently. The one-way mode transition is not visibly synchronized with access to that raw shared member. One possible interleaving is:
Thread A observes has_seen_non_main_interpreter() == false in get_pp() and prepares to return internals_singleton_pp_.
A non-main-interpreter thread B enters ensure_internals(), still observes the flag as false in unref(), and clears internals_singleton_pp_.
Thread A returns the now-null shared member.
Publishing has_seen_non_main_interpreter() = true before calling unref() in a non-main interpreter would cause that unref() to clear only the thread-local cache instead of the shared singleton. That is a focused first experiment, but it needs Windows stress testing and an audit of initialization/finalization behavior before being considered a fix.
A second path worth auditing is the thread-local cache update in get_pp():
If the second operation can throw after the interpreter identity has been cached, a later call in the same interpreter could skip cache initialization and return a null internals_p_tls(). The observed log did not show an earlier underlying exception, so this is only another invariant to check.
Suggested investigation sequence
Reproduce with the existing focused test on the recorded Windows/Python environment and a larger subprocess repetition count.
Test publishing has_seen_non_main_interpreter() before unref() as a narrowly targeted experiment.
Add temporary tracing or assertions around the flag, current interpreter, last_istate_tls(), internals_p_tls(), and internals_singleton_pp_ if reproduction remains intermittent.
Keep the existing test strict. Its musllinux xfail covers a different known failure and should not be generalized to Windows.
Regression status is unknown: this was observed once on current master, and the exact rerun passed.
Summary
The scheduled
masterCI intermittently reached a state thatinternals_pp_manager::get_pp()documents as impossible while importing a pybind11 module concurrently in multiple Python subinterpreters on Windows:The failure occurred once during the existing stress test and the complete job passed when rerun. This issue records the expiring CI evidence and the relevant implementation history so the race can be investigated later; it is not yet a proven root cause or proposed fix.
CI evidence (workflow logs will expire)
tests/test_multiple_interpreters.py::test_import_in_subinterpreter_concurrentlyThe failed job otherwise reported 1,295 passed tests, 32 skipped tests, and this single failure. The rerun on 2026-09-12 used the same commit, OS image, Python, compiler, configuration, and test code; it passed the Python tests and the rest of the job. That makes this look scheduling-sensitive rather than environment-version-sensitive.
Environment preserved from the log
windows-latest(Windows Server 2025, 10.0.26100)windows-2025-vs2026, version20260907.229.1PYBIND11_INTERNALS_VERSION=12PYBIND11_SIMPLE_GIL_MANAGEMENTdisabledDurable reproduction information
The exact test and helper at the failing commit are permanently available here:
test_import_in_subinterpreter_concurrentlycheck_script_success_in_subprocessmod_per_interpreter_gil_with_singletonThe test is already a meaningful stress test, rather than a single opportunistic import:
check_script_success_in_subprocess()runs its subprocess eight times by default.InterpreterPoolExecutor(max_workers=16).mod_per_interpreter_gil_with_singletonand inspect objects registered in its singleton. The module is explicitly declared withpy::multiple_interpreters::per_interpreter_gil()._ = 3, so three complete subprocess repetitions succeeded and the fourth failed.The core of each subprocess is:
Here
test()importsmod_per_interpreter_gil_with_singleton; the import failed in one worker with the diagnostic above. A useful initial reproduction approach is therefore to build the normal test modules on 64-bit Windows with CPython 3.14 and repeatedly run only:Increasing the helper's
rerunvalue provides a straightforward way to raise the stress level without changing the concurrency pattern that produced the failure.Why this is significant
At the failing commit,
get_pp()is explicitly documented with "Will never return nullptr":internals_pp_manager::get_pp()The null guard was added in #6018 as diagnostic hardening motivated by the unexplained Windows crashes in #5993. It turned this occurrence into a useful
ImportError; without the guard, dereferencing the null pointer could instead have crashed. The context in #5993 was different, so this issue should not assume the two problems have the same cause.This also overlaps directly with #5947, "Fix concurrency consistency for
internals_pp_managerunder multiple-interpreters." During development of that PR, commit 4ef8b0c disabled the manager's caches and always retrieved the state-dictionary entry. That experiment was reverted in 49952a8 before the final solution was merged. Repeating that experiment is a useful diagnostic comparison, although it is not necessarily the final design.Initial code analysis and a leading hypothesis
The following is a plausible race to investigate, not a demonstrated root cause.
At the failing commit,
ensure_internals()does this in order:Before
has_seen_non_main_interpreter()becomes true,get_pp()andunref()both use the shared, non-atomicinternals_singleton_pp_member. With independent interpreter GILs, two module-initialization threads may execute those paths concurrently. The one-way mode transition is not visibly synchronized with access to that raw shared member. One possible interleaving is:has_seen_non_main_interpreter() == falseinget_pp()and prepares to returninternals_singleton_pp_.ensure_internals(), still observes the flag as false inunref(), and clearsinternals_singleton_pp_.Publishing
has_seen_non_main_interpreter() = truebefore callingunref()in a non-main interpreter would cause thatunref()to clear only the thread-local cache instead of the shared singleton. That is a focused first experiment, but it needs Windows stress testing and an audit of initialization/finalization behavior before being considered a fix.A second path worth auditing is the thread-local cache update in
get_pp():If the second operation can throw after the interpreter identity has been cached, a later call in the same interpreter could skip cache initialization and return a null
internals_p_tls(). The observed log did not show an earlier underlying exception, so this is only another invariant to check.Suggested investigation sequence
has_seen_non_main_interpreter()beforeunref()as a narrowly targeted experiment.internals_pp_managerunder multiple-interpreters #5947's always-read-the-state-dictionary implementation to determine whether the singleton/TLS cache transition is essential to the failure.last_istate_tls(),internals_p_tls(), andinternals_singleton_pp_if reproduction remains intermittent.Regression status is unknown: this was observed once on current
master, and the exact rerun passed.