Skip to content

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


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v1.h
  • v2.cpp
  • v2.h

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.