Case 173: VTT Slot Count Changed (detected from a stripped binary)¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
vtt_slot_count_changed |
| Source files | examples/case173_vtt_slot_count_changed/ |
Category: C++ Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Diamond is a classic virtual-inheritance diamond: Left and Right each
declare Base as a virtual base, so Diamond contains exactly one
shared Base subobject. Constructing that object correctly requires a
VTT ("virtual table table" โ _ZTT7Diamond): an array of construction
sub-vtables that gives each base class's own constructor a temporarily
valid view of the not-yet-finished object. v2 adds a third
virtual-inheritance leg, Mixin, so Diamond's construction now needs one
more sub-vtable โ the VTT grows from 56 to 80 bytes. Any consumer that
embeds, copies, or by-value-returns a Diamond โ not just one that calls a
virtual method through it โ inherits a layout mismatch: sizeof(Diamond)
compiled against v1 disagrees with what v2's library actually uses.
Recompilation is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
struct Left : virtual Base {...}; struct Right : virtual Base {...}; struct Diamond : Left, Right {}; |
(unchanged) plus struct Mixin : virtual Base {...}; |
| (no Mixin) | struct Diamond : Left, Right, Mixin {}; |
_ZTT7Diamond = 56 bytes |
_ZTT7Diamond = 80 bytes |
abicheck command¶
g++ -shared -fPIC -g v1.cpp -o libv1.so
g++ -shared -fPIC -g v2.cpp -o libv2.so
strip --strip-debug libv1.so libv2.so
abicheck compare libv1.so libv2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- vtable_slot_count_changed: Vtable for 'Diamond' changed size: 144 -> 216 bytes
- rtti_inheritance_changed: RTTI typeinfo for 'Diamond' changed size: 56 -> 72 bytes
(2 base classes -> 3 base classes)
- vtt_slot_count_changed: VTT size changed for 'Diamond': 56 -> 80 bytes โ
virtual-base construction scaffolding changed
- vtable_thunk_offset_changed: Vtable thunk offset changed for
Diamond::~Diamond(): h:n8, v:0_n32 -> h:n16, h:n8, v:0_n32
Additions:
- func_added / var_added: Mixin's methods, typeinfo, VTT, and vtable
Adding a virtual-inheritance leg fires several findings at once on a
stripped binary; vtt_slot_count_changed is the one that specifically
names the construction-time scaffolding change โ the other three
describe how a Diamond* is used after construction (dispatch, typeinfo,
this-adjustment), not how it is built.
Minimum evidence¶
min_evidence: L0 โ read from the _ZTT7Diamond symbol's st_size alone;
no DWARF, no headers. The command above deliberately strips debug info to
demonstrate the finding survives on a fully stripped binary that carries
none of the type information needed to compute sizeof(Diamond) directly.
Why abicheck catches it¶
The Itanium ABI emits one VTT object per class with a non-trivial
virtual-base construction order, sized proportionally to the number of
sub-vtables it holds. abicheck reads each side's _ZTT<name> symbol size
directly from .dynsym and reports a change as vtt_slot_count_changed โ
proof the virtual-inheritance shape changed, independent of whether any
virtual method is ever called.
Runtime failure demonstration¶
Severity: BREAKING โ silent layout mismatch
Scenario: compile app against v1 (sizeof(Diamond) = 16), swap in v2
.so without recompile.
# Build old library + app
g++ -shared -fPIC -g v1.cpp -o libv1.so
g++ -g app.cpp -I. -L. -lv1 -Wl,-rpath,. -o app
./app
# โ app compiled with sizeof(Diamond) = 16
# โ loaded library reports sizeof(Diamond) = 16
# โ layouts agree
# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o libv2.so
cp libv2.so libv1.so
./app
# โ app compiled with sizeof(Diamond) = 16
# โ loaded library reports sizeof(Diamond) = 24
# โ MISMATCH: the app's compile-time Diamond layout disagrees with the
# library's actual layout.
Why this demo is deterministic, no crash required: it directly
compares the app's compile-time sizeof(Diamond) (baked in from v1.h)
against the value the loaded library actually uses. Any real consumer
that embeds, copies, or by-value-returns a Diamond inherits this same
mismatch, whether or not it happens to segfault on this particular run.
Safe redesign¶
- Treat adding a virtual base to a class in a published hierarchy as an ABI break, not a compatible addition โ it changes both the object's runtime layout and its construction scaffolding.
- Prefer non-virtual composition, or introduce new capabilities via a separate, independently-constructed interface rather than widening an existing virtual-inheritance diamond.
References¶
- Itanium C++ ABI: construction vtables (VTT)
- Related cases:
case142_vtable_slot_count_binary_only,case172_vtable_thunk_offset_changed,case174_secondary_vtable_group_changed
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.