Skip to content

Commit d9cf322

Browse files
committed
Add a test reproducing the #5827 crash
Signed-off-by: Rostan Tabet <rtabet@nvidia.com>
1 parent 852a4b5 commit d9cf322

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

tests/test_thread.cpp

Lines changed: 38 additions & 1 deletion
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,7 +39,6 @@ EmptyStruct SharedInstance;
3439
} // namespace
3540

3641
TEST_SUBMODULE(thread, m) {
37-
3842
py::class_<IntStruct>(m, "IntStruct").def(py::init([](const int i) { return IntStruct(i); }));
3943

4044
// implicitly_convertible uses loader_life_support when an implicit
@@ -67,6 +71,39 @@ TEST_SUBMODULE(thread, m) {
6771
py::class_<EmptyStruct>(m, "EmptyStruct")
6872
.def_readonly_static("SharedInstance", &SharedInstance);
6973

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

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)