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

Verdict and consumer impact

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() instead. Every prebuilt binary that calls a virtual method declared after the insertion point silently misdispatches, with no crash to signal it.

Old/new diff

// 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();
};                                 };

abicheck command

g++ -shared -fPIC -g v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g v2.cpp -o libfoo_v2.so
strip --strip-debug libfoo_v1.so libfoo_v2.so   # remove all DWARF
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- vtable_slot_count_changed: Vtable for 'Shape' changed size: 48 -> 56
  bytes (~4 -> ~5 virtual slots). A virtual method was added, removed, or
  reordered; existing binaries dispatch through fixed vtable offsets and
  will call the wrong slot. Detected from the ELF symbol size without
  debug info.

Additions:
- func_added: New public function: Shape::rotate()

Minimum evidence

min_evidence: L0 โ€” no DWARF, no headers. This case exists specifically to show binary-only detection: the point isn't that abicheck catches a vtable change (many cases do that from DWARF/headers), it's that it catches this one from the ELF symbol table alone, on a fully stripped .so.

Why abicheck catches it

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 directly from that size change โ€” closing the blind spot a pure symbol-name diff would have, since no mangled name needs to change for this break to occur. When debug info or headers are present, the same change is additionally reported as type_vtable_changed/virtual_method_added (L1/L2) with higher confidence; the L0 signal is what makes it detectable on a production binary with no debug info shipped.

Runtime failure demonstration

Severity: BREAKING / SILENT MISDISPATCH

Scenario: app calls perimeter() through a Shape*. The compiler emits a virtual call through a fixed vtable slot index (slot 1 under v1). Insert rotate() ahead of perimeter() in v2 and slot 1 now holds rotate(), so the same binary silently misdispatches.

# Build old library + app
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)

# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o libshape.so
./app
# โ†’ perimeter() = 99 (expected 20)
# โ†’ MISDISPATCH: vtable slot order changed

Why BREAKING: perimeter()'s call site was compiled to invoke whatever sits at slot 1 of Shape's vtable; under v2 that slot holds rotate() instead, and the app's binary has no way to know without recompiling against the v2 header.

Safe redesign

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.

Cross-tool comparison

abidiff works from DWARF (via abidw) and would flag this from debug info the same way most struct/vtable cases in this catalog do โ€” but it has no equivalent binary-only (stripped, no-DWARF) detection path; run against these stripped .sos, abidw has no type information to extract at all. abicheck's ELF-only _ZTV size heuristic is what makes this specific production scenario (a stripped release binary) detectable in the first place.

References


Source files

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

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