mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Check the package's declared dependencies against the libraries its binaries need
The package's Depends comes from cargo-deb's "$auto", which runs dpkg-shlibdeps over each binary. cargo-deb deliberately removes every libgcc entry from that result, on the grounds that every system has one, so the package never declared the libgcc-s1 all four binaries link. Otherwise it passes the dpkg-shlibdeps output through unchanged, with one more gap: when dpkg-shlibdeps fails on a binary it only warns and builds anyway, leaving that binary's libraries out of the list. testing/check-deb-depends.sh runs dpkg-shlibdeps once over every ELF object in the built package and compares the result with the shipped Depends. Every derived entry must be shipped, and the highest shipped floor for it must equal the derived floor. A missing or lower entry means the package installs where its binaries cannot run; a higher one means a hand-written floor no longer tracks what the binaries need. Both fail the build. build-deb-container.sh runs it inside the build image, so it reads the same symbols files and C library cargo-deb did, and every producer of the package is gated. libgcc-s1 (>= 4.2) is now declared by hand beside "$auto". There is no exception for it in the check, so its floor is held equal to the derived one and cannot fall behind or run ahead unnoticed.
This commit is contained in:
@@ -233,6 +233,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
scripts, so the `.ipk` and `.apk` packages install the same bodies and the
|
||||
scenarios in `testing/openwrt/` run what ships.
|
||||
|
||||
#### Packaging
|
||||
|
||||
- The `.deb` now declares `libgcc-s1 (>= 4.2)`. All four binaries link
|
||||
`libgcc_s.so.1`, but cargo-deb removes every libgcc entry from the
|
||||
dependencies it derives, so the package never said so. `libc6` depends on
|
||||
`libgcc-s1` on Debian 12 and Ubuntu 22.04, 24.04 and 26.04, so installs there
|
||||
were not affected. A new check, `testing/check-deb-depends.sh`, runs
|
||||
`dpkg-shlibdeps` over the package's binaries on every build and fails the
|
||||
build when the declared `Depends` leaves out a library the binaries need, or
|
||||
states a floor lower or higher than the one they need. A dependency the
|
||||
packaging tool drops, including one it drops after only a warning when it
|
||||
cannot resolve a binary, now fails the build instead of shipping.
|
||||
|
||||
### Changed
|
||||
|
||||
- The lockfile moves `chacha20` from 0.10.1 to 0.10.2, because 0.10.1 is yanked.
|
||||
|
||||
+6
-1
@@ -88,8 +88,13 @@ priority = "optional"
|
||||
# then could not start. Deriving it also picks up a libdbus floor the hand
|
||||
# written list lacked, and re-derives per architecture, which matters because
|
||||
# arm64 links libdbus in all four binaries where amd64 links it in one.
|
||||
# libgcc-s1 is written by hand beside "$auto" because cargo-deb removes every
|
||||
# libgcc entry from what it derives, on the grounds that every system has one.
|
||||
# testing/check-deb-depends.sh runs dpkg-shlibdeps on every build and fails if
|
||||
# this floor differs from the derived one, so it cannot fall behind or run
|
||||
# ahead of what the binaries need.
|
||||
# systemd stays by hand: nothing links it, so nothing can derive it.
|
||||
depends = "$auto, systemd"
|
||||
depends = "$auto, libgcc-s1 (>= 4.2), systemd"
|
||||
recommends = "bluez"
|
||||
extended-description = """\
|
||||
FIPS is a distributed, decentralized network routing protocol for mesh \
|
||||
|
||||
@@ -152,8 +152,18 @@ DEB="$DEST_ABS/$DEB_NAME"
|
||||
|
||||
# Check the artifact here rather than in one workflow, so every producer is
|
||||
# gated: the release, the CI job, a local run and packaging/Makefile all reach
|
||||
# the check through this script.
|
||||
# both checks through this script. The glibc floor is read from the binaries
|
||||
# and runs on the host. The Depends check runs in the build image, because it
|
||||
# compares against dpkg-shlibdeps and has to read the same symbols files and C
|
||||
# library that cargo-deb's "$auto" read; a host of another distribution could
|
||||
# produce a difference of its own.
|
||||
"$REPO_ROOT/testing/check-glibc-floor.sh" "$DEB" >&2
|
||||
docker run --rm \
|
||||
-v "$REPO_ROOT":/src:ro \
|
||||
-v "$DEST_ABS":/out:ro \
|
||||
-w /src \
|
||||
"$IMAGE_TAG" \
|
||||
testing/check-deb-depends.sh "/out/$DEB_NAME" >&2
|
||||
|
||||
echo "=== Built $DEB ===" >&2
|
||||
printf '%s\n' "$DEB"
|
||||
|
||||
Executable
+208
@@ -0,0 +1,208 @@
|
||||
#!/bin/bash
|
||||
# Fail when a package's declared Depends does not match the libraries its
|
||||
# binaries actually need.
|
||||
#
|
||||
# The package's Depends comes from cargo-deb's "$auto", which runs
|
||||
# dpkg-shlibdeps over each binary. cargo-deb 3.6.3 changes that result in two
|
||||
# ways, both silent. It removes every libgcc entry on the grounds that every
|
||||
# system has one (src/dependencies.rs in the cargo-deb source), so the package
|
||||
# never declared the libgcc-s1 its binaries link. And when dpkg-shlibdeps
|
||||
# fails on a binary it prints a warning and builds anyway, leaving that
|
||||
# binary's libraries out of the list. Neither shows up anywhere but in the
|
||||
# field itself.
|
||||
#
|
||||
# This runs dpkg-shlibdeps once over every ELF object the package contains and
|
||||
# compares the result with the Depends the package ships:
|
||||
#
|
||||
# - every derived entry must be shipped;
|
||||
# - for a derived "name (>= v)", the highest ">=" floor shipped for that name
|
||||
# must EQUAL v. Below it, the package installs where the binaries cannot
|
||||
# run. Above it, a hand-written floor no longer tracks what the binaries
|
||||
# need. There is no exception for any name;
|
||||
# - shipped entries that nothing derives (systemd) are listed, not failed:
|
||||
# they are the ones written by hand, and Cargo.toml says why each is there.
|
||||
#
|
||||
# libgcc-s1 is written by hand in Cargo.toml beside "$auto" because cargo-deb
|
||||
# drops it. The equality rule is what keeps that hand-written floor honest: if
|
||||
# the toolchain stops needing it, or starts needing a newer one, this fails.
|
||||
#
|
||||
# Run it inside the build image (build-deb-container.sh does), so the symbols
|
||||
# files and the C library it reads are the ones cargo-deb read. On a host of a
|
||||
# different distribution a difference could come from the host instead.
|
||||
#
|
||||
# Usage: check-deb-depends.sh <package.deb>...
|
||||
# Exit: 0 every package matches, 1 a mismatch, 2 could not establish a result.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
for tool in dpkg dpkg-deb dpkg-query dpkg-shlibdeps readelf; do
|
||||
command -v "$tool" >/dev/null 2>&1 || {
|
||||
echo "check-deb-depends: $tool is not installed; cannot check anything." >&2
|
||||
echo " Refusing to report a pass I did not establish." >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
|
||||
[ $# -gt 0 ] || {
|
||||
echo "usage: check-deb-depends.sh <package.deb>..." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
UNKNOWN=0
|
||||
CHECKED=0
|
||||
|
||||
# A relation list entry of the simple forms dpkg-shlibdeps prints: a bare name
|
||||
# or "name (>= version)". Anything else is compared word for word.
|
||||
SIMPLE_RE='^([a-z0-9][a-z0-9+.-]*)( \(>= ([^)]+)\))?$'
|
||||
|
||||
# Split a Depends value on commas into one trimmed entry per line.
|
||||
split_deps() {
|
||||
printf '%s\n' "$1" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | sed '/^$/d'
|
||||
}
|
||||
|
||||
check_deb() {
|
||||
local deb="$1" label tmp arch shipped derived out
|
||||
label=$(basename "$deb")
|
||||
tmp=$(mktemp -d)
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '$tmp'" RETURN
|
||||
|
||||
if ! dpkg-deb -x "$deb" "$tmp/root"; then
|
||||
echo " ERROR $label could not be unpacked" >&2
|
||||
UNKNOWN=$((UNKNOWN + 1))
|
||||
return
|
||||
fi
|
||||
arch=$(dpkg-deb -f "$deb" Architecture)
|
||||
shipped=$(dpkg-deb -f "$deb" Depends)
|
||||
if [ -z "$arch" ]; then
|
||||
echo " ERROR $label has no Architecture field" >&2
|
||||
UNKNOWN=$((UNKNOWN + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
# The same filter as check-glibc-floor.sh: executable files that parse as
|
||||
# ELF. A shared library shipped later is executable too, so it is covered.
|
||||
local bins=() f
|
||||
while IFS= read -r -d '' f; do
|
||||
readelf -hW "$f" >/dev/null 2>&1 || continue
|
||||
bins+=("$f")
|
||||
done < <(find "$tmp/root" -type f -perm -u+x -print0 | sort -z)
|
||||
if [ "${#bins[@]}" -eq 0 ]; then
|
||||
echo " ERROR $label contains no ELF objects; the layout moved and nothing was checked" >&2
|
||||
UNKNOWN=$((UNKNOWN + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
# dpkg-shlibdeps needs a debian/control in its working directory, and an
|
||||
# empty one is enough; cargo-deb sets it up the same way. DEB_HOST_ARCH is
|
||||
# set as cargo-deb sets it, so a foreign-architecture package is resolved
|
||||
# against that architecture's symbols files, not the host's.
|
||||
mkdir -p "$tmp/work/debian"
|
||||
: > "$tmp/work/debian/control"
|
||||
if ! out=$(cd "$tmp/work" && DEB_HOST_ARCH="$arch" dpkg-shlibdeps -O "${bins[@]}" 2>"$tmp/shlibdeps.err"); then
|
||||
echo " ERROR $label: dpkg-shlibdeps failed:" >&2
|
||||
sed 's/^/ /' "$tmp/shlibdeps.err" >&2
|
||||
UNKNOWN=$((UNKNOWN + 1))
|
||||
return
|
||||
fi
|
||||
derived=$(printf '%s\n' "$out" | sed -n 's/^shlibs:Depends=//p' | head -n 1)
|
||||
if [ -z "$derived" ]; then
|
||||
echo " ERROR $label: dpkg-shlibdeps derived no dependencies for ${#bins[@]} ELF object(s)" >&2
|
||||
UNKNOWN=$((UNKNOWN + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
echo " $label ($arch, ${#bins[@]} ELF objects)"
|
||||
echo " derived: $derived"
|
||||
echo " shipped: $shipped"
|
||||
|
||||
# Index the shipped list: the highest ">=" floor per name, the names
|
||||
# present at all, and every entry verbatim.
|
||||
declare -A floor=() present=() verbatim=() derivednames=()
|
||||
local e name ver
|
||||
while IFS= read -r e; do
|
||||
verbatim["$e"]=1
|
||||
[[ "$e" =~ $SIMPLE_RE ]] || continue
|
||||
name="${BASH_REMATCH[1]}"
|
||||
ver="${BASH_REMATCH[3]}"
|
||||
present["$name"]=1
|
||||
[ -n "$ver" ] || continue
|
||||
if [ -z "${floor[$name]:-}" ] || dpkg --compare-versions "$ver" gt "${floor[$name]}"; then
|
||||
floor["$name"]="$ver"
|
||||
fi
|
||||
done < <(split_deps "$shipped")
|
||||
|
||||
local bad=0 have
|
||||
while IFS= read -r e; do
|
||||
if ! [[ "$e" =~ $SIMPLE_RE ]]; then
|
||||
if [ -n "${verbatim[$e]:-}" ]; then
|
||||
echo " ok $e"
|
||||
else
|
||||
echo " FAIL $label: derived '$e' not matched by shipped Depends (missing: $shipped)" >&2
|
||||
bad=1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
name="${BASH_REMATCH[1]}"
|
||||
ver="${BASH_REMATCH[3]}"
|
||||
derivednames["$name"]=1
|
||||
if [ -z "${present[$name]:-}" ]; then
|
||||
echo " FAIL $label: derived '$e' not matched by shipped Depends (missing: $shipped)" >&2
|
||||
bad=1
|
||||
continue
|
||||
fi
|
||||
if [ -z "$ver" ]; then
|
||||
echo " ok $e"
|
||||
continue
|
||||
fi
|
||||
have="${floor[$name]:-}"
|
||||
if [ -z "$have" ]; then
|
||||
echo " FAIL $label: derived '$e' not matched by shipped Depends (below: $name with no version floor)" >&2
|
||||
bad=1
|
||||
elif dpkg --compare-versions "$have" lt "$ver"; then
|
||||
echo " FAIL $label: derived '$e' not matched by shipped Depends (below: $name (>= $have))" >&2
|
||||
bad=1
|
||||
elif dpkg --compare-versions "$have" gt "$ver"; then
|
||||
echo " FAIL $label: derived '$e' not matched by shipped Depends (above: $name (>= $have))" >&2
|
||||
bad=1
|
||||
else
|
||||
echo " ok $e"
|
||||
fi
|
||||
done < <(split_deps "$derived")
|
||||
|
||||
while IFS= read -r e; do
|
||||
if [[ "$e" =~ $SIMPLE_RE ]] && [ -n "${derivednames[${BASH_REMATCH[1]}]:-}" ]; then
|
||||
continue
|
||||
fi
|
||||
echo " hand-declared $e"
|
||||
done < <(split_deps "$shipped")
|
||||
|
||||
CHECKED=$((CHECKED + 1))
|
||||
[ "$bad" -eq 0 ] || FAILED=$((FAILED + 1))
|
||||
}
|
||||
|
||||
echo "=== Depends check (shipped Depends against dpkg-shlibdeps) ==="
|
||||
for arg in "$@"; do
|
||||
if [ ! -f "$arg" ]; then
|
||||
echo " ERROR $arg does not exist" >&2
|
||||
UNKNOWN=$((UNKNOWN + 1))
|
||||
continue
|
||||
fi
|
||||
check_deb "$arg"
|
||||
done
|
||||
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo "check-deb-depends: $FAILED of $CHECKED package(s) declare Depends that differ from what their binaries need." >&2
|
||||
echo " A missing or low entry installs where the binaries cannot run; a high one" >&2
|
||||
echo " is a hand-written floor that no longer tracks them. Fix Cargo.toml's depends." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Anything not established, or nothing examined at all, is not a pass.
|
||||
if [ "$UNKNOWN" -ne 0 ] || [ "$CHECKED" -eq 0 ]; then
|
||||
echo "check-deb-depends: could not establish a result ($UNKNOWN error(s), $CHECKED package(s) checked); refusing to report a pass." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "=== Depends check passed ($CHECKED package(s)) ==="
|
||||
Reference in New Issue
Block a user