Case 06: Symbol Visibility Leak¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, Bad practice |
Detected ChangeKinds |
func_visibility_changed |
| Source files | browse on GitHub |
Category: Visibility | Verdict: ๐ด BREAKING (bad practice)
ground_truth.json:
expected: BREAKING,category: breakingchecker_policy.py:FUNC_REMOVEDโBREAKING_KINDS
What this case is about¶
This case detects a single-library quality issue: a library that was compiled
without -fvisibility=hidden unintentionally exports all internal symbols as part
of its public ABI surface.
This is NOT a comparison between two libraries.
The bad practice lives in libv1.so (the "bad" library) alone.
libv2.so (the "good" library) is provided only as the correct reference โ
it shows how the library should look.
Why exposing internal symbols is bad practice¶
- Every internal symbol (
internal_helper,another_impl, etc.) accidentally becomes part of the public ABI contract. - Any future refactor of internal helpers โ rename, split, remove โ risks being detected as an ABI break or actually breaking consumers that mistakenly linked against them.
- Bloated
.dynsymtables slow dynamic linker startup (symbol resolution scan).
What abicheck detects¶
Running abicheck dump -H bad.c libv1.so + comparing to abicheck dump -H good.c libv2.so:
VISIBILITY_LEAK(BAD PRACTICE / COMPATIBLE):libv1.soexports internal-looking symbols (internal_helper,another_impl) without-fvisibility=hidden. Reported on the old library, not the transition.FUNC_REMOVED(BREAKING):another_impl()is declared inbad.cbut absent fromgood.centirely โ the function was removed from both the header and the library. Any consumer that calledanother_implwill fail to load.FUNC_VISIBILITY_CHANGED(BREAKING):internal_helper()changes from default to hidden visibility โ it disappears from.dynsym.
Overall verdict: BREAKING โ removing or hiding previously-exported symbols is an ABI break for any consumer that depended on them, even if the symbols were only exported by accident.
Note: In ELF-only mode (without
-H), both removals are classified asFUNC_REMOVED_ELF_ONLY(COMPATIBLE) because the tool cannot distinguish intentional public API from accidentally-leaked internals. Use-Hto get the accurate BREAKING verdict.
Dual nature of this case¶
This case is both a bad practice (v1 leaked internal symbols) and a breaking change (v2 removes those symbols from the dynamic table). The root cause is the visibility leak in v1; fixing it in v2 is the right thing to do, but it requires a SONAME bump or a transition plan.
How to reproduce¶
# Build
make -C examples/case06_visibility
# Check libv1.so (bad โ leaks internal symbols)
nm --dynamic --defined-only examples/case06_visibility/libv1.so
# โ public_api, internal_helper, another_impl โ leak!
# Check libv2.so (good โ only public API)
nm --dynamic --defined-only examples/case06_visibility/libv2.so
# โ public_api only โ correct
# Run abicheck (with headers for accurate detection)
python3 -m abicheck.cli dump examples/case06_visibility/libv1.so \
-H examples/case06_visibility/bad.c -o /tmp/v1.json
python3 -m abicheck.cli dump examples/case06_visibility/libv2.so \
-H examples/case06_visibility/good.c -o /tmp/v2.json
python3 -m abicheck.cli compare /tmp/v1.json /tmp/v2.json
# โ BREAKING (FUNC_REMOVED: another_impl) + VISIBILITY_LEAK warning on libv1.so
How to fix¶
Add -fvisibility=hidden to build flags and annotate every intended public
function with __attribute__((visibility("default"))). Use a FOO_EXPORT macro:
#define FOO_EXPORT __attribute__((visibility("default")))
FOO_EXPORT int public_api(void); // exported
static int internal_helper(void); // or just leave it static
Real-world example¶
Qt, GCC libstdc++, LLVM, and most large C++ projects gate their public API with
visibility macros (Q_DECL_EXPORT, _GLIBCXX_VISIBILITY) precisely to avoid
this. -fvisibility=hidden is standard practice since GCC 4.
References¶
Real Failure Demo¶
Severity: CRITICAL
Scenario: compile the app against the leaky v1 library and observe that it finds internal_helper. Rebuild the shared object with the hidden-symbol v2 source, rerun the same binary, and notice that the symbol becomes unavailable (exit code 1).
# Build the two libraries and keep them beside the app
gcc -shared -fPIC -g bad.c -o libv1.so
gcc -shared -fPIC -g good.c -o libv2.so
gcc -g app.c -ldl -o app
# Run the app while both libs are present
./app
# โ v1.so (bad): internal_helper EXPORTED (leak!)
# โ v2.so (good): internal_helper hidden (correct)
# โ WRONG RESULT: visibility contract not demonstrated as expected
echo "exit: $?" # โ 1
Why CRITICAL: The consumer relies on the accidentally-exported internal_helper symbol. v2 hides it, so any binary that resolved the symbol at load time will now fail to link/symbolize and abort before it can handle the crash. This app shows the missing symbol and exits with failure to make the issue obvious.
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.