Skip to content

Commit f5cbe15

Browse files
committed
fix: Guard against using a uninitialized value after __new__ allocating python object
fixes: #6153 Objects initialized with `cls.__new__(cls)` (`cls` is a pybind11 bound type). Will not have the C++ object allocated. When hitting `load_value` storage is allocated but not initialized, calling a virtual method will load a garbage vptr and segfault. This is similar to #2152, but the guard in metaclass `__call__` is not triggered when using `__new__`. Protect against giving a pointer to garbage in all cases except the `__init__` + `__setstate__` path. Authored with claude
1 parent 5e9611a commit f5cbe15

6 files changed

Lines changed: 119 additions & 0 deletions

File tree

docs/advanced/classes.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,4 +1427,12 @@ You can do that using ``py::custom_type_setup``:
14271427
cls.def("size", &ContainerOwnsPythonObjects::size);
14281428
cls.def("clear", &ContainerOwnsPythonObjects::clear);
14291429
1430+
.. note::
1431+
1432+
The ``py::detail::is_holder_constructed()`` guards above are required. During garbage
1433+
collection, ``tp_traverse`` and ``tp_clear`` may be handed an instance whose C++ value has
1434+
not been constructed yet -- for example one created with ``__new__`` before ``__init__``
1435+
has run. Casting such an instance raises ``ValueError``, and an exception must not be
1436+
allowed to escape either of these slots.
1437+
14301438
.. versionadded:: 2.8

include/pybind11/detail/common.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,11 @@ struct instance {
676676
bool has_patients : 1;
677677
/// If true, this Python object needs to be kept alive for the lifetime of the C++ value.
678678
bool is_alias : 1;
679+
/// If true, an old-style placement-new `__init__`/`__setstate__` is currently constructing the
680+
/// C++ value for this instance. This is the *only* situation in which
681+
/// `type_caster_generic::load_value()` may lazily allocate storage for a value that has not
682+
/// been constructed yet; see `instance_construction_scope` and `cpp_function::dispatcher()`.
683+
bool construction_in_progress : 1;
679684

680685
/// Initializes all of the above type/values/holders data (but not the instance values
681686
/// themselves)

include/pybind11/detail/type_caster_base.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ PYBIND11_NOINLINE void instance::allocate_layout() {
525525
= reinterpret_cast<std::uint8_t *>(&nonsimple.values_and_holders[flags_at]);
526526
}
527527
owned = true;
528+
construction_in_progress = false;
528529
}
529530

530531
// NOLINTNEXTLINE(readability-make-member-function-const)
@@ -534,6 +535,31 @@ PYBIND11_NOINLINE void instance::deallocate_layout() {
534535
}
535536
}
536537

