Skip to content

Case 21: Method Became Static

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux
Flags ABI break, API break
Detected ChangeKinds func_static_changed
Source files examples/case21_method_became_static/

Category: Breaking | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Per the Itanium C++ ABI, static-ness is not encoded in symbol mangling for this case, so Widget::bar()'s symbol still resolves after the change (_ZN6Widget3barEv in both versions) โ€” old binaries link fine against new. But the calling contract changes underneath them: old's bar() is an instance method that expects an implicit this pointer; new's bar() is static and expects none. A caller still passing this gets whatever garbage falls out of the mismatched calling convention โ€” no crash required, just silently wrong results.

Old/new diff

old/lib.h new/lib.h
class Widget { public: int value; int bar(); }; class Widget { public: static int bar(); };

abicheck command

g++ -shared -fPIC -std=c++17 -g old/lib.cpp -Iold -o libfoo_v1.so
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -Inew -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 (32 -> 8 bits)
  > Old code allocates or copies the type with the old size; heap/stack
    corruption, out-of-bounds access.
- type_field_removed: Field removed: Widget::value
  > Old code accesses a field that no longer exists at the expected offset;
    reads garbage or writes out of bounds.

Minimum evidence

min_evidence: L1 โ€” DWARF's struct-layout info already proves this is BREAKING: dropping value to make bar() static also shrinks Widget itself (32 โ†’ 8 bits, the minimum size of an empty class), which DW_TAG_structure_type size + member offsets capture directly. Note this isn't quite the same fact as "method became static" โ€” DWARF's own is_static concept (DW_AT_external) describes file-scope linkage, not instance-vs-static class methods, so plain DWARF can't directly see the qualifier change. Adding headers (L2, --ast-frontend clang here since this sandbox has no castxml) additionally surfaces the more semantically precise func_static_changed: Static qualifier changed: bar (False -> True) for the same break โ€” see below.

Why abicheck catches it

At L1, abicheck compares each struct's DW_TAG_structure_type size and member list directly from debug info; Widget losing its value field and shrinking to an empty class's minimum size is unambiguous breaking evidence on its own โ€” old code copying/allocating sizeof(Widget) now over-reads. At L2, the header AST additionally carries each method's static specifier, so abicheck can name the actual qualifier change:

abicheck compare libfoo_v1.so libfoo_v2.so \
  --header old=old/lib.h --header new=new/lib.h --ast-frontend clang
- func_static_changed: Static qualifier changed: bar (False -> True)
  > Static/non-static transition changes calling convention (implicit this
    pointer); ABI mismatch.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against old (instance method, sets value=41, expects bar() to return 42), swap in new .so without recompile.

# Build old library + app
g++ -shared -fPIC -std=c++17 -g old/lib.cpp -Iold -o libwidget.so
g++ -std=c++17 -g app.cpp -Iold -L. -lwidget -Wl,-rpath,. -o app
./app
# โ†’ bar() called (instance method), value=41
# โ†’ got=42 expected=42
# โ†’ exit: 0

# Swap in new library (no recompile)
g++ -shared -fPIC -std=c++17 -g new/lib.cpp -Inew -o libwidget.so
./app
# โ†’ bar() called (static method), returning fixed value
# โ†’ got=7 expected=42
# โ†’ WRONG RESULT: method call contract changed (instance -> static)
# โ†’ exit: 1

Why CRITICAL: the symbol still links โ€” no loader error, no crash โ€” but bar()'s calling contract silently changed. The app's compiled-in call still passes this as if bar() were an instance method; the new static bar() doesn't expect it, so the app observes a deterministically wrong result rather than a diagnosable failure.

Safe redesign

Never turn a public instance method into a static one (or vice versa) in place โ€” it changes the calling convention while keeping the same mangled symbol, so linkers can't catch the mismatch. Add a new static function or free function alongside the old instance method instead, and deprecate the old one for at least one release cycle.

Real-world example: this pattern shows up when a class is refactored to be "more functional" (e.g. removing per-instance state to make a method a pure computation) without renaming the method โ€” exactly the Widget::bar() shape here.

References


Source files

  • CMakeLists.txt
  • app.cpp

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