Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b863457
fix: Guard against using a uninitialized value after `__new__` alloca…
amjames Aug 28, 2026
66f8f37
fix: free lazily allocated storage on failed init and only permit laz…
henryiii Aug 29, 2026
4991f00
Merge branch 'master' into amjames→bugfix-6153
rwgk Aug 30, 2026
b80c224
fix: isolate old-style constructor storage
rwgk Aug 30, 2026
4455e3f
test: skip constructor thread test on Emscripten
rwgk Aug 30, 2026
14e32ae
fix: bump internals version to 13
rwgk Aug 30, 2026
9468fce
Revert "fix: bump internals version to 13"
rwgk Sep 2, 2026
91122b1
Merge branch 'master' into amjames→bugfix-6153
rwgk Sep 2, 2026
bda1151
fix: recover from legacy constructor storage collisions
rwgk Sep 2, 2026
23f2d0a
test: fix collision subprocess imports
rwgk Sep 2, 2026
89a5f72
refactor: simplify old-style constructor storage tracking
henryiii Sep 3, 2026
7a7e9f3
test: reject later self alias during old-style init
rwgk Sep 3, 2026
955cb19
fix: restrict old-style constructor self permission to self's own loa…
Sep 4, 2026
607d3c6
style: pre-commit fixes
pre-commit-ci[bot] Sep 8, 2026
ad4544b
style: clang-tidy fixes
amjames Sep 10, 2026
f061048
fix: GraalPY exceptions
amjames Sep 10, 2026
bf7f96f
test: pin the two gaps identified in the load-phase review
amjames Sep 11, 2026
d1fbf07
perf: only test the old-style frame pointer where the phase can change
amjames Sep 11, 2026
ab56b0f
docs: describe status_value_constructing with the other status bits
amjames Sep 11, 2026
c10fcc5
style: pre-commit fixes
pre-commit-ci[bot] Sep 11, 2026
182a9d0
style: clang-tidy/format
amjames Sep 11, 2026
1602f18
Merge branch 'master' into amjames→bugfix-6153
rwgk Sep 13, 2026
00981f7
fix: narrow uninitialized-instance guard to direct misuse
rwgk Sep 13, 2026
7811889
test: characterize old-style reentrant load limitation
rwgk Sep 13, 2026
c3c34ad
docs: document deprecated placement-new limitations
rwgk Sep 13, 2026
3c212e3
Merge branch 'master' into amjames→bugfix-6153
rwgk Sep 13, 2026
8f30ec1
Merge branch 'master' into amjames→bugfix-6153
rwgk Sep 13, 2026
1a2e35a
Merge branch 'master' into amjames→bugfix-6153
rwgk Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/advanced/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1427,4 +1427,12 @@ You can do that using ``py::custom_type_setup``:
cls.def("size", &ContainerOwnsPythonObjects::size);
cls.def("clear", &ContainerOwnsPythonObjects::clear);

.. note::

The ``py::detail::is_holder_constructed()`` guards above are required. During garbage
collection, ``tp_traverse`` and ``tp_clear`` may be handed an instance whose C++ value has
not been constructed yet -- for example one created with ``__new__`` before ``__init__``
has run. Casting such an instance raises ``ValueError``, and an exception must not be
allowed to escape either of these slots.

