Skip to content

Commit 074daf0

Browse files
committed
Add a test reproducing the #5827 crash
Signed-off-by: Rostan Tabet <[email protected]>
1 parent 852a4b5 commit 074daf0

File tree

2 files changed

+53
-0
lines changed

2 files changed

+53
-0
lines changed

tests/test_thread.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
#include <chrono>
1616
#include <thread>
1717

18+
#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include(<barrier>)
19+
# define PYBIND11_HAS_BARRIER 1
20+
# include <barrier>
21+
#endif
22+
1823
namespace py = pybind11;
1924

2025
namespace {
@@ -34,6 +39,9 @@ EmptyStruct SharedInstance;
3439
} // namespace
3540

3641
TEST_SUBMODULE(thread, m) {
42+
#ifdef Py_GIL_DISABLED
43+
PyUnstable_Module_SetGIL(m.ptr(), Py_MOD_GIL_NOT_USED);
44+
#endif
3745

3846
py::class_<IntStruct>(m, "IntStruct").def(py::init([](const int i) { return IntStruct(i); }));
3947

@@ -67,6 +75,39 @@ TEST_SUBMODULE(thread, m) {
6775
py::class_<EmptyStruct>(m, "EmptyStruct")
6876
.def_readonly_static("SharedInstance", &SharedInstance);
6977

78+
#if defined(PYBIND11_HAS_BARRIER)
79+
// In the free-threaded build, during PyThreadState_Clear, removing the thread from the biased
80+
// reference counting table may call destructors. Make sure that it doesn't crash.
81+
m.def("test_pythread_state_clear_destructor", [](py::type cls) {
82+
py::handle obj;
83+
84+
std::barrier barrier{2};
85+
std::thread thread1{[&]() {
86+
py::gil_scoped_acquire gil;
87+
obj = cls().release();
88+
barrier.arrive_and_wait();
89+
}};
90+
std::thread thread2{[&]() {
91+
py::gil_scoped_acquire gil;
92+
barrier.arrive_and_wait();
93+
// ob_ref_shared becomes negative; transition to the queued state
94+
obj.dec_ref();
95+
}};
96+
97+
// jthread is not supported by Apple Clang
98+
thread1.join();
99+
thread2.join();
100+
});
101+
#endif
102+
103+
m.attr("has_barrier") =
104+
#ifdef PYBIND11_HAS_BARRIER
105+
true;
106+
#else
107+
false;
108+
#endif
109+
m.def("acquire_gil", []() { py::gil_scoped_acquire gil_acquired; });
110+
70111
// NOTE: std::string_view also uses loader_life_support to ensure that
71112
// the string contents remain alive, but that's a C++ 17 feature.
72113
}

tests/test_thread.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77

8+
import env
89
from pybind11_tests import thread as m
910

1011

@@ -66,3 +67,14 @@ def access_shared_instance():
6667
thread.start()
6768
for thread in threads:
6869
thread.join()
70+
71+
72+
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
73+
@pytest.mark.skipif(not m.has_barrier, reason="no <barrier>")
74+
@pytest.mark.skipif(env.sys_is_gil_enabled(), reason="Deadlock with the GIL")
75+
def test_pythread_state_clear_destructor():
76+
class Foo:
77+
def __del__(self):
78+
m.acquire_gil()
79+
80+
m.test_pythread_state_clear_destructor(Foo)

0 commit comments

Comments
 (0)