Skip to content

ABI Breaking Cases Catalog

This catalog summarizes breakage patterns covered by the examples/ directory (63 cases). For full code walkthroughs, see Examples & Breakage Guide. For the complete list of 100+ detected change types, see Change Kind Reference.

1) Symbol/API surface breaks

  1. case01_symbol_removal — removal of a public symbol from .so.
  2. Risk: runtime loader error / undefined symbol.
  3. Type: hard break.
  4. Example: examples/case01_symbol_removal/
  5. Mitigation: keep compatibility wrappers and remove only in a major ABI line.

  6. case02_param_type_change — function parameter type changed.

  7. Risk: ABI mismatch in calling convention/register usage.
  8. Type: hard break.
  9. Example: examples/case02_param_type_change/
  10. Mitigation: preserve old symbol and add a versioned API entry point.

  11. case10_return_type — function return type changed.

  12. Risk: truncation / UB on the consumer side.
  13. Type: hard break.
  14. Example: examples/case10_return_type/
  15. Mitigation: keep old return-type function, add *_v2 with new contract.

  16. case12_function_removed — function disappears (for example after refactor).

  17. Risk: unresolved symbol for old binaries.
  18. Type: hard break.
  19. Example: examples/case12_function_removed/
  20. Mitigation: deprecate first and remove only with SONAME-major transition.

2) Type/layout breaks

  1. case07_struct_layout — struct layout changed (field add/reorder/offset shift).
  2. Risk: size/offset mismatch, memory corruption.
  3. Type: hard break.
  4. Example: examples/case07_struct_layout/
  5. Mitigation: use opaque handles/Pimpl; avoid in-place public struct mutation.

  6. case08_enum_value_change — enum values reordered/inserted.

  7. Risk: semantic incompatibility (wrong branch/mode selection).
  8. Type: semantic ABI break.
  9. Example: examples/case08_enum_value_change/
  10. Mitigation: treat released enum numeric values as immutable.

  11. case11_global_var_type — global variable type changed.

  12. Risk: size/alignment mismatch.
  13. Type: hard break.
  14. Example: examples/case11_global_var_type/
  15. Mitigation: avoid mutable exported globals; use accessor APIs.

  16. case17_template_abi — layout/ABI changed for instantiated template type.

  17. Risk: binary mismatch between translation units, ODR/size mismatch.
  18. Type: hard break.
  19. Example: examples/case17_template_abi/
  20. Mitigation: keep unstable templates out of ABI boundary.

  21. case18_dependency_leak — ABI dependency leak through public headers.

  22. Risk: external dependency upgrade breaks ABI with no changes in our .so.
  23. Type: transitive ABI break.
  24. Example: examples/case18_dependency_leak/
  25. Mitigation: do not expose third-party layout types in public headers.

  26. case24_union_field_removed — union member removed from public type.

    • Risk: representation contract shrink; old consumers may read/write invalid variant.
    • Type: semantic + layout contract break.
    • Example: examples/case24_union_field_removed/
    • Mitigation: keep union variants stable or introduce versioned replacement type.

3) C++ ABI-specific breaks

  1. case09_cpp_vtable — virtual method set/order changed (vtable drift).

    • Risk: wrong virtual method call target.
    • Type: hard break.
    • Example: examples/case09_cpp_vtable/
    • Mitigation: freeze vtable contract or version interfaces (I2).
  2. case14_cpp_class_size — class size changed.

    • Risk: new/delete mismatch, object layout corruption.
    • Type: hard break.
    • Example: examples/case14_cpp_class_size/
    • Mitigation: use Pimpl to stabilize externally visible object layout.
  3. case15_noexcept_changenoexcept removed/changed.

    • Risk: removing noexcept allows the function to throw; v1-compiled callers omit exception landing pads assuming noexcept, so an actual throw calls std::terminate. Additionally, adding throw can pull in new GLIBCXX version requirements, causing load failure on older systems.
    • Type: breaking — behavior contract broken for callers compiled without exception support; potential VERNEED side-effects on affected compilers.
    • Verdict: BREAKING.
    • Example: examples/case15_noexcept_change/
    • Mitigation: treat noexcept as stable public contract; never add throws to a previously noexcept function in a released library.
  4. case16_inline_to_non_inline — inline→non-inline (or reverse) with ODR effects.

    • Risk: multiple definitions, mixed TU behavior.
    • Type: ODR/semantic ABI risk.
    • Example: examples/case16_inline_to_non_inline/
    • Mitigation: keep inline strategy stable for public headers.
  5. case21_method_became_static — member method changed to static.

    • Risk: changed call/mangling ABI for existing callers.
    • Type: hard break.
    • Example: examples/case21_method_became_static/
    • Mitigation: keep original member method, add static helper under new name.
  6. case22_method_const_changed — method const-qualification changed.

    • Risk: mangled symbol identity and overload contract changed.
    • Type: hard break.
    • Example: examples/case22_method_const_changed/
    • Mitigation: preserve old signature, add new API variant.
  7. case23_pure_virtual_added — pure virtual method added.

    • Risk: existing implementations become incompatible with expanded interface.
    • Type: hard break.
    • Example: examples/case23_pure_virtual_added/
    • Mitigation: introduce interface v2 and keep old interface frozen.

