Case 76: Internal detail:: polymorphic base vtable change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
internal_type_leaks_via_public_api |
| Source files | browse on GitHub |
Category: Internal-leak | Verdict: BREAKING
What breaks¶
namespace mylib::detail {
class algorithm_iface {
virtual ~algorithm_iface();
virtual int run() = 0;
virtual int status() const = 0;
};
}
class svm_algorithm : public detail::algorithm_iface { /* ... */ };
v2 inserts a new virtual progress() between run() and status()
in the "internal" detail::algorithm_iface. The vtable layout shifts:
| Slot | v1 | v2 |
|---|---|---|
| 0 | ~algorithm_iface() |
~algorithm_iface() |
| 1 | run() |
run() |
| 2 | status() |
progress() โ NEW |
| 3 | โ | status() |
A v1-compiled consumer that calls status() on a svm_algorithm
instance now dispatches through the slot occupied at runtime by
progress() โ wrong return value at best, crash at worst. The
reshuffle never touches any public name; only the slot indices
move. That makes the break invisible to symbol-level tools.
Why abicheck catches it¶
On Linux the vtable symbol (_ZTVN5mylib6detail15algorithm_ifaceE)
grows from 48 to 56 bytes; symbol_size_changed catches that and
combined with the inherited public class drives the verdict to
BREAKING. The internal_type_leaks_via_public_api overlay attaches a
synthetic finding citing the inheritance chain
so reviewers see the "internal" vtable change is in fact part of the public ABI.
Known gap on macOS / Windows: Mach-O
LC_DYSYMTABand PE export tables do not carry a symbol-size field, so thesymbol_size_changedsignal cannot fire. castxml additionally emitsvtable_index=Nonefor every virtual on these toolchain profiles, so the structural vtable diff collapses all virtuals into a single slot andtype_vtable_changedalso misses the reshuffle. The case is registered as aknown_gapinexamples/ground_truth.jsonand the autodiscovery test xfails on those platforms.
Code diff¶
// v1
namespace mylib::detail {
class algorithm_iface {
public:
virtual ~algorithm_iface();
virtual int run() = 0;
virtual int status() const = 0;
};
}
// v2 โ one new virtual inserted MID-vtable
namespace mylib::detail {
class algorithm_iface {
public:
virtual ~algorithm_iface();
virtual int run() = 0;
virtual int progress() const; // NEW โ shifts every later slot
virtual int status() const = 0;
};
}
How to fix¶
Treat detail:: polymorphic bases as a frozen surface, or hide them
behind pimpl so the public class no longer inherits from anything
that can change:
class svm_algorithm {
public:
int run();
int status() const;
private:
struct impl;
impl* p_; // any virtual dispatch happens inside *p_,
// never through this class's vtable.
};
If polymorphism must remain part of the public surface, only ever append new virtuals at the end of the vtable โ never insert mid-table โ and document the slot order as part of the binary contract.
References¶
- Itanium C++ ABI ยง2.5.3: vtable layout, slot indices, and the "appending is OK, inserting is not" rule.
- KDE Techbase, Binary Compatibility Issues With C++: section on virtual-method insertion.
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.