Fix crash in gil_scoped_acquire (#5828)

* Add a test reproducing the #5827 crash

Signed-off-by: Rostan Tabet <rtabet@nvidia.com>

* Fix #5827

Signed-off-by: Rostan Tabet <rtabet@nvidia.com>

* Rename PYBIND11_HAS_BARRIER and move it to common.h

Signed-off-by: Rostan Tabet <rtabet@nvidia.com>

* In test_thread.{cpp,py}, rename has_barrier

Signed-off-by: Rostan Tabet <rtabet@nvidia.com>

---------

Signed-off-by: Rostan Tabet <rtabet@nvidia.com>
This commit is contained in:
Rostan
2025-11-14 01:29:02 +01:00
committed by GitHub
parent b30e72c6f6
commit 8ecf10e8cc
5 changed files with 60 additions and 5 deletions

View File

@@ -7,8 +7,7 @@
#include <thread>
#include <utility>
#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include(<barrier>)
# define PYBIND11_HAS_BARRIER 1
#if defined(PYBIND11_HAS_STD_BARRIER)
# include <barrier>
#endif
@@ -39,7 +38,7 @@ private:
std::atomic_bool value_{false};
};
#if defined(PYBIND11_HAS_BARRIER)
#if defined(PYBIND11_HAS_STD_BARRIER)
// Modifying the C/C++ members of a Python object from multiple threads requires a critical section
// to ensure thread safety and data integrity.
@@ -259,7 +258,7 @@ TEST_SUBMODULE(scoped_critical_section, m) {
(void) BoolWrapperHandle.ptr(); // suppress unused variable warning
m.attr("has_barrier") =
#ifdef PYBIND11_HAS_BARRIER
#ifdef PYBIND11_HAS_STD_BARRIER
true;
#else
false;

View File

@@ -15,6 +15,10 @@
#include <chrono>
#include <thread>
#if defined(PYBIND11_HAS_STD_BARRIER)
# include <barrier>
#endif
namespace py = pybind11;
namespace {
@@ -34,7 +38,6 @@ EmptyStruct SharedInstance;
} // namespace
TEST_SUBMODULE(thread, m) {
py::class_<IntStruct>(m, "IntStruct").def(py::init([](const int i) { return IntStruct(i); }));
// implicitly_convertible uses loader_life_support when an implicit
@@ -67,6 +70,39 @@ TEST_SUBMODULE(thread, m) {
py::class_<EmptyStruct>(m, "EmptyStruct")
.def_readonly_static("SharedInstance", &SharedInstance);
#if defined(PYBIND11_HAS_STD_BARRIER)
// In the free-threaded build, during PyThreadState_Clear, removing the thread from the biased
// reference counting table may call destructors. Make sure that it doesn't crash.
m.def("test_pythread_state_clear_destructor", [](py::type cls) {
py::handle obj;
std::barrier barrier{2};
std::thread thread1{[&]() {
py::gil_scoped_acquire gil;
obj = cls().release();
barrier.arrive_and_wait();
}};
std::thread thread2{[&]() {
py::gil_scoped_acquire gil;
barrier.arrive_and_wait();
// ob_ref_shared becomes negative; transition to the queued state
obj.dec_ref();
}};
// jthread is not supported by Apple Clang
thread1.join();
thread2.join();
});
#endif
m.attr("defined_PYBIND11_HAS_STD_BARRIER") =
#ifdef PYBIND11_HAS_STD_BARRIER
true;
#else
false;
#endif
m.def("acquire_gil", []() { py::gil_scoped_acquire gil_acquired; });
// NOTE: std::string_view also uses loader_life_support to ensure that
// the string contents remain alive, but that's a C++ 17 feature.
}

View File

@@ -5,6 +5,7 @@ import threading
import pytest
import env
from pybind11_tests import thread as m
@@ -66,3 +67,14 @@ def test_bind_shared_instance():
thread.start()
for thread in threads:
thread.join()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
@pytest.mark.skipif(not m.defined_PYBIND11_HAS_STD_BARRIER, reason="no <barrier>")
@pytest.mark.skipif(env.sys_is_gil_enabled(), reason="Deadlock with the GIL")
def test_pythread_state_clear_destructor():
class Foo:
def __del__(self):
m.acquire_gil()
m.test_pythread_state_clear_destructor(Foo)