Skip to content

Commit 8dba00b

Browse files
committed
Demonstration of Undefined Behavior in handling of polymorphic pointers.
(This demo does NOT involve smart pointers at all, unlike the otherwise similar test_smart_ptr_private_first_base.)
1 parent 71aa978 commit 8dba00b

File tree

2 files changed

+84
-0
lines changed

2 files changed

+84
-0
lines changed

Diff for: tests/test_private_first_base.cpp

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Demonstration of UB (Undefined Behavior) in handling of polymorphic pointers,
2+
// specifically:
3+
// https://github.com/pybind/pybind11/blob/30eb39ed79d1e2eeff15219ac00773034300a5e6/include/pybind11/cast.h#L229
4+
// `return reinterpret_cast<V *&>(vh[0]);`
5+
// casts a `void` pointer to a `base`. The `void` pointer is obtained through
6+
// a `dynamic_cast` here:
7+
// https://github.com/pybind/pybind11/blob/30eb39ed79d1e2eeff15219ac00773034300a5e6/include/pybind11/cast.h#L852
8+
// `return dynamic_cast<const void*>(src);`
9+
// The `dynamic_cast` is well-defined:
10+
// https://en.cppreference.com/w/cpp/language/dynamic_cast
11+
// 4) If expression is a pointer to a polymorphic type, and new-type
12+
// is a pointer to void, the result is a pointer to the most derived
13+
// object pointed or referenced by expression.
14+
// But the `reinterpret_cast` above is UB: `test_make_drvd_pass_base` in
15+
// `test_private_first_base.py` fails with a Segmentation Fault (Linux,
16+
// clang++ -std=c++17).
17+
// The only well-defined cast is back to a `drvd` pointer (`static_cast` can be
18+
// used), which can then safely be cast up to a `base` pointer. Note that
19+
// `test_make_drvd_up_cast_pass_drvd` passes because the `void` pointer is cast
20+
// to `drvd` pointer in this situation.
21+
22+
#include "pybind11_tests.h"
23+
24+
namespace pybind11_tests {
25+
namespace private_first_base {
26+
27+
struct base {
28+
base() : base_id(100) {}
29+
virtual ~base() = default;
30+
virtual int id() const { return base_id; }
31+
base(const base&) = default;
32+
int base_id;
33+
};
34+
35+
struct private_first_base { // Any class with a virtual function will do.
36+
private_first_base() {}
37+
virtual void some_other_virtual_function() const {}
38+
virtual ~private_first_base() = default;
39+
private_first_base(const private_first_base&) = default;
40+
};
41+
42+
struct drvd : private private_first_base, public base {
43+
drvd() {}
44+
int id() const override { return 2 * base_id; }
45+
};
46+
47+
inline drvd* make_drvd() { return new drvd; }
48+
inline base* make_drvd_up_cast() { return new drvd; }
49+
50+
inline int pass_base(const base* b) { return b->id(); }
51+
inline int pass_drvd(const drvd* d) { return d->id(); }
52+
53+
TEST_SUBMODULE(private_first_base, m) {
54+
py::class_<base>(m, "base");
55+
py::class_<drvd, base>(m, "drvd");
56+
57+
m.def("make_drvd", make_drvd,
58+
py::return_value_policy::take_ownership);
59+
m.def("make_drvd_up_cast", make_drvd_up_cast,
60+
py::return_value_policy::take_ownership);
61+
m.def("pass_base", pass_base);
62+
m.def("pass_drvd", pass_drvd);
63+
}
64+
65+
} // namespace private_first_base
66+
} // namespace pybind11_tests

Diff for: tests/test_private_first_base.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# -*- coding: utf-8 -*-
2+
import pytest
3+
4+
from pybind11_tests import private_first_base as m
5+
6+
7+
def test_make_drvd_pass_base():
8+
d = m.make_drvd()
9+
i = m.pass_base(d)
10+
assert i == 200
11+
12+
13+
def test_make_drvd_up_cast_pass_drvd():
14+
b = m.make_drvd_up_cast()
15+
# the base return is down-cast immediately.
16+
assert b.__class__.__name__ == "drvd"
17+
i = m.pass_drvd(b)
18+
assert i == 200

0 commit comments

Comments
 (0)