Case 72: Covariant Return Type Changed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_base_changed, type_vtable_changed, func_return_changed |
| Source files | browse on GitHub |
Category: VTable / Inheritance | Verdict: BREAKING
What breaks¶
The class hierarchy changes: a new intermediate class Drawable is inserted
between Shape and Circle. This causes Circle::clone() to change its
covariant return type from Circle* to Drawable*.
Under the Itanium C++ ABI, inserting an intermediate base class changes:
- Vtable layout:
Drawableintroduces new vtable entries (its own virtual functions and RTTI), shifting existingShapevtable slot positions - Object layout:
Circle's data members move to accommodate theDrawablesubobject, changing field offsets andsizeof(Circle) - Covariant return type:
clone()now returnsDrawable*instead ofCircle*, so callers expectingCircle*get a mistyped pointer
Old binaries compiled against v1 have hardcoded vtable slot indices and field
offsets for the Shape โ Circle hierarchy. In v2, the Drawable intermediate
class shifts everything, causing virtual dispatch to call the wrong function
and field accesses to read garbage.
This is distinct from case09 (vtable reorder within same hierarchy), case37 (base class type changed), and case38 (virtual methods added/removed). This case tests hierarchy insertion โ adding a class between existing base and derived โ which is a particularly common real-world mistake.
Why abicheck catches it¶
Type comparison detects the base class hierarchy change (type_base_changed) and
the vtable layout change (type_vtable_changed). The function return type change
from Circle* to Drawable* is also detected (func_return_changed).
Code diff¶
// v1: flat hierarchy
class Circle : public Shape {
Circle *clone() const override; // covariant: returns Circle*
};
// v2: intermediate class inserted, covariant return changes
class Drawable : public Shape { /* new */ };
class Circle : public Drawable {
Drawable *clone() const override; // covariant: returns Drawable*
};
Real Failure Demo¶
Severity: CRITICAL
Scenario: compile app against v1, swap in v2 .so without recompile.
# Build v1 and app
g++ -shared -fPIC -g v1.cpp -o libshape.so
g++ -g app.cpp -L. -lshape -Wl,-rpath,. -o app
./app
# -> clone radius = 5
# -> Expected: 5
# -> clone area = 75
# -> Expected: 75
# Build v2 (hierarchy changed, covariant return changed)
g++ -shared -fPIC -g v2.cpp -o libshape.so
./app
# -> crash or garbage (vtable layout shifted by Drawable insertion)
Why CRITICAL: The old binary's vtable for Circle was compiled with slot
indices for the two-level hierarchy Shape โ Circle. In v2, Drawable is
inserted between them, adding new vtable entries and shifting slot positions.
Old code dispatching through stale vtable indices calls the wrong function.
Additionally, sizeof(Circle) changes due to the Drawable subobject, so
the clone() return value points to an object with a different layout than
the caller expects.
How to fix¶
Never insert classes into an existing hierarchy without bumping the SONAME. If the hierarchy must evolve, use composition instead of inheritance:
/* Safe: composition instead of hierarchy insertion */
class Circle : public Shape {
Drawable drawable_; /* has-a instead of is-a */
Circle *clone() const override; /* covariant return unchanged */
};
Or freeze the public hierarchy and use internal delegation:
/* Preserve public ABI, change implementation */
class Circle : public Shape { /* public hierarchy frozen */
Circle *clone() const override;
private:
struct Impl; /* internal hierarchy changes hidden */
Impl *impl_;
};
Real-world example¶
Qt's class hierarchy is carefully designed to never insert intermediate classes
in public hierarchies. The KDE Binary Compatibility policy explicitly forbids
this. The COM/XPCOM interface model avoids this entirely by using flat interface
inheritance. LLVM's RTTI system (isa<>, dyn_cast<>) would break if class
hierarchies changed because the classof() chain encodes the exact hierarchy.
References¶
- Itanium C++ ABI ยง2.5.3: Virtual Covariant Thunks
- KDE: Binary Compatibility โ Do not add base classes
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.