fix: re-acquire the GIL in class_::init_instance before instance registration (#6172)
* fix: re-acquire GIL in init_instance for thread-safe instance registration
With a factory-based py::init combined with py::call_guard<py::gil_scoped_release>,
init_instance -> register_instance runs while the GIL is released, racing on
internals.registered_instances with GIL-holding threads. This corrupts the
instance map and leads to 'pybind11_object_dealloc(): Tried to deallocate
unregistered instance!' -> std::terminate. Acquire the GIL (no-op if already
held) in both class_::init_instance overloads; free-threaded builds keep using
their sharded mutex and are unaffected.
* tests: avoid std::make_unique in test_gil_scoped (C++14 only)
The iOS/Android wheel CI jobs build the test suite with -std=gnu++11.
* tests: skip test_init_factory_gil_released_concurrent_construction on free-threaded builds
On free-threaded builds py::gil_scoped_release detaches the thread state and the
constructor machinery is not safe when called detached, so the test segfaults
(pre-existing limitation of call_guard<gil_scoped_release>, unrelated to the
init_instance fix). The instance map is mutex-protected on free-threaded builds
anyway, so there is nothing to test there. An early return inside the function
covers the _run_in_process parametrizations, where pytest skip markers do not
apply.
* tests: run the init_instance race regression only as a direct test
The test was in ALL_BASIC_TESTS, so it also ran in the _run_in_process
parametrizations, whose subprocesses impose a 10s timeout; on Windows
(sequential variant) the extra sleep timer granularity and GIL handoff
overhead pushed that over the limit. It is a data-race regression, not a
deadlock check, so define it after ALL_BASIC_TESTS like the
test_run_in_process_* functions and drop the now-unneeded free-threaded
early return.
* chore: retrigger CI (GCC 9 job failed on apt 404 for python3-setuptools, unrelated)
* fix: make init_instance GIL acquisition unconditional
The `#if !defined(Py_GIL_DISABLED)` guards skipped the acquire on free-threaded builds, where `gil_scoped_release` detaches the thread state. `init_instance` then crashes in `PyCriticalSection_BeginMutex` (via `get_type_info`) even single-threaded, because the critical section requires an attached thread state.
`gil_scoped_acquire` attaches the thread state without taking a global lock, so this is safe on free-threaded builds and does not serialize threads.
* test: run init_instance regression on free-threaded builds too
The acquire in init_instance is now unconditional, so the regression test also covers the free-threaded detached-thread-state crash. Drop the PY_GIL_DISABLED skip.
* docs: cover free-threaded thread-state attach in init_instance comments
The unconditional gil_scoped_acquire is also needed on free-threaded builds, where gil_scoped_release detaches the thread state and get_type_info requires it to be attached. Mention this in the init_instance comment and the regression-test docstring.
3 files changed