fix: Clean sibling storage after old-style init - #6180
Conversation
|
@espressolee for visibility |
espressolee
left a comment
There was a problem hiding this comment.
Measured at 95d54b0e against 63121f7b, CPython 3.14.0rc1t, macOS arm64, two reproducers built from both include trees (type_caster_base.h the only differing file).
The reported hole is closed on both paths — the failing old-style __init__ and the successful one:
63121f7b 95d54b0e
class C(A, B), init raises SIGSEGV 5/5 ValueError 5/5
class PPCC(PC, CppDrvd), init ok SIGSEGV 3/3 ValueError 3/3
I tried to make the wider cleanup free something live. The closest case is the related-base shape where load_impl routes CppBase& to the CppDrvd slot: after a successful old-style __init__, 95d54b0e leaves the instance uninitialized, so the call is a silent no-op. Worth knowing, but not a regression — at 63121f7b the same sequence leaves a CppBase in a CppDrvd slot, and calling a CppDrvd method is SIGBUS; here it raises and teardown is clean. Both shas already abort that shape under assert (register_instance_impl, class.h:348), so it is out of contract either way.
Reviewed with Claude Code assistance.
|
Thanks @espressolee! |
Description
Follow-up to #6157 and its post-merge review.
When a constructor chain contains a deprecated old-style placement-new callback,
old_style_init_activeintentionally permits lazy allocation across the whole Python instance. In a Python multiple-inheritance layout, reentrant conversion can therefore allocate an unconstructed sibling-base slot. The existing scope cleanup tracked only the constructor self slot, leaving sibling storage published after dispatch; a later ordinary load could treat it as a live C++ object and invoke undefined behavior.This change snapshots all empty value slots when the compatibility window opens and cleans every such slot that acquired storage without constructing a holder. Pre-existing slots and successfully constructed slots are retained. A regression covers a successful old-style base constructor that exposes a new-style sibling during argument conversion, then verifies that the sibling is rejected after dispatch and remains normally constructible.
The scope is intentionally narrow:
Although this is a pre-existing edge case confined to deprecated machinery, the fix is small and localized. More importantly, it completes the boundary established by #6157: the temporary old-style compatibility exception may remain unsafe while active, but it should not leave an unconstructed pointer that poisons later ordinary loads. The upgrade-guide wording is adjusted to describe the actual overload-chain boundary.
Suggested changelog entry:
📚 Documentation preview 📚: https://pybind11--6180.org.readthedocs.build/