Skip to content

Commit 326deef

Browse files
authored
Fix segfault when reloading interpreter with external modules (#1092)
* Fix segfault when reloading interpreter with external modules When embedding the interpreter and loading external modules in that embedded interpreter, the external module correctly shares its internals_ptr with the one in the embedded interpreter. When the interpreter is shut down, however, only the `internals_ptr` local to the embedded code is actually reset to nullptr: the external module remains set. The result is that loading an external pybind11 module, letting the interpreter go through a finalize/initialize, then attempting to use something in the external module fails because this external module is still trying to use the old (destroyed) internals. This causes undefined behaviour (typically a segfault). This commit fixes it by adding a level of indirection in the internals path, converting the local internals variable to `internals **` instead of `internals *`. With this change, we can detect a stale internals pointer and reload the internals pointer (either from a capsule or by creating a new internals instance). (No issue number: this was reported on gitter by @henryiii and @aoloe).
1 parent 05d379a commit 326deef

File tree

5 files changed

+50
-12
lines changed

5 files changed

+50
-12
lines changed

include/pybind11/detail/internals.h

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,29 +127,29 @@ struct type_info {
127127

128128
/// Each module locally stores a pointer to the `internals` data. The data
129129
/// itself is shared among modules with the same `PYBIND11_INTERNALS_ID`.
130-
inline internals *&get_internals_ptr() {
131-
static internals *internals_ptr = nullptr;
132-
return internals_ptr;
130+
inline internals **&get_internals_pp() {
131+
static internals **internals_pp = nullptr;
132+
return internals_pp;
133133
}
134134

135135
/// Return a reference to the current `internals` data
136136
PYBIND11_NOINLINE inline internals &get_internals() {
137-
auto *&internals_ptr = get_internals_ptr();
138-
if (internals_ptr)
139-
return *internals_ptr;
137+
auto **&internals_pp = get_internals_pp();
138+
if (internals_pp && *internals_pp)
139+
return **internals_pp;
140140

141141
constexpr auto *id = PYBIND11_INTERNALS_ID;
142142
auto builtins = handle(PyEval_GetBuiltins());
143143
if (builtins.contains(id) && isinstance<capsule>(builtins[id])) {
144-
internals_ptr = *static_cast<internals **>(capsule(builtins[id]));
144+
internals_pp = static_cast<internals **>(capsule(builtins[id]));
145145

146146
// We loaded builtins through python's builtins, which means that our `error_already_set`
147147
// and `builtin_exception` may be different local classes than the ones set up in the
148148
// initial exception translator, below, so add another for our local exception classes.
149149
//
150150
// libstdc++ doesn't require this (types there are identified only by name)
151151
#if !defined(__GLIBCXX__)
152-
internals_ptr->registered_exception_translators.push_front(
152+
(*internals_pp)->registered_exception_translators.push_front(
153153
[](std::exception_ptr p) -> void {
154154
try {
155155
if (p) std::rethrow_exception(p);
@@ -160,6 +160,8 @@ PYBIND11_NOINLINE inline internals &get_internals() {
160160
);
161161
#endif
162162
} else {
163+
if (!internals_pp) internals_pp = new internals*();
164+
auto *&internals_ptr = *internals_pp;
163165
internals_ptr = new internals();
164166
#if defined(WITH_THREAD)
165167
PyEval_InitThreads();
@@ -168,7 +170,7 @@ PYBIND11_NOINLINE inline internals &get_internals() {
168170
PyThread_set_key_value(internals_ptr->tstate, tstate);
169171
internals_ptr->istate = tstate->interp;
170172
#endif
171-
builtins[id] = capsule(&internals_ptr);
173+
builtins[id] = capsule(internals_pp);
172174
internals_ptr->registered_exception_translators.push_front(
173175
[](std::exception_ptr p) -> void {
174176
try {
@@ -192,7 +194,7 @@ PYBIND11_NOINLINE inline internals &get_internals() {
192194
internals_ptr->default_metaclass = make_default_metaclass();
193195
internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass);
194196
}
195-
return *internals_ptr;
197+
return **internals_pp;
196198
}
197199

198200
/// Works like `internals.registered_types_cpp`, but for module-local registered types:

include/pybind11/embed.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ inline void finalize_interpreter() {
145145
// Get the internals pointer (without creating it if it doesn't exist). It's possible for the
146146
// internals to be created during Py_Finalize() (e.g. if a py::capsule calls `get_internals()`
147147
// during destruction), so we get the pointer-pointer here and check it after Py_Finalize().
148-
detail::internals **internals_ptr_ptr = &detail::get_internals_ptr();
148+
detail::internals **internals_ptr_ptr = detail::get_internals_pp();
149149
// It could also be stashed in builtins, so look there too:
150150
if (builtins.contains(id) && isinstance<capsule>(builtins[id]))
151151
internals_ptr_ptr = capsule(builtins[id]);

tests/test_embed/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ target_link_libraries(test_embed PUBLIC ${CMAKE_THREAD_LIBS_INIT})
3333

3434
add_custom_target(cpptest COMMAND $<TARGET_FILE:test_embed>
3535
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
36+
37+
pybind11_add_module(external_module THIN_LTO external_module.cpp)
38+
set_target_properties(external_module PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
39+
add_dependencies(cpptest external_module)
40+
3641
add_dependencies(check cpptest)

tests/test_embed/external_module.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#include <pybind11/pybind11.h>
2+
3+
namespace py = pybind11;
4+
5+
/* Simple test module/test class to check that the referenced internals data of external pybind11
6+
* modules aren't preserved over a finalize/initialize.
7+
*/
8+
9+
PYBIND11_MODULE(external_module, m) {
10+
class A {
11+
public:
12+
A(int value) : v{value} {};
13+
int v;
14+
};
15+
16+
py::class_<A>(m, "A")
17+
.def(py::init<int>())
18+
.def_readwrite("value", &A::v);
19+
20+
m.def("internals_at", []() {
21+
return reinterpret_cast<uintptr_t>(&py::detail::get_internals());
22+
});
23+
}

tests/test_embed/test_interpreter.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,20 @@ bool has_pybind11_internals_builtin() {
101101
};
102102

103103
bool has_pybind11_internals_static() {
104-
return py::detail::get_internals_ptr() != nullptr;
104+
auto **&ipp = py::detail::get_internals_pp();
105+
return ipp && *ipp;
105106
}
106107

107108
TEST_CASE("Restart the interpreter") {
108109
// Verify pre-restart state.
109110
REQUIRE(py::module::import("widget_module").attr("add")(1, 2).cast<int>() == 3);
110111
REQUIRE(has_pybind11_internals_builtin());
111112
REQUIRE(has_pybind11_internals_static());
113+
REQUIRE(py::module::import("external_module").attr("A")(123).attr("value").cast<int>() == 123);
114+
115+
// local and foreign module internals should point to the same internals:
116+
REQUIRE(reinterpret_cast<uintptr_t>(*py::detail::get_internals_pp()) ==
117+
py::module::import("external_module").attr("internals_at")().cast<uintptr_t>());
112118

113119
// Restart the interpreter.
114120
py::finalize_interpreter();
@@ -123,6 +129,8 @@ TEST_CASE("Restart the interpreter") {
123129
pybind11::detail::get_internals();
124130
REQUIRE(has_pybind11_internals_builtin());
125131
REQUIRE(has_pybind11_internals_static());
132+
REQUIRE(reinterpret_cast<uintptr_t>(*py::detail::get_internals_pp()) ==
133+
py::module::import("external_module").attr("internals_at")().cast<uintptr_t>());
126134

127135
// Make sure that an interpreter with no get_internals() created until finalize still gets the
128136
// internals destroyed

0 commit comments

Comments
 (0)