Skip to content

Case 09: C++ Vtable Change

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux
Flags ABI break, API break
Detected ChangeKinds type_vtable_changed
Source files examples/case09_cpp_vtable/

Category: C++ ABI | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Every Widget object carries a hidden vptr into a static array of virtual function pointers. Old code calls widget->resize() via vtable slot 1. After v2 inserts recolor() at slot 1, that same call site now dispatches to recolor() instead โ€” silent wrong behavior with no crash and no error.

Old/new diff

v1.h v2.h
virtual int draw(); virtual int draw();
virtual int resize(); virtual int recolor(); โ† inserted
virtual int resize();

abicheck command

g++ -shared -fPIC -g v1.cpp -o libwidget_v1.so
g++ -shared -fPIC -g v2.cpp -o libwidget_v2.so
abicheck compare libwidget_v1.so libwidget_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- type_vtable_changed: vtable changed: Widget
  (Widget::draw(), Widget::resize() -> Widget::draw(), Widget::recolor(), Widget::resize())
  > Vtable slot reordering; virtual dispatch calls wrong method.
  Affected symbols: make_widget
- vtable_slot_count_changed: Vtable for 'Widget' changed size: 32 -> 40 bytes (~2 -> ~3 virtual slots)
  > A virtual method was added, removed, or reordered; existing binaries
    dispatch through fixed vtable offsets and will call the wrong slot.

Minimum evidence

min_evidence: L1 โ€” DWARF's class/vtable metadata (DW_AT_vtable_elem_location on each virtual member function) records each method's vtable slot for both versions; -g alone is enough to detect the reordering, no public headers required.

Why abicheck catches it

DWARF records the vtable slot index for every virtual method; abicheck compares the ordered method-to-slot mapping between versions and flags any class whose vtable layout changed โ€” plus a binary-only cross-check from the _ZTV symbol's ELF size, which independently confirms the slot count grew.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app compiled with v1's vtable layout (resize at slot 1) calls resize(). With v2, recolor() is inserted at slot 1, so the app calls the wrong method.

# Build v1 + app
g++ -shared -fPIC -g v1.cpp -o libwidget.so
g++ -g app.cpp -I. -L. -lwidget -Wl,-rpath,. -o app
./app
# โ†’ draw()   = 10 (expected 10)
# โ†’ resize() = 20 (expected 20)

# Swap in v2 (no recompile)
g++ -shared -fPIC -g v2.cpp -o libwidget.so
./app
# โ†’ draw()   = 10 (expected 10)
# โ†’ resize() = 99 (expected 20)
# โ†’ WRONG RESULT: vtable layout changed

Why CRITICAL: the vtable in the app's compiled code indexes resize() at slot 1. In v2 that slot now holds recolor() (returns 99). The wrong method is called silently โ€” no crash, just completely wrong behavior.

Safe redesign

Only append new virtual methods โ€” never insert them in the middle of the vtable. Alternatively, use the non-virtual interface (NVI) pattern: keep a few virtual hooks and add non-virtual public methods that call them.

Real-world example: Qt's strict "no vtable reordering" rule is part of their documented binary-compatibility policy โ€” binary-compatible Qt releases never insert virtual methods in an existing class.

Cross-tool comparison

abidw --out-file v1.xml libwidget_v1.so
abidw --out-file v2.xml libwidget_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"   # โ†’ 4

Note on abidiff 2.4.0: returns exit 4 even though this is a hard vtable incompatibility. abidiff's text output explicitly notes: "note that this is an ABI incompatible change to the vtable of class Widget".

References


Source files

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

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