fix: detect virtual inheritance in add_base to prevent pointer offset crash#6017
Conversation
… crash
Virtual inheritance places the base subobject at a dynamic offset, but
load_impl Case 2a uses reinterpret_cast which assumes a fixed offset.
This caused segfaults when dispatching inherited methods through virtual
bases (e.g. SftVirtDerived2::name()).
Add an is_static_downcastable SFINAE trait that detects whether
static_cast<Derived*>(Base*) is valid. When it is not (virtual
inheritance), set multiple_inheritance = true in add_base to force the
implicit_casts path, which correctly adjusts pointers at runtime.
Remove the workaround .def("name", &SftVirtDerived2::name) from
test_smart_ptr.cpp that was papering over the issue.
Made-with: Cursor
How long has the virtual inheritance pointer offset bug existed?The bug has been latent in pybind11 since its very first commit 38bd711 (July 5, 2015). Root causeWhen pybind11 needs to convert a Python object to a C++ base type, the Initial commit (38bd711, July 5, 2015): if (PyType_IsSubtype(Py_TYPE(src), typeinfo->type)) {
value = ((instance_type *) src)->value; // stored pointer used as-is
return true;
}This was later refactored into // Case 2a: ... we can use reinterpret_cast.
if (bases.size() == 1 && (no_cpp_mi || bases.front()->type == typeinfo->type)) {
this_.load_value(reinterpret_cast<instance *>(src.ptr())->get_value_and_holder());
return true;
}For non-virtual inheritance, the base subobject is at a fixed compile-time Why it was never triggered
Timeline
The fixAdd a compile-time check in |
|
Thanks @itamaro! |
… crash (pybind#6017) Virtual inheritance places the base subobject at a dynamic offset, but load_impl Case 2a uses reinterpret_cast which assumes a fixed offset. This caused segfaults when dispatching inherited methods through virtual bases (e.g. SftVirtDerived2::name()). Add an is_static_downcastable SFINAE trait that detects whether static_cast<Derived*>(Base*) is valid. When it is not (virtual inheritance), set multiple_inheritance = true in add_base to force the implicit_casts path, which correctly adjusts pointers at runtime. Remove the workaround .def("name", &SftVirtDerived2::name) from test_smart_ptr.cpp that was papering over the issue. Made-with: Cursor
Description
The bug fixed in this PR was discovered by chance while working on tests for PR #6014.
Note that this bug existed "forever".
Summary
SftVirtDerived2::name()viaSftVirtBase::name())is_static_downcastableSFINAE trait to detect virtual inheritance at compile timemultiple_inheritance = truefor virtual bases, forcingload_implto useimplicit_castsinstead ofreinterpret_cast.def("name", &SftVirtDerived2::name)workaround fromtest_smart_ptr.cppBackground
When a class uses virtual inheritance (e.g.
struct D : virtual Base), the base subobject is at a dynamic offset determined at runtime via the vtable. However,load_implCase 2a usesreinterpret_castto convert between base and derived pointers, which assumes a fixed offset. This produces a corrupted pointer, leading to segfaults inshared_ptrcontrol block operations or method dispatch.The existing test (
test_shared_from_this_virt_shared_ptr_arg) had a TODO workaround: an explicit.def("name", ...)re-binding onSftVirtDerived2to avoid the inherited method dispatch path. This PR fixes the root cause so the workaround is no longer needed.Approach
static_cast<Derived*>(Base*)is ill-formed whenBaseis a virtual base ofDerived. We use this as a SFINAE probe:In
class_::add_base, whenis_static_downcastable<Base, type>isfalse, we setrec.multiple_inheritance = true. This forcesload_implto use theimplicit_castspath, which walks the registered base chain and appliesstatic_castin the upcast direction (derived-to-base), which is always valid even for virtual inheritance.Suggested changelog entry:
load_implCase 2a usedreinterpret_castto adjust pointers, which is invalid for virtual inheritance where the base subobject is at a dynamic offset. This bug existed since the initial commit but was never triggered until virtual inheritance tests exercised inherited method dispatch.