Skip to content

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:

  1. Vtable layout: Drawable introduces new vtable entries (its own virtual functions and RTTI), shifting existing Shape vtable slot positions
  2. Object layout: Circle's data members move to accommodate the Drawable subobject, changing field offsets and sizeof(Circle)
  3. Covariant return type: clone() now returns Drawable* instead of Circle*, so callers expecting Circle* 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


Source files

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