Case 174: Secondary Vtable Group Changed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
secondary_vtable_group_changed |
| Source files | examples/case174_secondary_vtable_group_changed/ |
Category: C++ Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Derived's own declaration โ struct Derived : Base1, Base2 {} โ is
byte-for-byte identical in v1 and v2. Only Base2's own declaration
changes: it gains a virtual method. Under the Itanium C++ ABI that makes
Base2 polymorphic, so Derived now needs a new secondary vtable group
to dispatch through a Base2*. Any binary compiled against v1 that embeds,
copies, or derives from Derived (or takes its sizeof) is laid out
incompatibly against v2 โ even though nothing in Derived's own source
changed. Recompilation against v2 is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
struct Base1 { virtual int f1(); virtual ~Base1(); }; |
struct Base1 { virtual int f1(); virtual ~Base1(); }; (unchanged) |
struct Base2 { int helper(); }; |
struct Base2 { virtual int helper(); virtual ~Base2(); }; |
struct Derived : Base1, Base2 {}; |
struct Derived : Base1, Base2 {}; (byte-identical) |
abicheck command¶
g++ -shared -fPIC -g v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- secondary_vtable_group_changed: Secondary vtable groups changed for
'Derived': (none) -> Base2
> A base's polymorphism changed, restructuring the derived vtable even
though Derived's own base declaration list never moved.
- vptr_introduced: 'Base2' gained a vtable pointer (became polymorphic)
- type_size_changed: Base2 (8 -> 64 bits); Derived (64 -> 128 bits)
- vtable_slot_count_changed: Vtable for 'Derived' changed size: 40 -> 80
bytes (~3 -> ~8 virtual slots)
- base_class_offset_changed: Base class 'Base2' moved within 'Derived'
(0 -> 64 bits)
- vtable_thunk_set_changed: Derived::~Derived() thunk added
- type_vtable_changed: vtable populated for Base2
8 breaking findings total (1 flagged as RTTI/internal churn; 7 genuine
public-surface breaks).
Minimum evidence¶
min_evidence: L1 โ reconstructing Derived's secondary vtable groups
requires DWARF's base/vtable metadata (bases, virtual_bases, vtable
on RecordType) on both sides, since the fact that changed lives on
Base2's own declaration, not Derived's. Unlike cases 172/173, this is
not visible on a stripped binary โ Base2's polymorphism has to be known
from debug info, not inferred from a symbol's size alone.
Why abicheck catches it¶
A per-type diff of Derived alone finds nothing โ same bases, same
members. abicheck's secondary_vtable_group_changed detector instead
recomputes each class's ordered list of secondary vtable groups from the
current snapshot's DWARF inheritance metadata on both sides and reports
when that list changes for a class whose own base declaration list did not
move โ a cross-type effect that only becomes visible by also inspecting
Base2's declaration.
Runtime failure demonstration¶
Severity: BREAKING / silent layout mismatch
Scenario: compile app against v1's Derived (Base2 non-polymorphic),
swap in v2's .so without recompile.
# Build old library + app
g++ -shared -fPIC -g v1.cpp -o libfoo.so
g++ -g app.cpp -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ app compiled with sizeof(Derived) = 8
# โ loaded library reports sizeof(Derived) = 8
# โ layouts agree
# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o libfoo.so
./app
# โ app compiled with sizeof(Derived) = 8
# โ loaded library reports sizeof(Derived) = 16
# โ MISMATCH: Base2 gained a vtable pointer underneath an unchanged
# Derived declaration -- Derived's own diff is a no-op, yet its layout
# changed.
Why BREAKING: the app's copy of Derived was sized and laid out
assuming Base2 contributes no vtable pointer; the loaded v2 library's
Base2 subobject now starts with one, shifting every subsequent byte and
silently desynchronizing the two sides' idea of the object's layout.
Safe redesign¶
Adding a virtual method to a base class is a break for every class that derives from it, not just a local change to that base โ audit derived classes, not just the base being edited. If a base might need virtual dispatch later, declare at least one virtual method (even a virtual destructor) from the start, so its polymorphism status is part of the class's original, versioned contract.
Real-world example: this pattern shows up whenever a previously-plain
mixin or interface base is retrofitted with virtual methods โ e.g. adding a
virtual on_event() hook to a formerly non-polymorphic utility base โ
every class that multiply-inherits from it needs a rebuild, even though the
derived class's own source never changed.
Cross-tool comparison¶
abidiff/abidw are not installed in this environment, so no output is
reproduced here.
References¶
- Itanium C++ ABI: primary and secondary vtable groups
- Related cases:
case172_vtable_thunk_offset_changed,case173_vtt_slot_count_changed,case60_base_class_position_changed
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.