Case 142: Vtable Slot Count Changed (detected from a stripped binary)¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
vtable_slot_count_changed |
| Source files | examples/case142_vtable_slot_count_binary_only/ |
Category: C++ Layout | Verdict: BREAKING
What this case is about¶
// v1 // v2
struct Shape { struct Shape {
virtual int area(); virtual int area();
virtual int rotate(); // <- inserted in the middle
virtual int perimeter(); virtual int perimeter();
virtual ~Shape(); virtual ~Shape();
}; };
A new virtual function is inserted between two existing ones. Under the
Itanium C++ ABI a class's vtable is an ordered array of slots; inserting a
virtual in the middle shifts every later slot down by one. A consumer compiled
against v1 dispatches perimeter() through a fixed slot index โ after the
insertion that index now points at rotate().
Why this case exists: binary-only (L0) detection¶
The point of this case is not that abicheck catches a vtable change โ many cases do that from DWARF/headers. The point is that abicheck catches it from the ELF symbol table alone, with no debug info and no headers.
The Itanium ABI emits a _ZTV5Shape ("vtable for Shape") object whose layout is
[offset-to-top, typeinfo*, slot0, slot1, โฆ]. Adding one virtual grows that
object by exactly one pointer (8 bytes on LP64). abicheck's binary-only layout
detector (diff_elf_layout.py) reads the st_size of the _ZTV symbol on both
sides and infers the slot-count delta โ closing the blind spot a pure
symbol-name diff would have (no mangled name need change).
_ZTV5Shape size: v1 = 5 words (offset-to-top + typeinfo + 3 slots)
v2 = 6 words (one extra slot) -> ~3 โ ~4 virtual slots
What abicheck detects¶
vtable_slot_count_changedโ_ZTV5Shapegrew by one pointer; a virtual method was added, removed, or reordered. Evidence tier L0 (ELF symbol size only โ works on a fully stripped.so). Confidence isMEDIUMbecause the slot count is inferred from size, not read from a vtable dump.
When debug info or headers are present, the same change is additionally
reported as type_vtable_changed / virtual_method_added (L1/L2) โ but the L0
signal is what makes this detectable on a stripped production binary.
Overall verdict: BREAKING
How to reproduce (stripped, binary-only)¶
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 # remove all DWARF
python3 -m abicheck.cli dump libv1.so -o /tmp/v1.json
python3 -m abicheck.cli dump libv2.so -o /tmp/v2.json
python3 -m abicheck.cli compare /tmp/v1.json /tmp/v2.json
# โ BREAKING: vtable_slot_count_changed (_ZTV5Shape)
Real Failure Demo¶
Severity: BREAKING / SILENT MISDISPATCH
g++ -shared -fPIC -g v1.cpp -o libshape.so
g++ -g app.cpp -I. -L. -lshape -Wl,-rpath,. -o app
./app
# perimeter() = 20 (expected 20) โ
g++ -shared -fPIC -g v2.cpp -o libshape.so # insert rotate() ahead of perimeter()
./app
# perimeter() = 99 (expected 20) โ the call dispatched to rotate()
Mitigation¶
- Never insert or reorder virtual functions in a published polymorphic class; append-only, and even then only with per-ABI review.
- Prefer non-virtual extension points or explicitly versioned interfaces.
References¶
- Itanium C++ ABI: vtable layout
- Related cases:
case09_cpp_vtable,case68_virtual_method_added,case140_empty_base_optimization_lost
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.