Skip to content

Case 60: Base Class Position Changed (Multiple Inheritance Reorder)

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

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

Verdict and consumer impact

Widget inherits from Drawable and Clickable; v2 swaps the declaration order (Clickable, Drawable instead of Drawable, Clickable). In the Itanium C++ ABI, base subobjects are laid out in declaration order, so swapping them moves every base subobject โ€” and every member declared after them โ€” to a new offset, and changes the this-pointer adjustments baked into every virtual call and cross-cast. A binary compiled against v1 that calls into a v2 library reads and writes through stale offsets. Recompilation against v2 is mandatory.

Old/new diff

v1.cpp v2.cpp
struct Widget : public Drawable, public Clickable { ... } struct Widget : public Clickable, public Drawable { ... }

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)

- type_size_changed: Size changed: Widget (256 -> 320 bits)
  > Old code allocates or copies the type with the old size; heap/stack
    corruption, out-of-bounds access.
  Affected symbols: widget_click, widget_create, widget_destroy, widget_draw, widget_get_id
- type_field_offset_changed: Field offset changed: Widget::widget_id (224 -> 256 bits)
- base_class_offset_changed: Base class 'Clickable' moved within 'Widget' (128 -> 0 bits).
  > The this-pointer adjustment for that base and the offset of every field
    after it shift; existing binaries read the wrong addresses.
- base_class_offset_changed: Base class 'Drawable' moved within 'Widget' (0 -> 128 bits)
- base_class_position_changed: Base class order reordered: Widget โ€” this-pointer
  adjustments changed (['Drawable', 'Clickable'] -> ['Clickable', 'Drawable'])

Minimum evidence

min_evidence: L1 โ€” DWARF DW_TAG_inheritance entries record each base class's offset within the derived class for both versions; abicheck diffs those offsets and the declared base order directly from debug info, no public headers required.

Why abicheck catches it

Each DW_TAG_inheritance DIE under Widget carries the base class type and its DW_AT_data_member_location (byte offset) within the derived object. abicheck compares the ordered list of bases and each base's offset between versions โ€” a reordering changes both, which is exactly what base_class_position_changed and the paired base_class_offset_changed findings above encode.

Runtime failure demonstration

Severity: CRITICAL โ€” object corruption

Scenario: compile app against v1 (assumes Drawable, Clickable layout), swap in v2 .so without recompile.

# Build old library + app
g++ -shared -fPIC -g v1.cpp -o libfoo.so
g++ -g app.cpp -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ draw at (10,20)
# โ†’ clicked zone 5
# โ†’ id = 42
# โ†’ expected id = 42

# Swap in new library (no recompile)
g++ -shared -fPIC -g v2.cpp -o libfoo.so
./app
# โ†’ clicked zone 5
# โ†’ draw at (10,20)
# โ†’ id = -380072416
# โ†’ expected id = 42
# โ†’ CORRUPTION: base-class order changed, subobject offsets mismatch

Why CRITICAL: widget_get_id() reads widget_id at the offset v2's compiler assigned (after the swapped base subobjects), but the app's Widget object was laid out by v1's compiler with widget_id at a different offset โ€” the read lands on unrelated bytes, producing a garbage id. The virtual dispatch for draw()/on_click() also resolves through the wrong vtable pointer for each base, though it happens to still print correct values here.

Safe redesign

Freeze base class declaration order once a class is published as part of a public ABI โ€” treat reordering bases the same as reordering struct members. If a new base is genuinely needed, append it last, or move to a pointer-to-implementation design so the public type's layout never depends on inheritance details.

Real-world example: GUI frameworks with multiple interface inheritance (Qt, GTK+) must freeze base class order once published; COM-style interfaces on Linux also depend on base class order for vtable layout.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml

References


Source files

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

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