Skip to content

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 โ€” _ZTV5Shape grew 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 is MEDIUM because 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


Source files

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

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