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¶
References¶
Source files¶
CMakeLists.txtapp.cppv1.cppv2.cpp
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.