Skip to content

Case 203: Class Gained a Vtable Pointer

Field Value
Verdict 🔴 BREAKING
Category Breaking
Classification Rule
Platforms Linux
Flags ABI break
Detected ChangeKinds vptr_introduced, type_size_changed, type_field_offset_changed
Source files catalog/cases/case203_class_gained_vtable_pointer/
Rule family class-gained-vtable-pointer
Subject Vtable and virtual dispatch changes

Category: Breaking | Verdict: ❌ BREAKING

Verdict and consumer impact

Handle::close() becomes virtual — the class's first virtual member. The compiler responds by giving Handle a vtable pointer at offset 0, so sizeof(Handle) grows from 4 to 16 bytes, its alignment rises from 4 to 8, and id_ moves from offset 0 to offset 8. Every consumer that embeds a Handle by value, derives from it, or allocates one on its own stack was compiled for the old, smaller, vptr-less layout.

The existing vtable cases in the catalog all start from an already-polymorphic class and change its slot set; this one crosses the non-polymorphic → polymorphic boundary, which is the mechanism vptr_introduced names. Its negative control is case204_class_gained_non_virtual_method, where the same class gains a method that is not virtual and therefore adds no vptr. The pair proves two things about these two fixtures: the first virtual function is flagged, and an ordinary method addition is not.

Old/new diff

v1.h v2.h
void close(); virtual void close();

abicheck command

g++ -shared -fPIC -g v1.cpp -o libv1.so
g++ -shared -fPIC -g v2.cpp -o libv2.so
abicheck compare libv1.so libv2.so --header old=v1.h --header new=v2.h

Expected abicheck finding

Verdict: BREAKING (exit 4)

vptr_introduced: 'Handle' gained a vtable pointer (became polymorphic).
type_size_changed: Size changed: Handle (32 → 128 bits)
type_field_offset_changed: Field offset changed: Handle::id_ (0 → 64 bits)

(type_alignment_changed, type_vtable_changed and func_virtual_added also fire, along with imported_symbol_added for the type_info vtable the new polymorphic class pulls in from libstdc++.)

Minimum evidence

min_evidence: L1 — a -g build records the record's size, its members' offsets and its vtable holder in DWARF, which is everything the finding rests on. L0 sees new _ZTV/_ZTI symbols appear but cannot attribute the member-offset shift to them.

Why abicheck catches it

diff_types.py compares the matched Handle records' size, alignment and per-member offsets, and vptr_introduced is emitted specifically for the non-polymorphic → polymorphic transition so the cause of the shift is named rather than left as three unrelated layout findings. That distinction is what makes the finding actionable: the fix is not "pad the struct" but "do not add the first virtual function to a released non-polymorphic type."

Runtime failure demonstration

Severity: memory corruption.

Scenario: the consumer owns a Handle sized by v1 (4 bytes) and constructs it in place. After the swap v2's constructor stores a vtable pointer at offset 0 and id_ at offset 8 — 12 bytes into a 4-byte object.

g++ -shared -fPIC -g v1.cpp -o libv1.so
g++ -g app.cpp -L. -lv1 -Wl,-rpath,'$ORIGIN' -o app
./app            # local.id() = 7                                 exit 0
g++ -shared -fPIC -g v2.cpp -o libv1.so   # swap in v2, no recompile
./app            # CORRUPTION: v2's constructor overran the v1-sized object, exit 1

The witness heap-allocates the object with the v1 size, followed by a 32-byte guard region filled with 0xA5, and asserts the guard afterwards. That makes the overrun visible deterministically under GCC and Clang, Debug and Release, without depending on stack layout, optimisation level or a stack-protector canary (the original stack-canary witness stayed silent under some Clang builds). An independent ASan build of the same consumer reports heap-buffer-overflow after the swap.

Safe redesign

Decide polymorphism at the point a type is first published. If a type may ever need virtual dispatch, give it a virtual destructor from release one (paying the vptr cost up front); otherwise keep it non-polymorphic and add behaviour through free functions or a separate interface type.

Cross-tool comparison

abidiff reports the size, alignment and member-offset changes plus the new vtable; ABICC reports "size of the type has changed" and the added virtual method. Neither names the non-polymorphic → polymorphic transition as its own finding, which is what vptr_introduced adds.


Source files

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

See also: Compatibility Catalog · All BREAKING cases · Category: Breaking · Rule: Class gained a vtable pointer · Subject: Vtable and virtual dispatch changes.