4) ELF/linker/policy cases (important for release policy)

  1. case05_soname — missing/incorrect SONAME.

    • Risk: uncontrolled ABI substitution on upgrade.
    • Type: policy break.
    • Example: examples/case05_soname/
    • Mitigation: bump SONAME on incompatible ABI changes.
  2. case06_visibility — internal symbols leaked as exports.

    • Risk: accidental public ABI surface, future lock-in.
    • Type: ABI hygiene break.
    • Example: examples/case06_visibility/
    • Mitigation: default hidden visibility and explicit export macros.
  3. case13_symbol_versioning — missing symbol versioning.

    • Risk: harder compatibility control across releases/distributions.
    • Type: policy/tooling break.
    • Example: examples/case13_symbol_versioning/
    • Mitigation: maintain and test symbol version scripts in CI.
  4. Version node removal — entire version node removed from version script.

    • Risk: applications linked against the removed version node get unresolved symbols.
    • Type: hard break (symbol_version_node_removed).
    • Mitigation: retain old version nodes and add new symbols under new nodes.
  5. Symbol version migration — symbol moved between version nodes.

    • Risk: applications linked against old version node cannot find the symbol.
    • Type: deployment risk (symbol_moved_version_node).
    • Mitigation: keep the symbol under the old version node and add an alias under the new one.
  6. Missing SONAME bump — breaking changes without SONAME bump.

    • Risk: consumers linked against old SONAME will silently pick up the broken library.
    • Type: policy advisory (soname_bump_recommended).
    • Mitigation: bump SONAME on every binary-incompatible release.
  7. Missing version script — library exports symbols without --version-script.

    • Risk: no fine-grained symbol versioning; harder to evolve the ABI.
    • Type: quality advisory (version_script_missing).
    • Mitigation: add a version script (e.g., --version-script=libfoo.map).

5) Control cases (not direct ABI breaks)

  1. case03_compat_addition — compatible symbol addition.
  2. case04_no_change — unchanged baseline.

6) Additional enum compatibility cases

  1. case19_enum_member_removed — enum member removed.

    • Risk: old persisted/protocol values become invalid or semantically undefined.
    • Type: semantic compatibility break.
    • Example: examples/case19_enum_member_removed/
    • Mitigation: keep old enum members; mark deprecated instead of deleting.
  2. case20_enum_member_value_changed — enum member numeric value changed.

    • Risk: cross-version state/wire interpretation mismatch.
    • Type: semantic compatibility break.
    • Example: examples/case20_enum_member_value_changed/
    • Mitigation: never renumber released enum constants.

7) Additional compatible/informational cases

  1. case25_enum_member_added — enum member appended at end.

    • Risk: source-level only (switch statements may not handle new value).
    • Type: compatible — existing compiled values are unchanged.
    • Example: examples/case25_enum_member_added/
    • Note: if adding shifts existing values, that is caught by ENUM_MEMBER_VALUE_CHANGED.
  2. case26_union_field_added — union field added with larger alignment.

    • Risk: if the new field is the largest member, sizeof(union) grows → TYPE_SIZE_CHANGED → BREAKING.
    • Type: breaking — this fixture adds double d (8 bytes) to a union of int/float (4 bytes each), growing it from 4→8 bytes. Callers that stack-allocate or embed union Value in a struct are broken.
    • Note: adding a field that does not grow the union (smaller or equal size) is compatible. The field addition itself is not the break — the size change is. This fixture demonstrates the breaking variant.
    • Example: examples/case26_union_field_added/
  3. case27_symbol_binding_weakened — GLOBAL → WEAK symbol binding.

    • Risk: interposition — WEAK symbol can be overridden by another GLOBAL definition.
    • Type: compatible — symbol is still exported and resolvable.
    • Example: examples/case27_symbol_binding_weakened/
  4. case29_ifunc_transition — regular function → GNU IFUNC.

    • Risk: older dynamic linkers may not support IFUNC resolution.
    • Type: compatible — PLT/GOT mechanism handles indirection transparently.
    • Example: examples/case29_ifunc_transition/

8) Additional detected changes and verdicts

Beyond the core symbol/type/C++ checks above, abicheck detects a number of ELF-metadata, DWARF-diagnostic, and qualifier changes. Each is classified according to whether it causes a proven binary-level failure.

Compatible/warning changes

