Skip to content

Commit 4dc33d6

Browse files
authored
Fix smart_holder multiple/virtual inheritance bugs in shared_ptr and unique_ptr to-Python conversions (#5836)
* ChatGPT-generated diamond virtual-inheritance test case. * Report "virtual base at offset 0" but don't skip test. * Remove Left/Right virtual default dtors, to resolve clang-tidy errors: ``` /__w/pybind11/pybind11/tests/test_class_sh_mi_thunks.cpp:44:13: error: prefer using 'override' or (rarely) 'final' instead of 'virtual' [modernize-use-override,-warnings-as-errors] 44 | virtual ~Left() = default; | ~~~~~~~ ^ | override /__w/pybind11/pybind11/tests/test_class_sh_mi_thunks.cpp:48:13: error: prefer using 'override' or (rarely) 'final' instead of 'virtual' [modernize-use-override,-warnings-as-errors] 48 | virtual ~Right() = default; | ~~~~~~~ ^ | override ``` * Add assert(ptr) in register_instance_impl, deregister_instance_impl * Proper bug fix * Also exercise smart_holder_from_unique_ptr * [skip ci] ChatGPT-generated bug fix: smart_holder::from_unique_ptr() * Exception-safe ownership transfer from unique_ptr to shared_ptr ChatGPT: * shared_ptr’s ctor can throw (control-block alloc). Using get() keeps unique_ptr owning the memory if that happens, so no leak. * Only after the shared_ptr is successfully constructed do you release(), transferring ownership exactly once. * [skip ci] Rename alias_ptr to mi_subobject_ptr to distinguish from trampoline code (which often uses the term "alias", too) * [skip ci] Also exercise smart_holder::from_raw_ptr_take_ownership * [skip ci] Add st.first comments (generated by ChatGPT) * [skip ci] Copy and extend (raw_ptr, unique_ptr) reproducer from PR #5796 * Some polishing: comments, add back Left/Right dtors for consistency within test_class_sh_mi_thunks.cpp * explicitly default copy/move for VBase to silence -Wdeprecated-copy-with-dtor * Resolve clang-tidy error: ``` /__w/pybind11/pybind11/tests/test_class_sh_mi_thunks.cpp:67:5: error: 'auto ptr' can be declared as 'auto *ptr' [readability-qualified-auto,-warnings-as-errors] 67 | auto ptr = new Diamond; | ^~~~ | auto * ``` * Expand comment in `smart_holder::from_unique_ptr()` * Better Left/Right padding to make it more likely that we avoid "all at offset 0". Clarify comment. * Give up on `alignas(16)` to resolve MSVC warning: ``` "D:\a\pybind11\pybind11\build\ALL_BUILD.vcxproj" (default target) (1) -> "D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj" (default target) (13) -> (ClCompile target) -> D:\a\pybind11\pybind11\tests\test_class_sh_mi_thunks.cpp(70,17): warning C4316: 'test_class_sh_mi_thunks::Diamond': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] D:\a\pybind11\pybind11\tests\test_class_sh_mi_thunks.cpp(80,43): warning C4316: 'test_class_sh_mi_thunks::Diamond': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: 'std::_Ref_count_obj2<_Ty>': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: with [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: [ [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: _Ty=test_class_sh_mi_thunks::Diamond [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\memory(2913,46): warning C4316: ] [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] D:\a\pybind11\pybind11\include\pybind11\detail\init.h(77,21): warning C4316: 'test_class_sh_mi_thunks::Diamond': object allocated on the heap may not be aligned 16 [D:\a\pybind11\pybind11\build\tests\pybind11_tests.vcxproj] ``` The warning came from alignas(16) making Diamond over-aligned, while regular new/make_shared aren’t guaranteed to return 16-byte aligned memory on MSVC (hence C4316). I’ve removed the explicit alignment and switched to asymmetric payload sizes (char[4] vs char[24]), which still nudges MI layout without relying on over-alignment. This keeps the test goal and eliminates the warning across all MSVC builds. If we ever want to stress over-alignment explicitly, we can add aligned operator new/delete under __cpp_aligned_new, but that’s more than we need here. * Rename test_virtual_base_at_offset_0() → test_virtual_base_not_at_offset_0() and replace pytest.skip() with assert. Add helpful comment for future maintainers.
1 parent 0161da9 commit 4dc33d6

File tree

5 files changed

+227
-15
lines changed

5 files changed

+227
-15
lines changed

include/pybind11/detail/class.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,13 +334,15 @@ inline void enable_try_inc_ref(PyObject *obj) {
334334
#endif
335335

336336
inline bool register_instance_impl(void *ptr, instance *self) {
337+
assert(ptr);
337338
#ifdef Py_GIL_DISABLED
338339
enable_try_inc_ref(reinterpret_cast<PyObject *>(self));
339340
#endif
340341
with_instance_map(ptr, [&](instance_map &instances) { instances.emplace(ptr, self); });
341342
return true; // unused, but gives the same signature as the deregister func
342343
}
343344
inline bool deregister_instance_impl(void *ptr, instance *self) {
345+
assert(ptr);
344346
return with_instance_map(ptr, [&](instance_map &instances) {
345347
auto range = instances.equal_range(ptr);
346348
for (auto it = range.first; it != range.second; ++it) {

include/pybind11/detail/struct_smart_holder.h

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -339,22 +339,42 @@ struct smart_holder {
339339

340340
template <typename T, typename D>
341341
static smart_holder from_unique_ptr(std::unique_ptr<T, D> &&unq_ptr,
342-
void *void_ptr = nullptr) {
342+
void *mi_subobject_ptr = nullptr) {
343343
smart_holder hld;
344344
hld.rtti_uqp_del = &typeid(D);
345345
hld.vptr_is_using_std_default_delete = uqp_del_is_std_default_delete<T, D>();
346-
guarded_delete gd{nullptr, false};
347-
if (hld.vptr_is_using_std_default_delete) {
348-
gd = make_guarded_std_default_delete<T>(true);
349-
} else {
350-
gd = make_guarded_custom_deleter<T, D>(std::move(unq_ptr.get_deleter()), true);
351-
}
352-
if (void_ptr != nullptr) {
353-
hld.vptr.reset(void_ptr, std::move(gd));
346+
347+
// Build the owning control block on the *real object start* (T*).
348+
guarded_delete gd
349+
= hld.vptr_is_using_std_default_delete
350+
? make_guarded_std_default_delete<T>(true)
351+
: make_guarded_custom_deleter<T, D>(std::move(unq_ptr.get_deleter()), true);
352+
// Critical: construct owner with pointer we intend to delete
353+
std::shared_ptr<T> owner(unq_ptr.get(), std::move(gd));
354+
// Relinquish ownership only after successful construction of owner
355+
(void) unq_ptr.release();
356+
357+
// Publish either the MI/VI subobject pointer (if provided) or the full object.
358+
// Why this is needed:
359+
// * The `owner` shared_ptr must always manage the true object start (T*).
360+
// That ensures the deleter is invoked on a valid object header, so the
361+
// virtual destructor can dispatch safely (critical on MSVC with virtual
362+
// inheritance, where base subobjects are not at offset 0).
363+
// * However, pybind11 needs to *register* and expose the subobject pointer
364+
// appropriate for the type being bound.
365+
// This pointer may differ from the T* object start under multiple/virtual
366+
// inheritance.
367+
// This is achieved by using an aliasing shared_ptr<void>:
368+
// - `owner` retains lifetime of the actual T* object start for deletion.
369+
// - `vptr` points at the adjusted subobject (mi_subobject_ptr), giving
370+
// Python the correct identity/registration address.
371+
// If no subobject pointer is passed, we simply publish the full object.
372+
if (mi_subobject_ptr) {
373+
hld.vptr = std::shared_ptr<void>(owner, mi_subobject_ptr);
354374
} else {
355-
hld.vptr.reset(unq_ptr.get(), std::move(gd));
375+
hld.vptr = std::static_pointer_cast<void>(owner);
356376
}
357-
(void) unq_ptr.release();
377+
358378
hld.is_populated = true;
359379
return hld;
360380
}

include/pybind11/detail/type_caster_base.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,8 @@ handle smart_holder_from_unique_ptr(std::unique_ptr<T, D> &&src,
576576
if (!src) {
577577
return none().release();
578578
}
579+
// st.first is the subobject pointer appropriate for tinfo (may differ from src.get()
580+
// under MI/VI). Use this for Python identity/registration, but keep ownership on T*.
579581
void *src_raw_void_ptr = const_cast<void *>(st.first);
580582
assert(st.second != nullptr);
581583
const detail::type_info *tinfo = st.second;
@@ -657,9 +659,10 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr<T> &src,
657659
return none().release();
658660
}
659661

660-
auto src_raw_ptr = src.get();
662+
// st.first is the subobject pointer appropriate for tinfo (may differ from src.get()
663+
// under MI/VI). Use this for Python identity/registration, but keep ownership on T*.
664+
void *src_raw_void_ptr = const_cast<void *>(st.first);
661665
assert(st.second != nullptr);
662-
void *src_raw_void_ptr = static_cast<void *>(src_raw_ptr);
663666
const detail::type_info *tinfo = st.second;
664667
if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) {
665668
// PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder.
@@ -673,8 +676,7 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr<T> &src,
673676
void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr();
674677
valueptr = src_raw_void_ptr;
675678

676-
auto smhldr
677-
= smart_holder::from_shared_ptr(std::shared_ptr<void>(src, const_cast<void *>(st.first)));
679+
auto smhldr = smart_holder::from_shared_ptr(std::shared_ptr<void>(src, src_raw_void_ptr));
678680
tinfo->init_instance(inst_raw_ptr, static_cast<const void *>(&smhldr));
679681

680682
if (policy == return_value_policy::reference_internal) {

tests/test_class_sh_mi_thunks.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace test_class_sh_mi_thunks {
1010
// C++ vtables - Part 2 - Multiple Inheritance
1111
// ... the compiler creates a 'thunk' method that corrects `this` ...
1212

13+
// This test was added under PR #4380
14+
1315
struct Base0 {
1416
virtual ~Base0() = default;
1517
Base0() = default;
@@ -30,6 +32,110 @@ struct Derived : Base1, Base0 {
3032
Derived(const Derived &) = delete;
3133
};
3234

35+
// ChatGPT-generated Diamond added under PR #5836
36+
37+
struct VBase {
38+
VBase() = default;
39+
VBase(const VBase &) = default; // silence -Wdeprecated-copy-with-dtor
40+
VBase &operator=(const VBase &) = default;
41+
VBase(VBase &&) = default;
42+
VBase &operator=(VBase &&) = default;
43+
virtual ~VBase() = default;
44+
virtual int ping() const { return 1; }
45+
int vbase_tag = 42; // ensure it's not empty
46+
};
47+
48+
// Make the virtual bases non-empty and (likely) differently sized.
49+
// The test does *not* require different sizes; we only want to avoid "all at offset 0".
50+
// If a compiler/ABI still places the virtual base at offset 0, our test logs that via
51+
// test_virtual_base_at_offset_0() and continues.
52+
struct Left : virtual VBase {
53+
char pad_l[4]; // small, typically 4 + padding
54+
~Left() override = default;
55+
};
56+
struct Right : virtual VBase {
57+
char pad_r[24]; // larger, to differ from Left
58+
~Right() override = default;
59+
};
60+
61+
struct Diamond : Left, Right {
62+
Diamond() = default;
63+
Diamond(const Diamond &) = default;
64+
~Diamond() override = default;
65+
int ping() const override { return 7; }
66+
int self_tag = 99;
67+
};
68+
69+
VBase *make_diamond_as_vbase_raw_ptr() {
70+
auto *ptr = new Diamond;
71+
return ptr; // upcast
72+
}
73+
74+
std::shared_ptr<VBase> make_diamond_as_vbase_shared_ptr() {
75+
auto shptr = std::make_shared<Diamond>();
76+
return shptr; // upcast
77+
}
78+
79+
std::unique_ptr<VBase> make_diamond_as_vbase_unique_ptr() {
80+
auto uqptr = std::unique_ptr<Diamond>(new Diamond);
81+
return uqptr; // upcast
82+
}
83+
84+
// For diagnostics
85+
struct DiamondAddrs {
86+
uintptr_t as_self;
87+
uintptr_t as_vbase;
88+
uintptr_t as_left;
89+
uintptr_t as_right;
90+
};
91+
92+
DiamondAddrs diamond_addrs() {
93+
auto sp = std::make_shared<Diamond>();
94+
return DiamondAddrs{reinterpret_cast<uintptr_t>(sp.get()),
95+
reinterpret_cast<uintptr_t>(static_cast<VBase *>(sp.get())),
96+
reinterpret_cast<uintptr_t>(static_cast<Left *>(sp.get())),
97+
reinterpret_cast<uintptr_t>(static_cast<Right *>(sp.get()))};
98+
}
99+
100+
// Animal-Cat-Tiger reproducer copied from PR #5796
101+
// clone_raw_ptr, clone_unique_ptr added under PR #5836
102+
103+
class Animal {
104+
public:
105+
Animal() = default;
106+
Animal(const Animal &) = default;
107+
Animal &operator=(const Animal &) = default;
108+
virtual Animal *clone_raw_ptr() const = 0;
109+
virtual std::shared_ptr<Animal> clone_shared_ptr() const = 0;
110+
virtual std::unique_ptr<Animal> clone_unique_ptr() const = 0;
111+
virtual ~Animal() = default;
112+
};
113+
114+
class Cat : virtual public Animal {
115+
public:
116+
Cat() = default;
117+
Cat(const Cat &) = default;
118+
Cat &operator=(const Cat &) = default;
119+
~Cat() override = default;
120+
};
121+
122+
class Tiger : virtual public Cat {
123+
public:
124+
Tiger() = default;
125+
Tiger(const Tiger &) = default;
126+
Tiger &operator=(const Tiger &) = default;
127+
~Tiger() override = default;
128+
Animal *clone_raw_ptr() const override {
129+
return new Tiger(*this); // upcast
130+
}
131+
std::shared_ptr<Animal> clone_shared_ptr() const override {
132+
return std::make_shared<Tiger>(*this); // upcast
133+
}
134+
std::unique_ptr<Animal> clone_unique_ptr() const override {
135+
return std::unique_ptr<Tiger>(new Tiger(*this)); // upcast
136+
}
137+
};
138+
33139
} // namespace test_class_sh_mi_thunks
34140

35141
TEST_SUBMODULE(class_sh_mi_thunks, m) {
@@ -90,4 +196,35 @@ TEST_SUBMODULE(class_sh_mi_thunks, m) {
90196
}
91197
return obj_der->vec.size();
92198
});
199+
200+
py::class_<VBase, py::smart_holder>(m, "VBase").def("ping", &VBase::ping);
201+
202+
py::class_<Left, VBase, py::smart_holder>(m, "Left");
203+
py::class_<Right, VBase, py::smart_holder>(m, "Right");
204+
205+
py::class_<Diamond, Left, Right, py::smart_holder>(m, "Diamond", py::multiple_inheritance())
206+
.def(py::init<>())
207+
.def("ping", &Diamond::ping);
208+
209+
m.def("make_diamond_as_vbase_raw_ptr",
210+
&make_diamond_as_vbase_raw_ptr,
211+
py::return_value_policy::take_ownership);
212+
m.def("make_diamond_as_vbase_shared_ptr", &make_diamond_as_vbase_shared_ptr);
213+
m.def("make_diamond_as_vbase_unique_ptr", &make_diamond_as_vbase_unique_ptr);
214+
215+
py::class_<DiamondAddrs, py::smart_holder>(m, "DiamondAddrs")
216+
.def_readonly("as_self", &DiamondAddrs::as_self)
217+
.def_readonly("as_vbase", &DiamondAddrs::as_vbase)
218+
.def_readonly("as_left", &DiamondAddrs::as_left)
219+
.def_readonly("as_right", &DiamondAddrs::as_right);
220+
221+
m.def("diamond_addrs", &diamond_addrs);
222+
223+
py::classh<Animal>(m, "Animal");
224+
py::classh<Cat, Animal>(m, "Cat");
225+
py::classh<Tiger, Cat>(m, "Tiger", py::multiple_inheritance())
226+
.def(py::init<>())
227+
.def("clone_raw_ptr", &Tiger::clone_raw_ptr)
228+
.def("clone_shared_ptr", &Tiger::clone_shared_ptr)
229+
.def("clone_unique_ptr", &Tiger::clone_unique_ptr);
93230
}

tests/test_class_sh_mi_thunks.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,54 @@ def test_get_shared_vec_size_unique():
5151
assert (
5252
str(exc_info.value) == "Cannot disown external shared_ptr (load_as_unique_ptr)."
5353
)
54+
55+
56+
def test_virtual_base_not_at_offset_0():
57+
# This test ensures that the Diamond fixture actually exercises a non-zero
58+
# virtual-base subobject offset on our supported platforms/ABIs.
59+
#
60+
# If this assert ever fails on some platform/toolchain, please adjust the
61+
# C++ fixture so the virtual base is *not* at offset 0:
62+
# - Keep VBase non-empty.
63+
# - Make Left and Right non-empty and asymmetrically sized and, if
64+
# needed, nudge with a modest alignment.
65+
# - The goal is to achieve a non-zero address delta between `Diamond*`
66+
# and `static_cast<VBase*>(Diamond*)`.
67+
#
68+
# Rationale: certain smart_holder features are exercised only when the
69+
# registered subobject address differs from the most-derived object start,
70+
# so this check guards test efficacy across compilers.
71+
addrs = m.diamond_addrs()
72+
assert addrs.as_vbase - addrs.as_self != 0, (
73+
"Diamond VBase at offset 0 on this platform; to ensure test efficacy, "
74+
"tweak fixtures (VBase/Left/Right) to ensure non-zero subobject offset."
75+
)
76+
77+
78+
@pytest.mark.parametrize(
79+
"make_fn",
80+
[
81+
m.make_diamond_as_vbase_raw_ptr, # exercises smart_holder::from_raw_ptr_take_ownership
82+
m.make_diamond_as_vbase_shared_ptr, # exercises smart_holder_from_shared_ptr
83+
m.make_diamond_as_vbase_unique_ptr, # exercises smart_holder_from_unique_ptr
84+
],
85+
)
86+
def test_make_diamond_as_vbase(make_fn):
87+
# Added under PR #5836
88+
vb = make_fn()
89+
assert vb.ping() == 7
90+
91+
92+
@pytest.mark.parametrize(
93+
"clone_fn",
94+
[
95+
m.Tiger.clone_raw_ptr,
96+
m.Tiger.clone_shared_ptr,
97+
m.Tiger.clone_unique_ptr,
98+
],
99+
)
100+
def test_animal_cat_tiger(clone_fn):
101+
# Based on Animal-Cat-Tiger reproducer under PR #5796
102+
tiger = m.Tiger()
103+
cloned = clone_fn(tiger)
104+
assert isinstance(cloned, m.Tiger)

0 commit comments

Comments
 (0)