Case 162: Exported Symbol's Declaring File Moved¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux |
| Flags | โ |
Detected ChangeKinds |
exported_symbol_source_owner_changed |
| Source files | examples/case162_symbol_source_owner_changed/ |
Category: Build/Source Evidence (L5) | Verdict: ๐ก COMPATIBLE_WITH_RISK
This case ships a hand-built pair of evidence-model fixtures (
old.json+new.jsonโSourceGraphSummarydumps) instead of compilablev1/v2sources, so the corpus is also validated compiler-free bytests/test_l3l4l5_examples.py(which callsdiff_source_graph_findingson the same two files directly). Seescripts/gen_l3l4l5_examples.pyfor how they were generated.
Verdict and consumer impact¶
The exported symbol demo::init is declared by include/demo/legacy.h in
v1 and by include/demo/init.h in v2, per the L5 source graph's
SOURCE_DECLARES edge โ while the symbol's name and signature are
completely unchanged. Nothing breaks for a consumer that includes whichever
header currently declares demo::init and links against either release:
the ABI is stable. The risk is narrower โ a consumer that included
include/demo/legacy.h specifically (rather than a stable umbrella header)
now gets a different, possibly missing, declaration, and the refactor can
introduce include-path drift, unexpected inlining changes, or an ODR risk if
the old header still declares the symbol too.
Old/new diff¶
| v1 source graph | v2 source graph |
|---|---|
demo::init() declared by include/demo/legacy.h (SOURCE_DECLARES edge); maps to exported symbol _ZN4demo4initEv |
demo::init() declared by include/demo/init.h โ same exported symbol _ZN4demo4initEv, same name/signature |
abicheck command¶
# old.json / new.json here are this case's committed L5 source-graph
# fixtures (SourceGraphSummary dumps, as produced by build_source_graph over
# a real source checkout) -- not AbiSnapshot files, so they're supplied to
# abicheck as an out-of-band --build-info pack rather than as compare's
# positional inputs.
mkdir -p pack_old/graph pack_new/graph
echo '{"build_source_pack_version": 1}' > pack_old/manifest.json
echo '{"build_source_pack_version": 1}' > pack_new/manifest.json
cp old.json pack_old/graph/source_graph_summary.json
cp new.json pack_new/graph/source_graph_summary.json
# Nothing changed at the binary/header level between v1 and v2, so a pair of
# otherwise-empty snapshots stands in for the (unchanged) artifact side.
python3 -c "
from abicheck.model import AbiSnapshot
from abicheck.serialization import save_snapshot
save_snapshot(AbiSnapshot(library='libdemo.so', version='1.0'), 'empty_old.json')
save_snapshot(AbiSnapshot(library='libdemo.so', version='2.0'), 'empty_new.json')
"
abicheck compare empty_old.json empty_new.json \
--build-info old=pack_old --build-info new=pack_new
Expected abicheck finding¶
Verdict: COMPATIBLE_WITH_RISK (exit 0)
- exported_symbol_source_owner_changed: demo::init's declaring file
changed (include/demo/legacy.h -> include/demo/init.h)
> Name and signature are unchanged, so the artifact diff is quiet; the
file owning the declaration moved -- review for include-path,
inlining, or ODR effects.
Minimum evidence¶
min_evidence: L5 โ the symbol's name, mangled name, and signature are
byte-for-byte identical between v1 and v2, so the exported-symbol table
(L0), DWARF (L1), and the public declaration's own text (L2) all show no
change at all. Only the L5 graph's SOURCE_DECLARES edge, which tracks
which file declares a given source entity, records that the declaration's
owning file moved โ a fact no artifact-level diff carries.
Why abicheck catches it¶
build_source_graph emits one SOURCE_DECLARES edge per declaration,
pointing from the header node that declares it to the source-entity node,
alongside the existing SOURCE_DECL_MAPS_TO_SYMBOL edge to the exported
binary symbol. diff_source_graph_findings follows the same declaration's
SOURCE_DECL_MAPS_TO_SYMBOL edge across both graphs and reports
exported_symbol_source_owner_changed when the source side of its
SOURCE_DECLARES edge differs while the mapped symbol stays the same.
Build/deployment scenario¶
This is what a source-graph audit over two release checkouts would flag
during a header reorganization: demo::init's declaration moved from a
legacy umbrella header to a new, more specific one. The exported symbol and
its signature are untouched, so nothing fails to link or load in either
release โ the risk is purely at the source level, for any consumer that
included include/demo/legacy.h directly instead of a stable public entry
point, and for maintainers who assume both headers still declare the
symbol.
Safe redesign¶
Confirm the declaration move is intentional and that no other header still
declares the same symbol (which would create an ODR/multiple-declaration
risk), and update documentation or examples that reference the old include
path. If the old header needs to keep working for one release, leave a
forwarding #include "init.h" in legacy.h rather than deleting the
declaration from it outright.
Real-world example: large header reorganizations โ splitting a
monolithic umbrella header into per-feature headers โ routinely relocate
declarations like this; libraries commonly leave a forwarding #include in
the old location for one release specifically to avoid silently breaking
direct includes.
Cross-tool comparison¶
Neither abidiff nor abi-compliance-checker have an operand here: both
tools diff compiled binaries (plus optional DWARF/headers), and a
declaration's owning file is โ by construction โ invisible at that layer
once the symbol's name and signature are unchanged; DWARF only records a
DW_AT_decl_file for the compilation unit's own view, not a cross-release
ownership diff. The whole reason this needs L5 source-graph evidence is that
no binary or header-AST-only diff tracks which header a stable symbol
comes from across releases.
Source files¶
new.jsonold.json
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.