Skip to content

Case 161: New Inter-Target Build/Link Dependency

Field Value
Verdict ๐ŸŸก COMPATIBLE_WITH_RISK
Category Risk
Platforms Linux
Flags โ€”
Detected ChangeKinds target_dependency_added
Source files examples/case161_target_dependency_added/

Category: Build/Source Evidence (L5) | Verdict: ๐ŸŸก COMPATIBLE_WITH_RISK

This case ships a hand-built pair of evidence-model fixtures (old.json + new.json โ€” SourceGraphSummary dumps) instead of compilable v1/v2 sources, so the corpus is also validated compiler-free by tests/test_l3l4l5_examples.py (which calls diff_source_graph_findings on the same two files directly). See scripts/gen_l3l4l5_examples.py for how they were generated.

Verdict and consumer impact

The L5 build graph gains a TARGET_DEPENDS_ON edge from libdemo to libcrypto between v1 and v2. libdemo's own exported symbols and types are unaffected, so nothing about its own ABI breaks โ€” but the shipped artifact may now require an additional shared library at load time, and it takes on that dependency's ABI transitively. A deployment that only shipped (or vendored) the old dependency set can fail to resolve libdemo.so at load time even though nothing in libdemo's own public surface changed.

Old/new diff

v1 build graph v2 build graph
target:libdemo โ€” no outgoing TARGET_DEPENDS_ON edges new edge: target:libdemo --[TARGET_DEPENDS_ON]--> target:libcrypto

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 build/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)

- target_dependency_added: Target libdemo gained a build/link dependency
  on libcrypto
  > The shipped artifact may now require an additional library at load
    time and takes on that dependency's ABI transitively; the DT_NEEDED
    diff proves any concrete new load-time dependency.

Minimum evidence

min_evidence: L5 โ€” a new inter-target link dependency is build-graph structure, not something either side's exported-symbol table (L0), DWARF (L1), or public headers (L2) reveal on their own; you'd only notice it by separately inspecting the built binary's DT_NEEDED entries. The L5 build graph is what makes the dependency edge itself, and which target introduced it, directly comparable across releases.

Why abicheck catches it

SourceGraphSummary.edges records TARGET_DEPENDS_ON edges between build targets, derived from the build evidence (link libraries per compile/link command); diff_source_graph_findings compares the two graphs' target-level edge sets and reports target_dependency_added for any edge present in v2 and absent in v1. Surfacing it from the graph โ€” rather than requiring a manual ldd/readelf -d pass โ€” lets abicheck flag the new coupling and localize which target it came from.

Build/deployment scenario

This is what a packaging or dependency-tracking job diffing the L5 build graph across releases (or, more simply, comparing readelf -d/ldd DT_NEEDED entries) would catch: libdemo.so now links against libcrypto. A downstream package, container image, or SDK bundle that only shipped the old dependency set fails to resolve the shared object at load time in the new release, even though nothing in libdemo's own exported ABI moved.

Safe redesign

Confirm the new dependency is deliberate and that every distribution channel for the library actually ships it; if it's really an implementation detail rather than a public requirement, statically link it or hide its symbols (-fvisibility=hidden) so it never surfaces as a new run time requirement for consumers.

Real-world example: libraries that gained a new crypto/compression backend in a minor version (e.g. picking up a TLS or compression library as a new dependency) have shipped exactly this kind of surprise DT_NEEDED entry, breaking downstream images that hadn't bundled the new shared library.

Cross-tool comparison

Neither abidiff nor abi-compliance-checker have an operand here in this fixture-only form: both tools diff compiled binaries (plus optional DWARF/headers) and would need an actual .so with a real DT_NEEDED table to compare โ€” which this case, being a hand-built L5 graph fixture, doesn't ship. On a real build, readelf -d libdemo.so.2 | grep NEEDED compared against the v1 build is the direct binary-level equivalent of what this finding reports from the build graph.


Source files

  • new.json
  • old.json

See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.