Skip to content

Commit 5dde1d1

Browse files
Vincent Palanchernyibbang
authored andcommitted
qi.pyguard: Fixes segfault at Python interpreter finalization due to GIL bad manipulation. #47167
A segfault occurs during the finalization of the Python interpreter when an object has a destructor that uses `GILRelease`. The constructor of `GILRelease` effectively releases the GIL but the destructor disables its acquisition because the interpreter is finalizing. When the call returns to the interpreter, the GIL stays released and this leads to crashes or errors when the interpreter tries to make Python calls. This patch rewrites the `GILRelease` type so that during finalization, the object does nothing (therefore the GIL is not released).
1 parent 228d3ef commit 5dde1d1

3 files changed

Lines changed: 85 additions & 32 deletions

File tree

cmake/add_tests.cmake

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,32 @@ target_link_libraries(test_qipython
4949
qi.interface
5050
cxx11
5151
gmock)
52-
# Unfortunately, in some of our toolchains, gtest/gmock headers are found in the qi-framework
53-
# package, which comes first in the include paths order given to the compiler. This causes the
54-
# compiler to use those headers instead of the ones we got from a `FetchContent` of the googletest
55-
# repository.
56-
target_include_directories(test_qipython
57-
BEFORE # Force this path to come first in the list of include paths.
58-
PRIVATE $<TARGET_PROPERTY:gmock,INTERFACE_INCLUDE_DIRECTORIES>)
59-
enable_warnings(test_qipython)
60-
set_build_rpath_to_qipython_dependencies(test_qipython)
52+
53+
add_executable(test_qipython_local_interpreter)
54+
target_sources(test_qipython_local_interpreter
55+
PRIVATE tests/test_qipython_local_interpreter.cpp)
56+
target_link_libraries(test_qipython_local_interpreter
57+
PRIVATE Python::Python
58+
pybind11
59+
qi_python_objects
60+
qi.interface
61+
cxx11
62+
gmock)
6163

6264
set(_sdk_prefix "${CMAKE_BINARY_DIR}/sdk")
63-
gtest_discover_tests(test_qipython EXTRA_ARGS --qi-sdk-prefix ${_sdk_prefix})
65+
foreach(test_target IN ITEMS test_qipython
66+
test_qipython_local_interpreter)
67+
# Unfortunately, in some of our toolchains, gtest/gmock headers are found in the qi-framework
68+
# package, which comes first in the include paths order given to the compiler. This causes the
69+
# compiler to use those headers instead of the ones we got from a `FetchContent` of the googletest
70+
# repository.
71+
target_include_directories(${test_target}
72+
BEFORE # Force this path to come first in the list of include paths.
73+
PRIVATE $<TARGET_PROPERTY:gmock,INTERFACE_INCLUDE_DIRECTORIES>)
74+
enable_warnings(${test_target})
75+
set_build_rpath_to_qipython_dependencies(${test_target})
76+
gtest_discover_tests(${test_target} EXTRA_ARGS --qi-sdk-prefix ${_sdk_prefix})
77+
endforeach()
6478

6579
if(NOT Python_Interpreter_FOUND)
6680
message(WARNING "tests: a compatible Python Interpreter was NOT found, Python tests are DISABLED.")

qipython/pyguard.hpp

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -143,59 +143,57 @@ void pybind11GuardDisarm(G& guard)
143143
/// RAII utility type that guarantees that the GIL is locked for the scope of
144144
/// the lifetime of the object.
145145
///
146+
/// Objects of this type (or objects composed of them) must not be kept alive
147+
/// after the hand is given back to the interpreter.
148+
///
146149
/// This type is re-entrant.
147150
///
148-
/// postcondition: `GILAcquire acq;` establishes `currentThreadHoldsGil()`
151+
/// postcondition: `GILAcquire acq;` establishes
152+
/// `(interpreterIsFinalizing() && *interpreterIsFinalizing()) || currentThreadHoldsGil()`
149153
struct GILAcquire
150154
{
151155
inline GILAcquire()
152156
{
157+
const auto optIsFinalizing = interpreterIsFinalizing();
158+
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
153159
// `gil_scoped_acquire` is re-entrant by itself, so we don't need to check
154160
// whether or not the GIL is already held by the current thread.
155-
QI_ASSERT(currentThreadHoldsGil());
161+
if (!definitelyFinalizing)
162+
_acq.emplace();
163+
QI_ASSERT(definitelyFinalizing || currentThreadHoldsGil());
156164
}
157165

158166
GILAcquire(const GILAcquire&) = delete;
159167
GILAcquire& operator=(const GILAcquire&) = delete;
160168

161-
inline ~GILAcquire()
162-
{
163-
const auto optIsFinalizing = interpreterIsFinalizing();
164-
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
165-
if (definitelyFinalizing)
166-
detail::pybind11GuardDisarm(_acq);
167-
}
168-
169169
private:
170-
pybind11::gil_scoped_acquire _acq;
170+
boost::optional<pybind11::gil_scoped_acquire> _acq;
171171
};
172172