538+
/// RAII helper marking `inst` as "currently being constructed", which is the only situation in
539+
/// which `type_caster_generic::load_value()` will lazily allocate storage for a C++ value that has
540+
/// not been constructed yet. Passing `nullptr` makes this a no-op. Nesting is supported: the
541+
/// previous state is restored, not unconditionally cleared.
542+
class instance_construction_scope {
543+
public:
544+
explicit instance_construction_scope(instance *inst) : inst_{inst} {
545+
if (inst_ != nullptr) {
546+
was_in_progress_ = inst_->construction_in_progress;
547+
inst_->construction_in_progress = true;
548+
}
549+
}
550+
~instance_construction_scope() {
551+
if (inst_ != nullptr) {
552+
inst_->construction_in_progress = was_in_progress_;
553+
}
554+
}
555+
instance_construction_scope(const instance_construction_scope &) = delete;
556+
instance_construction_scope &operator=(const instance_construction_scope &) = delete;
557+
558+
private:
559+
instance *inst_;
560+
bool was_in_progress_ = false;
561+
};
562+
537563
PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) {
538564
handle type = detail::get_type_handle(tp, false);
539565
if (!type) {
@@ -1140,6 +1166,20 @@ class type_caster_generic {
11401166
auto *&vptr = v_h.value_ptr();
11411167
// Lazy allocation for unallocated values:
11421168
if (vptr == nullptr) {
1169+
// Lazy allocation exists only to support the deprecated old-style placement-new
1170+
// `__init__`/`__setstate__` idiom, which is handed a reference to uninitialized
1171+
// storage and constructs the C++ value into it. In any other context a null value
1172+
// pointer means the C++ object was never constructed -- e.g. the instance was created
1173+
// with `__new__()`, bypassing `__init__()` -- and handing out a pointer to
1174+
// uninitialized memory from here is undefined behavior (typically a segfault on the
1175+
// first virtual call). Fail loudly instead.
1176+
if (!v_h.inst->construction_in_progress) {
1177+
throw value_error("Missing value for wrapped C++ type `"
1178+
+ clean_type_id(cpptype->name())
1179+
+ "`: Python instance is uninitialized: the C++ object was "
1180+
"never constructed (`__init__()` was bypassed, e.g. by "
1181+
"calling `__new__()` directly).");
1182+
}
11431183
const auto *type = v_h.type ? v_h.type : typeinfo;
11441184
if (type->operator_new) {
11451185
vptr = type->operator_new(type->type_size);

include/pybind11/pybind11.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,14 @@ class cpp_function : public function {
10011001
}
10021002
}
10031003

1004+
// While a constructor runs, `type_caster_generic::load_value()` is permitted to lazily
1005+
// allocate storage for the C++ value that the constructor is about to construct (the
1006+
// deprecated old-style placement-new `__init__`/`__setstate__` idiom relies on this).
1007+
// Outside this scope, loading a not-yet-constructed instance is an error.
1008+
detail::instance_construction_scope construction_scope(
1009+
overloads->is_constructor ? reinterpret_cast<detail::instance *>(parent.ptr())
1010+
: nullptr);
1011+
10041012
try {
10051013
// We do this in two passes: in the first pass, we load arguments with `convert=false`;
10061014
// in the second, we allow conversion (except for arguments with an explicit

tests/test_class.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ static_assert(!py::detail::is_same_or_base_of<
7777
test_class::pr5396_forward_declared_class::ForwardClass>::value,
7878
"");
7979

80+
// test_new_bypasses_init
81+
struct NewNoInit {
82+
int m_data;
83+
explicit NewNoInit(int data) : m_data(data) {}
84+
NewNoInit(const NewNoInit &) = default;
85+
virtual ~NewNoInit() = default;
86+
int data() const { return m_data; }
87+
// Virtual on purpose: using a not-yet-constructed instance reads the vtable pointer out of
88+
// uninitialized storage, which segfaults rather than merely returning a garbage value.
89+
virtual int v_data() const { return m_data; }
90+
};
91+
8092
TEST_SUBMODULE(class_, m) {
8193
m.def("obj_class_name", [](py::handle obj) { return py::detail::obj_class_name(obj.ptr()); });
8294

@@ -597,6 +609,18 @@ TEST_SUBMODULE(class_, m) {
597609
m.def("return_universal_recipient", []() -> test_class::ConvertibleFromAnything {
598610
return test_class::ConvertibleFromAnything{};
599611
});
612+
613+
py::class_<NewNoInit>(m, "NewNoInit")
614+
.def(py::init<int>())
615+
.def("data", &NewNoInit::data)
616+
.def("v_data", &NewNoInit::v_data)
617+
.def(py::pickle([](const NewNoInit &p) { return py::make_tuple(p.m_data); },
618+
[](const py::tuple &t) {
619+
if (t.size() != 1) {
620+
throw std::runtime_error("Invalid state!");
621+
}
622+
return NewNoInit(t[0].cast<int>());
623+
}));
600624
}
601625

602626
template <int N>

tests/test_class.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import gc
4+
import pickle
45
import sys
56
from unittest import mock
67

@@ -251,6 +252,39 @@ def __init__(self):
251252
assert msg(exc_info.value) == expected
252253

253254

255+
def test_new_bypasses_init():
256+
"""`__new__` allocates the Python object but not the C++ one; using the instance before
257+
`__init__` has run must raise instead of segfaulting."""
258+
obj = m.NewNoInit.__new__(m.NewNoInit)
259+
260+
for use in (lambda: obj.data(), lambda: obj.v_data(), lambda: obj.__getstate__()):
261+
with pytest.raises(ValueError) as exc_info:
262+
use()
263+
assert "Python instance is uninitialized" in str(exc_info.value)
264+
assert "NewNoInit" in str(exc_info.value)
265+
266+
# Calling `__init__()` is the sanctioned way to finish an object made with `__new__()`.
267+
obj.__init__(42)
268+
assert obj.data() == 42
269+
assert obj.v_data() == 42
270+
271+
272+
def test_new_then_setstate():
273+
"""`__new__` must not be blocked: pickle relies on it, and `__setstate__` finishes the
274+
object off. This walks the protocol by hand, then checks the real thing."""
275+
real_obj = m.NewNoInit(42)
276+
assert real_obj.data() == 42
277+
state = real_obj.__getstate__()
278+
279+
obj = m.NewNoInit.__new__(m.NewNoInit) # NEWOBJ
280+
obj.__setstate__(state) # BUILD
281+
assert obj.data() == 42
282+
assert obj.v_data() == 42
283+
284+
for protocol in range(2, pickle.HIGHEST_PROTOCOL + 1):
285+
assert pickle.loads(pickle.dumps(m.NewNoInit(7), protocol)).v_data() == 7
286+
287+
254288
@pytest.mark.parametrize(
255289
"mock_return_value", [None, (1, 2, 3), m.Pet("Polly", "parrot"), m.Dog("Molly")]
256290
)

0 commit comments

Comments
 (0)