.. versionadded:: 2.8
3 changes: 3 additions & 0 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,8 @@ struct instance {
bool has_patients : 1;
/// If true, this Python object needs to be kept alive for the lifetime of the C++ value.
bool is_alias : 1;
/// For simple layout, tracks whether a constructor is currently constructing the C++ value.
bool simple_value_constructing : 1;

/// Initializes all of the above type/values/holders data (but not the instance values
/// themselves)
Expand All @@ -693,6 +695,7 @@ struct instance {
/// Bit values for the non-simple status flags
static constexpr uint8_t status_holder_constructed = 1;
static constexpr uint8_t status_instance_registered = 2;
static constexpr uint8_t status_value_constructing = 4;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A reference to this should be added in the comment above that describes the other two flags.

};

static_assert(std::is_standard_layout<instance>::value,
Expand Down
253 changes: 237 additions & 16 deletions include/pybind11/detail/type_caster_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#pragma once

#include <pybind11/critical_section.h>
#include <pybind11/gil.h>
#include <pybind11/pytypes.h>
#include <pybind11/trampoline_self_life_support.h>
Expand Down Expand Up @@ -38,6 +39,18 @@
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
PYBIND11_NAMESPACE_BEGIN(detail)

/// Discards a value that was published in `v_h` but whose construction did not complete.
inline void deallocate_instance_value(value_and_holder &v_h) {
if (v_h.instance_registered()) {
if (!deregister_instance(v_h.inst, v_h.value_ptr(), v_h.type)) {
pybind11_fail(
"deallocate_instance_value(): Tried to deallocate unregistered instance!");
}
v_h.set_instance_registered(false);
}
v_h.type->dealloc(v_h);
}

/// A life support system for temporary objects created by `type_caster::load()`.
/// Adding a patient will keep it alive up until the enclosing function returns.
class loader_life_support {
Expand All @@ -61,9 +74,79 @@ class loader_life_support {
loader_life_support *parent = nullptr;
std::unordered_set<PyObject *> keep_alive;

// Old-style placement-new constructors need raw storage while loading their `self`
// argument. Keep it private to the exact overload candidate until its C++ callable returns.
// The dispatcher holds the instance critical section for the lifetime of such a frame.
value_and_holder *old_style_init_self = nullptr;
void *old_style_init_storage = nullptr;
bool old_style_init_self_claimed = false;

static bool is_same_value_and_holder(const value_and_holder &lhs,
const value_and_holder &rhs) {
return lhs.inst == rhs.inst && lhs.vh == rhs.vh;
}

static void report_cleanup_error(const char *message, PyObject *context) noexcept {
error_scope scope;
PyErr_SetString(PyExc_RuntimeError, message);
PyErr_WriteUnraisable(context);
}

// Invoke the deallocator selected by the DSO which registered `type` without exposing the
// private pointer through the real Python instance. In particular, this preserves matching
// class-specific and aligned operator new/delete behavior.
static void deallocate_unconstructed_storage(const type_info *type, void *storage) {
instance storage_instance{};
storage_instance.owned = true;
storage_instance.simple_layout = true;
storage_instance.simple_holder_constructed = false;
storage_instance.simple_value_holder[0] = storage;
value_and_holder storage_v_h(&storage_instance, type, 0, 0);
type->dealloc(storage_v_h);
}

PYBIND11_NOINLINE void cleanup_old_style_init_storage() noexcept {
void *const storage = old_style_init_storage;
old_style_init_storage = nullptr;

auto v_h = *old_style_init_self;
auto *const context = reinterpret_cast<PyObject *>(v_h.inst);
bool private_storage_is_published = false;
if (v_h.value_ptr() != nullptr) {
private_storage_is_published = v_h.value_ptr() == storage;
try {
// Roll back storage published by stale inline caster code before another
// overload candidate is attempted. If the private reservation itself was
// published, deallocate that allocation only once.
deallocate_instance_value(v_h);
} catch (...) {
// A throwing deallocator cannot be retried safely: it may already have partly
// destroyed the holder or released the allocation. Abandon the slot so that
// construction-scope or instance cleanup does not invoke it a second time.
// Reset the slot before PyErr_WriteUnraisable() can run user Python code.
v_h.set_holder_constructed(false);
v_h.set_instance_registered(false);
v_h.value_ptr() = nullptr;
report_cleanup_error(
"loader_life_support: failed to clean up colliding constructor storage",
context);
}
}
if (!private_storage_is_published) {
try {
deallocate_unconstructed_storage(v_h.type, storage);
} catch (...) {
report_cleanup_error(
"loader_life_support: failed to clean up private constructor storage",
context);
}
}
}

public:
/// A new patient frame is created when a function is entered
loader_life_support() {
explicit loader_life_support(value_and_holder *old_style_init_self = nullptr)
: old_style_init_self(old_style_init_self) {
auto &frame = tls_current_frame();
parent = frame;
frame = this;
Expand All @@ -76,11 +159,89 @@ class loader_life_support {
pybind11_fail("loader_life_support: internal error");
}
frame = parent;
if (old_style_init_storage != nullptr) {
cleanup_old_style_init_storage();
}
for (auto *item : keep_alive) {
Py_DECREF(item);
}
}

/// Claims and allocates the private storage for the one old-style constructor `self` load of
/// the current frame; returns nullptr for every other load. `self` is loaded first, or cast
/// inside a legacy `py::object` callback after all arguments were loaded. Nested bound calls
/// and other threads have their own frames and never qualify.
/// The permission is consumed before invoking a potentially user-defined operator new.
static void *try_reserve_old_style_init_storage(value_and_holder &v_h, const type_info *type) {
auto *frame = tls_current_frame();
if (frame == nullptr || frame->old_style_init_self == nullptr
|| frame->old_style_init_self_claimed
|| !is_same_value_and_holder(v_h, *frame->old_style_init_self)
|| v_h.value_ptr() != nullptr) {
return nullptr;
}

frame->old_style_init_self_claimed = true;
if (type->operator_new) {
frame->old_style_init_storage = type->operator_new(type->type_size);
} else {
#if defined(__cpp_aligned_new)
if (type->type_align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
frame->old_style_init_storage
= ::operator new(type->type_size, std::align_val_t(type->type_align));
} else {
frame->old_style_init_storage = ::operator new(type->type_size);
}
#else
frame->old_style_init_storage = ::operator new(type->type_size);
#endif
}
if (frame->old_style_init_storage == nullptr) {
throw std::bad_alloc();
}
return frame->old_style_init_storage;
}

/// Publishes a successfully placement-constructed value and immediately finalizes its holder.
/// This runs after the C++ callable, but before return-value conversion and post-call
/// policies.
static void complete_old_style_init() {
auto *frame = tls_current_frame();
if (frame == nullptr || frame->old_style_init_storage == nullptr) {
return;
}

auto v_h = *frame->old_style_init_self;
if (!v_h.value_constructing()) {
pybind11_fail("loader_life_support: invalid old-style constructor commit");
}

if (v_h.value_ptr() != nullptr) {
// A stale v12 caster can publish a second allocation while the updated constructor's
// storage is private. Roll the whole constructor transaction back, using the
// registered holder/deallocator to clean up the successfully placement-constructed
// private value just as it would clean up a normal instance.
void *const private_storage = frame->old_style_init_storage;
// From here onward the private value has been constructed. Do not let loader cleanup
// mistake it for raw storage if one of the rollback operations throws.
frame->old_style_init_storage = nullptr;
if (v_h.value_ptr() != private_storage) {
deallocate_instance_value(v_h);
v_h.value_ptr() = private_storage;
}
if (!v_h.holder_constructed()) {
v_h.type->init_instance(v_h.inst, nullptr);
}
deallocate_instance_value(v_h);
pybind11_fail("loader_life_support: old-style constructor storage collision");
}

v_h.value_ptr() = frame->old_style_init_storage;
frame->old_style_init_storage = nullptr;
v_h.type->init_instance(v_h.inst, nullptr);
v_h.set_value_constructing(false);
}

/// Keep `h` alive until the current patient frame is destroyed, if there is one.
/// Returns false when called outside a bound function (no frame). Use this, rather
/// than `add_patient`, when failing to register is acceptable because the caller
Expand Down Expand Up @@ -525,6 +686,7 @@ PYBIND11_NOINLINE void instance::allocate_layout() {
= reinterpret_cast<std::uint8_t *>(&nonsimple.values_and_holders[flags_at]);
}
owned = true;
simple_value_constructing = false;
}

// NOLINTNEXTLINE(readability-make-member-function-const)
Expand All @@ -534,6 +696,57 @@ PYBIND11_NOINLINE void instance::deallocate_layout() {
}
}

/// Marks the exact value slot targeted by a constructor. This state is never permission to load
/// the value: every load is rejected until construction finishes, apart from the one-shot
/// old-style constructor `self` permission maintained by `loader_life_support`.
/// The dispatcher creates and destroys this scope while holding the instance critical section.
class instance_construction_scope {
public:
explicit instance_construction_scope(value_and_holder *v_h) {
if (v_h != nullptr) {
start(*v_h);
}
}
~instance_construction_scope() {
if (started_) {
finish();
}
}
instance_construction_scope(const instance_construction_scope &) = delete;
instance_construction_scope &operator=(const instance_construction_scope &) = delete;

bool started() const { return started_; }
bool already_registered() const { return already_registered_; }

private:
PYBIND11_NOINLINE void start(const value_and_holder &v_h) {
v_h_ = v_h;
if (v_h_.value_constructing()) {
return;
}
if (v_h_.instance_registered()) {
already_registered_ = true;
return;
}
value_was_null_ = v_h_.value_ptr() == nullptr;
v_h_.set_value_constructing();
started_ = true;
}
PYBIND11_NOINLINE void finish() {
// A failed new-style constructor can have published a value without completing its
// holder. Preserve the existing cleanup guarantee for that case.
if (value_was_null_ && !v_h_.holder_constructed() && v_h_.value_ptr() != nullptr) {
deallocate_instance_value(v_h_);
}
v_h_.set_value_constructing(false);
}

value_and_holder v_h_;
bool started_ = false;
bool already_registered_ = false;
bool value_was_null_ = false;
};

PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) {
handle type = detail::get_type_handle(tp, false);
if (!type) {
Expand Down Expand Up @@ -1128,6 +1341,24 @@ class type_caster_generic {

// Base methods for generic caster; there are overridden in copyable_holder_caster
void load_value(value_and_holder &&v_h) {
scoped_critical_section lock(handle(reinterpret_cast<PyObject *>(v_h.inst)));

// A non-null value pointer is not sufficient while a constructor is running: old-style
// placement-new storage may exist before the C++ object's lifetime has begun. Only the
// one `self` load of the current old-style constructor candidate may access private raw
// storage; reentrant, cross-base, nested, and cross-thread loads must all fail.
if (v_h.value_constructing()) {
const auto *type = v_h.type ? v_h.type : typeinfo;
if (void *reserved
= loader_life_support::try_reserve_old_style_init_storage(v_h, type)) {
value = reserved;
return;
}
throw value_error("Missing value for wrapped C++ type `"
+ clean_type_id(cpptype->name())
+ "`: Python instance is still being constructed.");
}

if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
smart_holder_type_caster_support::value_and_holder_helper v_h_helper;
v_h_helper.loaded_v_h = v_h;
Expand All @@ -1138,22 +1369,12 @@ class type_caster_generic {
}
}
auto *&vptr = v_h.value_ptr();
// Lazy allocation for unallocated values:
if (vptr == nullptr) {
const auto *type = v_h.type ? v_h.type : typeinfo;
if (type->operator_new) {
vptr = type->operator_new(type->type_size);
} else {
#if defined(__cpp_aligned_new)
if (type->type_align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
vptr = ::operator new(type->type_size, std::align_val_t(type->type_align));
} else {
vptr = ::operator new(type->type_size);
}
#else
vptr = ::operator new(type->type_size);
#endif
}
throw value_error("Missing value for wrapped C++ type `"
+ clean_type_id(cpptype->name())
+ "`: Python instance is uninitialized: the C++ object was "
"never constructed (`__init__()` was bypassed, e.g. by "
"calling `__new__()` directly).");
}
value = vptr;
}
Expand Down
16 changes: 16 additions & 0 deletions include/pybind11/detail/value_and_holder.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ struct value_and_holder {
&= static_cast<std::uint8_t>(~instance::status_instance_registered);
}
}
bool value_constructing() const {
return inst->simple_layout
? inst->simple_value_constructing
: ((inst->nonsimple.status[index] & instance::status_value_constructing) != 0);
}
// NOLINTNEXTLINE(readability-make-member-function-const)
void set_value_constructing(bool v = true) {
if (inst->simple_layout) {
inst->simple_value_constructing = v;
} else if (v) {
inst->nonsimple.status[index] |= instance::status_value_constructing;
} else {
inst->nonsimple.status[index]
&= static_cast<std::uint8_t>(~instance::status_value_constructing);
}
}
};

// This is a semi-public API to check if the corresponding instance has been constructed with a
Expand Down
Loading
Loading