Required prerequisites
What version (or hash if on master) of pybind11 are you using?
since v2.6 (#2152)
Problem description
Summary
Since v2.6 (#2152) the default metaclass throws TypeError when a Python subclass overrides __init__ without calling the bound C++ constructor, precisely to prevent the documented "uninitialized C++ portion → undefined behavior" footgun. That guard lives in pybind11_meta_call(), i.e. the metaclass __call__ path.
Calling cls.__new__(cls) directly bypasses __call__, so the guard never runs. The instance is returned with its C++ storage never constructed, and the first native access to it takes the lazy ::operator new path in detail/type_caster_base.h load_value() and dispatches into unconstructed storage. A virtual call then dereferences an uninitialized vtable pointer and the process dies with SIGSEGV.
This matters because cls.__new__(cls) is not an exotic operation: it is exactly what pickle's NEWOBJ opcode does. So any binding that is (de)serialized can reach the documented UB with no subclass and no misuse in sight.
Reproducible example code
### Reproduction (standalone, no downstream library)
`repro.cpp`:
#include <pybind11/pybind11.h>
#include <string>
namespace py = pybind11;
struct Widget {
int magic = 0x0000ABCD;
std::string name = "constructed";
virtual ~Widget() = default;
virtual int compute() const { return magic; } // virtual -> vtable dispatch
int get_magic() const { return magic; } // non-virtual field read
const std::string &get_name() const { return name; }
};
PYBIND11_MODULE(repro, m) {
py::class_<Widget>(m, "Widget")
.def(py::init<>())
.def("compute", &Widget::compute)
.def("get_magic", &Widget::get_magic)
.def("get_name", &Widget::get_name);
}
Build:
python -m venv .venv && ./.venv/bin/pip install pybind11
EXT=$(./.venv/bin/python -c "import sysconfig;print(sysconfig.get_config_var('EXT_SUFFIX'))")
c++ -O3 -std=c++17 -fPIC -shared $(./.venv/bin/python -m pybind11 --includes) \
repro.cpp -o repro$EXT -undefined dynamic_lookup
Behavior, each case in a fresh interpreter:
# A. subclass overrides __init__ and skips base init -> guard fires (correct)
class Sub(repro.Widget):
def __init__(self): pass
Sub() # TypeError: repro.Widget.__init__() must be called when overriding __init__
# B. base type via __new__ directly -> no guard, segfault
u = repro.Widget.__new__(repro.Widget)
u.compute() # SIGSEGV
# C. subclass via __new__ directly -> no guard, segfault
class Sub2(repro.Widget):
def __init__(self): repro.Widget.__init__(self)
Sub2.__new__(Sub2).compute() # SIGSEGV
Observed on **pybind11 3.1.0, Python 3.14.6, macOS arm64 (Darwin 25.5.0), Apple clang**:
| Case | Guard | Result |
|---|---|---|
| A — subclass skips base `__init__` (the #2152 case) | `TypeError` | safe |
| B — `Widget.__new__(Widget)` | none | SIGSEGV (exit 139) |
| C — `Sub.__new__(Sub)` | none | SIGSEGV (exit 139) |
Only the virtual call crashes. Non-virtual reads on the same object return `0` / `""` on this platform (the allocator happens to hand back a zeroed block; on glibc they return garbage instead), so the uninitialized storage is directly observable and which access faults is layout/allocator dependent.
### Why this is worth closing rather than "don't do that"
The docs already state this is UB, and #2152 already committed to catching it. The gap is only that the catch sits in the metaclass call path while `__new__` walks around it. Since `NEWOBJ` is the standard pickle mechanism, the UB is reachable from ordinary serialization, not just from a user forgetting `super().__init__()`. A concrete downstream instance: `torch.distributed.tensor.placement_types.Shard.__new__(Shard).is_shard()` segfaults on torch 2.10–2.13.
### Suggested direction
Zero-initializing the lazy allocation would only turn the UB into a dependable null-vptr crash, since the first virtual call still dereferences it. The behavior that restores the invariant is to treat native access to a holder that was never constructed as an error (raise), rather than lazily allocating raw storage in `load_value()`. Happy to help with a PR or a regression test.
Is this a regression? Put the last known working version here if it is.
Not a regression
Required prerequisites
What version (or hash if on master) of pybind11 are you using?
since v2.6 (#2152)
Problem description
Summary
Since v2.6 (#2152) the default metaclass throws
TypeErrorwhen a Python subclass overrides__init__without calling the bound C++ constructor, precisely to prevent the documented "uninitialized C++ portion → undefined behavior" footgun. That guard lives inpybind11_meta_call(), i.e. the metaclass__call__path.Calling
cls.__new__(cls)directly bypasses__call__, so the guard never runs. The instance is returned with its C++ storage never constructed, and the first native access to it takes the lazy::operator newpath indetail/type_caster_base.hload_value()and dispatches into unconstructed storage. A virtual call then dereferences an uninitialized vtable pointer and the process dies with SIGSEGV.This matters because
cls.__new__(cls)is not an exotic operation: it is exactly whatpickle'sNEWOBJopcode does. So any binding that is (de)serialized can reach the documented UB with no subclass and no misuse in sight.Reproducible example code
Is this a regression? Put the last known working version here if it is.
Not a regression