These are detected and reported but do not trigger a BREAKING verdict because they do not cause binary linkage or layout failures on their own.

  1. noexcept added/removednoexcept specifier changed on a function.

    • Itanium ABI mangling does not change in practice — the same symbol resolves.
    • Source-level concern only (C++17 function-pointer type mismatch).
    • Verdict: COMPATIBLE.
  2. Enum member added — new enumerator appended to an existing enum.

    • Existing compiled enum values are unchanged.
    • Source-level concern (switch statement coverage).
    • Value shifts (if any) are caught separately by ENUM_MEMBER_VALUE_CHANGED.
    • Verdict: COMPATIBLE.
  3. Union field added — new field added to an existing union.

    • All union fields share offset 0; existing fields are unaffected.
    • Size increase (if any) is caught separately by TYPE_SIZE_CHANGED.
    • Verdict: COMPATIBLE.
  4. GLOBAL→WEAK symbol binding — symbol weakened from STB_GLOBAL to STB_WEAK.

    • Symbol is still exported and resolvable by the dynamic linker.
    • Interposition semantics change but existing binaries continue to work.
    • Verdict: COMPATIBLE.
  5. GNU IFUNC introduced/removed — symbol changed to/from STT_GNU_IFUNC.

    • At the call-ABI level, IFUNC resolution is transparent to callers: the PLT/GOT mechanism dispatches through the resolver without changing the calling convention or symbol signature.
    • However, this is only compatible when: (a) the symbol's signature and versioning are preserved, and (b) the deployment environment supports IFUNC and R_*_IRELATIVE relocations (requires a recent dynamic loader, e.g., glibc ≥ 2.11; musl added partial support in 1.1.0; some embedded/non-glibc runtimes do not support IFUNC at all).
    • On older loaders or non-glibc runtimes, an IFUNC symbol may fail to resolve at load time.
    • Verdict: COMPATIBLE (assuming modern glibc/musl toolchain; may break on older or non-standard loaders).
  6. New/removed DT_NEEDED dependency — library gained or dropped a shared library dependency.

    • Deployment/packaging concern; does not affect the library's exported symbol contract.
    • Verdict: COMPATIBLE.
  7. RPATH/RUNPATH changed — library search path metadata changed.

    • Operational concern; no effect on symbol contract or type layout.
    • Verdict: COMPATIBLE.
  8. Toolchain flag drift — different compiler flags detected via DW_AT_producer.

    • Informational diagnostic; not a proven binary break on its own.
    • Verdict: COMPATIBLE.
  9. DWARF info missing — new binary lacks debug info.

    • Coverage gap warning: struct/enum layout comparison was skipped.
    • Not a break; indicates the comparison is incomplete.
    • Verdict: COMPATIBLE.

Borderline changes classified as BREAKING

These changes are less obvious than a removed symbol or shifted struct layout, but they can cause hard runtime failures in realistic deployments.

  1. ELF st_size changed — symbol size metadata changed in .dynsym.

    • While st_size is not used for normal symbol resolution, it is used by the dynamic linker for COPY relocations (R_*_COPY) to determine the number of bytes to copy into the executable's BSS and to validate size consistency. A size mismatch can cause truncated or over-copied data for COPY-relocated globals.
    • In ELF-only mode (no headers/DWARF) it may also be the sole signal for vtable growth or variable type changes.
    • Verdict: BREAKING (COPY relocation correctness and stripped-binary false-negative avoidance).
  2. New dependency version requirement — library now requires e.g. GLIBC_2.34.

    • Library fails to load on runtimes lacking the required version.
    • Verdict: BREAKING (hard runtime failure on affected systems).
  3. Typeinfo/vtable visibility changed — visibility attribute changed on type metadata.

    • Cross-DSO dynamic_cast and C++ exception matching can fail at runtime.
    • Verdict: BREAKING.
  4. Variable const qualifier added/removed — global variable gained or lost const.

    • Adding const moves variable to .rodata; existing writes cause SIGSEGV.
    • Removing const is an ODR / inlining break (callers may have cached the value).
    • Verdict: BREAKING.

Candidate additions for v2

  1. Calling convention drift (cdecl/stdcall, SysV vs vectorcall).
  2. Alignment/packing changes (#pragma pack, alignas).
  3. Bit-field layout changes (compiler/version/flags dependent).
  4. Exception type ABI changes (throw spec + RTTI interplay).
  5. Allocator ABI changes (std::pmr, custom allocator hooks).
  6. STL ABI toggles (_GLIBCXX_USE_CXX11_ABI, libc++/libstdc++ mixing).
  7. Cross-compiler ABI drift (GCC vs Clang vs MSVC for same headers).
  8. LTO/visibility interaction (inlined symbol disappearance with LTO).
  9. IFUNC / CPU dispatch symbol changes.
  10. Weak symbol semantic changes.