Skip to content

Case 12: Function Removed from Shared Library

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds func_removed
Source files browse on GitHub

Category: Symbol API | Abicheck verdict: BREAKING | Verdict: ๐Ÿ”ด BREAKING

What breaks

Any binary dynamically linked against v1 will fail to load with undefined symbol: fast_add when upgraded to v2. The .so no longer exports the symbol, so pre-built binaries have nowhere to resolve it. Even if the function were moved to a header as an inline, already-compiled binaries cannot benefit from that โ€” they need the dynamic symbol.

Why abidiff catches it

Reports 1 Removed function: fast_add with exit 12 (breaking removal).

Code diff

v1.c v2.c
int fast_add(int a, int b) { return a+b; } (function removed from .so)
int other_func(int x) { return x; }

Reproduce manually

gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"   # โ†’ 12

How to fix

Keep the exported wrapper in the .so even if the implementation moves to an inline. The wrapper can simply call the inline: int fast_add(int a, int b) { return _fast_add_impl(a,b); }. Only remove it on a SONAME-bumping major release.

Real-world example

Several C++ standard library implementors have moved functions to inlines for performance and then had to keep exported stubs for ABI compatibility โ€” libstdc++'s std::string refactor in GCC 5 is the canonical cautionary tale.

Real Failure Demo

Severity: CRITICAL

Scenario: app calls fast_add() compiled against v1. v2 removes it from the .so.

# Build v1 + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ fast_add(3, 4) = 7
# โ†’ other_func(5)  = 5

# Swap in v2 (fast_add gone from .so)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ†’ ./app: symbol lookup error: ./app: undefined symbol: fast_add

Why CRITICAL: With default lazy binding (RTLD_LAZY), the error surfaces on the first call through the PLT โ€” the app starts but immediately dies when fast_add is called. With LD_BIND_NOW=1 or RTLD_NOW, it fails at load time. Either way, every binary that ever called fast_add is broken until recompiled against v2 headers.

Runtime behavior

The function is removed from the .so โ€” the dynamic linker emits an undefined symbol error at load time (with RTLD_NOW) or at first call (lazy binding). Runtime result and verdict both agree: BREAKING.

References


Source files

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