Skip to content

Commit 8ecf10e

Browse files
authored
Fix crash in gil_scoped_acquire (#5828)
* Add a test reproducing the #5827 crash Signed-off-by: Rostan Tabet <[email protected]> * Fix #5827 Signed-off-by: Rostan Tabet <[email protected]> * Rename PYBIND11_HAS_BARRIER and move it to common.h Signed-off-by: Rostan Tabet <[email protected]> * In test_thread.{cpp,py}, rename has_barrier Signed-off-by: Rostan Tabet <[email protected]> --------- Signed-off-by: Rostan Tabet <[email protected]>
1 parent b30e72c commit 8ecf10e

File tree

5 files changed

+60
-5
lines changed

5 files changed

+60
-5
lines changed

include/pybind11/detail/common.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
# define PYBIND11_DTOR_CONSTEXPR
104104
#endif
105105

106+
#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include(<barrier>)
107+
# define PYBIND11_HAS_STD_BARRIER 1
108+
#endif
109+
106110
// Compiler version assertions
107111
#if defined(__INTEL_COMPILER)
108112
# if __INTEL_COMPILER < 1800

include/pybind11/gil.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ class gil_scoped_acquire {
120120
pybind11_fail("scoped_acquire::dec_ref(): internal error!");
121121
}
122122
# endif
123+
// Make sure that PyThreadState_Clear is not recursively called by finalizers.
124+
// See issue #5827
125+
++tstate->gilstate_counter;
123126
PyThreadState_Clear(tstate);
127+
--tstate->gilstate_counter;
124128
if (active) {
125129
PyThreadState_DeleteCurrent();
126130
}

tests/test_scoped_critical_section.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
#include <thread>
88
#include <utility>
99

10-
#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include(<barrier>)
11-
# define PYBIND11_HAS_BARRIER 1
10+
#if defined(PYBIND11_HAS_STD_BARRIER)
1211
# include <barrier>
1312
#endif
1413

@@ -39,7 +38,7 @@ class BoolWrapper {
3938
std::atomic_bool value_{false};
4039
};
4140

42-
#if defined(PYBIND11_HAS_BARRIER)
41+
#if defined(PYBIND11_HAS_STD_BARRIER)
4342

4443
// Modifying the C/C++ members of a Python object from multiple threads requires a critical section
4544
// to ensure thread safety and data integrity.
@@ -259,7 +258,7 @@ TEST_SUBMODULE(scoped_critical_section, m) {
259258
(void) BoolWrapperHandle.ptr(); // suppress unused variable warning
260259

261260
m.attr("has_barrier") =
262-
#ifdef PYBIND11_HAS_BARRIER
261+
#ifdef PYBIND11_HAS_STD_BARRIER
263262
true;
264263
#else
265264
false;

tests/test_thread.cpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
#include <chrono>
1616
#include <thread>
1717

18+
#if defined(PYBIND11_HAS_STD_BARRIER)
19+
# include <barrier>
20+
#endif
21+
1822
namespace py = pybind11;
1923

2024
namespace {
@@ -34,7 +38,6 @@ EmptyStruct SharedInstance;
3438
} // namespace
3539

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

4043
// implicitly_convertible uses loader_life_support when an implicit
@@ -67,6 +70,39 @@ TEST_SUBMODULE(thread, m) {
6770
py::class_<EmptyStruct>(m, "EmptyStruct")
6871
.def_readonly_static("SharedInstance", &SharedInstance);
6972

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

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.defined_PYBIND11_HAS_STD_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)