173173
/// RAII utility type that guarantees that the GIL is unlocked for the scope of
174174
/// the lifetime of the object.
175175
///
176+
/// Objects of this type (or objects composed of them) must not be kept alive
177+
/// after the hand is given back to the interpreter.
178+
///
176179
/// This type is re-entrant.
177180
///
178-
/// postcondition: `GILRelease rel;` establishes `!currentThreadHoldsGil()`
181+
/// postcondition: `GILRelease rel;` establishes
182+
/// `(interpreterIsFinalizing() && *interpreterIsFinalizing()) || !currentThreadHoldsGil()`
179183
struct GILRelease
180184
{
181185
inline GILRelease()
182186
{
183-
if (currentThreadHoldsGil())
187+
const auto optIsFinalizing = interpreterIsFinalizing();
188+
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
189+
if (!definitelyFinalizing && currentThreadHoldsGil())
184190
_release.emplace();
185-
QI_ASSERT(!currentThreadHoldsGil());
191+
QI_ASSERT(definitelyFinalizing || !currentThreadHoldsGil());
186192
}
187193

188194
GILRelease(const GILRelease&) = delete;
189195
GILRelease& operator=(const GILRelease&) = delete;
190196

191-
inline ~GILRelease()
192-
{
193-
const auto optIsFinalizing = interpreterIsFinalizing();
194-
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
195-
if (_release && definitelyFinalizing)
196-
detail::pybind11GuardDisarm(*_release);
197-
}
198-
199197
private:
200198
boost::optional<pybind11::gil_scoped_release> _release;
201199
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#include <gtest/gtest.h>
2+
#include <qi/application.hpp>
3+
#include <qi/property.hpp>
4+
#include <qi/session.hpp>
5+
#include <qipython/common.hpp>
6+
#include <qipython/pyguard.hpp>
7+
#include <qipython/pyobject.hpp>
8+
#include <qipython/pysession.hpp>
9+
#include <qipython/pyexport.hpp>
10+
#include <pybind11/pybind11.h>
11+
#include <pybind11/embed.h>
12+
13+
PYBIND11_EMBEDDED_MODULE(test_local_interpreter, m) {
14+
struct ObjectDtorOutsideGIL
15+
{
16+
~ObjectDtorOutsideGIL()
17+
{
18+
qi::py::GILRelease _rel;
19+
// nothing.
20+
}
21+
22+
};
23+
pybind11::class_<ObjectDtorOutsideGIL>(m, "ObjectDtorOutsideGIL")
24+
.def(pybind11::init([]{
25+
return std::make_unique<ObjectDtorOutsideGIL>();
26+
}));
27+
}
28+
29+
TEST(InterpreterFinalize, DoesNotSegfaultGarbageObjectDtorOutsideGIL)
30+
{
31+
pybind11::scoped_interpreter interp;
32+
pybind11::globals()["qitli"] = pybind11::module::import("test_local_interpreter");
33+
pybind11::exec("obj = qitli.ObjectDtorOutsideGIL()");
34+
}
35+
36+
int main(int argc, char **argv)
37+
{
38+
::testing::InitGoogleTest(&argc, argv);
39+
qi::Application app(argc, argv);
40+
return RUN_ALL_TESTS();
41+
}

0 commit comments

Comments
 (0)