Report inputs the glibc floor check could not examine as unchecked

check-glibc-floor.sh counted a missing path, a file that is not an ELF
object and a .deb with no ELF executables as floor failures, so each
exited 1 with "A binary above the floor installs cleanly and then fails
to start" and the advice to rebuild in the container. Passing a release
tarball, which is the usual mistake, therefore read as a broken release
when nothing had been examined. A .deb that dpkg-deb could not unpack
killed the script under set -e with no summary, leaving later arguments
unexamined.

Those inputs are now recorded as "could not check" with the reason, and
listed at the end. A tarball's reason says to unpack it and pass its
binaries. Exit status is 1 only when a binary is above the floor (still
listing anything unchecked), 2 when anything could not be examined, and
0 only when every input was examined and passed. Neither caller branches
on the status, and both stop on 1 or 2 as before.

Two setup failures also exited 1: a missing packaging/build-floor.env
ended the run through set -e when it was sourced, and an empty
FIPS_GLIBC_FLOOR ended it through the ":?" expansion. The env file is
now read only after a readable-file check, and an empty floor is caught
by an explicit test; both exit 2 with a message saying there is no
floor to check against. The file's shellcheck source directive now
resolves from the script's directory, so shellcheck -x run from the
repository root no longer reports SC1091.

The floor check runs only at release time, so a regression in how it
reports would surface on a release day. testing/glibc-floor/test.sh
builds its inputs at run time from the host's own true executable (a
text file, a tarball holding the binary, a missing path, an unreadable
file, a corrupt .deb, a .deb holding only a script, and a .deb holding
the binary), sets the floor explicitly in every case, and asserts the
exit status, the "could not check" listing and the presence or absence
of the rebuild advice. Scratch copies of the check cover a missing
build-floor.env, an empty floor, a file that declares none, and one
whose file supplies a floor. The test exits 2 rather than passing when
readelf, dpkg-deb or a dynamic true is missing. ci-local.sh runs it
beside the Debian version check, and ci.yml's static job runs the same
script.
This commit is contained in:
Johnathan Corgan
2026-10-01 22:40:40 +00:00
parent 8b396b662d
commit 888d393f8b
4 changed files with 298 additions and 22 deletions
+5
View File
@@ -106,6 +106,11 @@ jobs:
# reports. Kept in step with ci-local.sh's run_nextest_flaky by hand.
- name: Check the flaky-test reporter against its fixtures
run: bash testing/nextest-flaky/test.sh
# The glibc floor check's cases, built from the host's own true
# executable. Not a matrix suite, so it is kept in step with
# ci-local.sh's run_glibc_floor by hand.
- name: Check the glibc floor check against its cases
run: bash testing/glibc-floor/test.sh
fmt:
name: Format check
+86 -22
View File
@@ -16,17 +16,37 @@
# Anything else is treated as a single ELF binary.
#
# Reads the floor from packaging/build-floor.env unless FIPS_GLIBC_FLOOR is set.
#
# Exit 0 = every input was examined and none is above the floor. Exit 1 = a
# binary needs a newer glibc than the floor. Exit 2 = an input could not be
# examined (missing, not an ELF object, a .deb that would not unpack or holds
# no binaries), or the check could not run at all; never treated as a pass,
# and never reported as a binary above the floor.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# A missing or empty floor means the check cannot run, so it exits 2 here
# rather than letting `set -e` or a `:?` expansion end the run with status 1,
# which callers would read as a binary above the floor.
FLOOR_ENV="$REPO_ROOT/packaging/build-floor.env"
if [ -z "${FIPS_GLIBC_FLOOR:-}" ]; then
# shellcheck source=../packaging/build-floor.env
. "$REPO_ROOT/packaging/build-floor.env"
[ -r "$FLOOR_ENV" ] || {
echo "check-glibc-floor: cannot read $FLOOR_ENV and FIPS_GLIBC_FLOOR is not set;" >&2
echo " there is no floor to check against." >&2
exit 2
}
# shellcheck source-path=SCRIPTDIR source=../packaging/build-floor.env
. "$FLOOR_ENV"
fi
FLOOR="${FIPS_GLIBC_FLOOR:?no floor declared}"
if [ -z "${FIPS_GLIBC_FLOOR:-}" ]; then
echo "check-glibc-floor: $FLOOR_ENV declares no FIPS_GLIBC_FLOOR;" >&2
echo " there is no floor to check against." >&2
exit 2
fi
FLOOR="$FIPS_GLIBC_FLOOR"
for tool in readelf dpkg dpkg-deb; do
command -v "$tool" >/dev/null 2>&1 || {
@@ -65,6 +85,18 @@ max_glibc_need() {
FAILED=0
CHECKED=0
UNCHECKED=0
UNCHECKED_LIST=()
# unchecked <label> <reason>: records an input this run could not examine. It
# is kept apart from FAILED because the remedy differs: an input that was never
# read says nothing about the floor, and the above-the-floor advice would send
# the reader to rebuild a package that may be fine.
unchecked() {
echo " UNCHECKED $1: could not check ($2)" >&2
UNCHECKED_LIST+=("$1: $2")
UNCHECKED=$((UNCHECKED + 1))
}
is_elf() { readelf -hW "$1" >/dev/null 2>&1; }
@@ -78,8 +110,12 @@ check_binary() {
CHECKED=$((CHECKED + 1))
return
fi
echo " ERROR $label is not a readable ELF object" >&2
FAILED=$((FAILED + 1))
case "$label" in
*.tar.gz|*.tgz|*.tar|*.tar.*)
unchecked "$label" "not a readable ELF object; unpack it and pass its binaries" ;;
*)
unchecked "$label" "not a readable ELF object" ;;
esac
return
fi
CHECKED=$((CHECKED + 1))
@@ -96,11 +132,14 @@ check_deb() {
tmp=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$tmp'" RETURN
dpkg-deb -x "$deb" "$tmp"
if ! dpkg-deb -x "$deb" "$tmp"; then
unchecked "$(basename "$deb")" "dpkg-deb could not unpack it"
return
fi
# A package legitimately ships executable shell scripts alongside its
# binaries -- fips-dns-setup and its teardown are two -- so filter to ELF
# objects rather than treating a script as an unreadable binary. A .deb
# with no ELF object at all is still an error: it means the glob or the
# with no ELF object at all is still not a pass: it means the glob or the
# layout moved and this check examined nothing.
local found=0 f
while IFS= read -r -d '' f; do
@@ -109,8 +148,7 @@ check_deb() {
check_binary "$f" "$(basename "$deb"):$(basename "$f")"
done < <(find "$tmp" -type f -perm -u+x -print0)
if [ "$found" -eq 0 ]; then
echo " ERROR $(basename "$deb") contains no ELF executables" >&2
FAILED=$((FAILED + 1))
unchecked "$(basename "$deb")" "contains no ELF executables"
fi
}
@@ -121,27 +159,53 @@ check_deb() {
echo "=== glibc floor check (declared floor: $FLOOR) ==="
for arg in "$@"; do
[ -e "$arg" ] || { echo " ERROR $arg does not exist" >&2; FAILED=$((FAILED + 1)); continue; }
[ -e "$arg" ] || { unchecked "$arg" "does not exist"; continue; }
case "$arg" in
*.deb) check_deb "$arg" ;;
*) check_binary "$arg" "$(basename "$arg")" ;;
esac
done
# Nothing examined is a failure, not a pass. An argument list that matched no
# binary means the caller's glob went stale, and reporting that as green is how
# a guard quietly stops guarding.
if [ "$CHECKED" -eq 0 ] && [ "$FAILED" -eq 0 ]; then
# print_unchecked: lists the inputs this run could not examine, if any.
print_unchecked() {
[ "$UNCHECKED" -gt 0 ] || return 0
echo " Not checked:" >&2
local entry
for entry in "${UNCHECKED_LIST[@]}"; do
echo " $entry" >&2
done
}
# A real floor failure is the stronger signal, so it decides the status even
# when other inputs went unchecked; those are still listed.
if [ "$FAILED" -ne 0 ]; then
echo "check-glibc-floor: $FAILED binary(ies) above the floor across $CHECKED checked, $UNCHECKED input(s) not checked." >&2
echo " A binary above the floor installs cleanly and then fails to start." >&2
echo " Build through packaging/debian/build-deb-container.sh, which pins" >&2
echo " the build image to the oldest supported distribution." >&2
print_unchecked
exit 1
fi
# An input that could not be examined is not a pass. This is also where a
# stale caller glob lands: an unexpanded pattern such as "$UNPACK"/*/fips
# arrives as a path that does not exist, and reporting that as green is how a
# guard quietly stops guarding.
if [ "$UNCHECKED" -ne 0 ]; then
if [ "$CHECKED" -eq 0 ]; then
echo "check-glibc-floor: could not check $UNCHECKED input(s) and examined no binaries." >&2
else
echo "check-glibc-floor: could not check $UNCHECKED input(s); nothing was found above the floor in the $CHECKED binaries checked." >&2
fi
print_unchecked
exit 2
fi
# Backstop: every argument raises CHECKED, FAILED or UNCHECKED, and an empty
# argument list is refused above, so this is not expected to be reached.
if [ "$CHECKED" -eq 0 ]; then
echo "check-glibc-floor: examined no binaries; refusing to report a pass." >&2
exit 2
fi
if [ "$FAILED" -ne 0 ]; then
echo "check-glibc-floor: $FAILED problem(s) across $CHECKED binaries." >&2
echo " A binary above the floor installs cleanly and then fails to start." >&2
echo " Build through packaging/debian/build-deb-container.sh, which pins" >&2
echo " the build image to the oldest supported distribution." >&2
exit 1
fi
echo "=== glibc floor check passed ($CHECKED binaries, all at or below $FLOOR) ==="
+12
View File
@@ -1637,6 +1637,17 @@ run_nextest_flaky() {
record "nextest-flaky" $rc
}
# check-glibc-floor.sh's cases: an input it cannot examine reports "could not
# check" with exit 2, and only a binary above the floor gets exit 1 and the
# rebuild advice. Static, a few seconds, and builds its inputs from the host's
# own true executable.
run_glibc_floor() {
local rc=0
info "[glibc-floor] Checking the glibc floor check against its cases"
bash "$SCRIPT_DIR/glibc-floor/test.sh" || rc=$?
record "glibc-floor" $rc
}
# ── Main ───────────────────────────────────────────────────────────────────
main() {
@@ -1662,6 +1673,7 @@ main() {
run_wait_converge
run_deb_version
run_nextest_flaky
run_glibc_floor
if [[ "$TEST_ONLY" == true ]]; then
run_tests
+195
View File
@@ -0,0 +1,195 @@
#!/bin/bash
# ── Cases for check-glibc-floor.sh ──────────────────────────────────────────
# The floor check runs at release time, on artifacts nobody re-reads, so how it
# reports matters as much as what it finds. An input it cannot examine (a
# missing path, a file that is not an ELF object such as an unextracted
# tarball, a corrupt or binary-less .deb) must come out as "could not check"
# with exit 2, never as a binary above the floor: that advice sends the reader
# to rebuild a package that was never examined. A binary that really is above
# the floor must still exit 1 with the advice, even beside unchecked inputs. A
# floor that is missing or empty means the check cannot run at all, which is
# exit 2 as well.
#
# Inputs are built at run time from the host's own true executable (the first
# one on PATH). Every case either sets FIPS_GLIBC_FLOOR or runs a scratch copy
# of the check beside a build-floor.env of its own, so nothing depends on the
# host's glibc or on the repository's packaging/build-floor.env.
#
# Exit 0 = every case behaved. Exit 1 = a case did not. Exit 2 = the cases
# could not run (no readelf, no dpkg-deb, no dynamic true); never a pass.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHECK="$SCRIPT_DIR/../check-glibc-floor.sh"
WORK="$(mktemp -d)"
trap 'chmod -R u+rwx "$WORK" 2>/dev/null; rm -rf "$WORK"' EXIT
cannot() { echo "glibc-floor cases: $*; cannot run." >&2; exit 2; }
for tool in readelf dpkg dpkg-deb tar; do
command -v "$tool" >/dev/null 2>&1 || cannot "$tool is not installed"
done
# `true` is a shell builtin, so `command -v` would print the bare word.
HOSTELF="$(type -P true)" || cannot "no true executable on PATH"
readelf -hW "$HOSTELF" >/dev/null 2>&1 || cannot "$HOSTELF is not an ELF object"
NEED="$(readelf -VW "$HOSTELF" 2>/dev/null | awk '/Version needs section/,0' \
| grep -oE 'GLIBC_[0-9.]+' | sed 's/GLIBC_//' | sort -V | tail -1)"
[ -n "$NEED" ] || cannot "$HOSTELF has no glibc version requirement"
dpkg --compare-versions "$NEED" gt 2.0 || cannot "$HOSTELF needs glibc $NEED, not above 2.0"
FAILED=0
ok() { echo " ok $*"; }
bad() { echo " FAIL $*"; FAILED=$((FAILED + 1)); }
ADVICE='installs cleanly and then fails to start'
# run_script <script> <name> <floor> <arg...>: runs the given copy of the
# check, leaving combined output in $WORK/out and the status in RC.
run_script() {
local script="$1"
CASE="$2"
local floor="$3"
shift 3
FIPS_GLIBC_FLOOR="$floor" bash "$script" "$@" > "$WORK/out" 2>&1
RC=$?
return 0
}
# run_case <name> <floor> <arg...>: runs the check under test.
run_case() { run_script "$CHECK" "$@"; }
# floor_tree <name> [env contents]: a scratch copy of the check at
# $WORK/<name>/testing, so it reads its floor from $WORK/<name>/packaging. With
# no contents there is no packaging/build-floor.env at all. Prints the copy.
floor_tree() {
local root="$WORK/$1"
mkdir -p "$root/testing"
cp "$CHECK" "$root/testing/check-glibc-floor.sh"
if [ $# -gt 1 ]; then
mkdir -p "$root/packaging"
printf '%s\n' "$2" > "$root/packaging/build-floor.env"
fi
printf '%s\n' "$root/testing/check-glibc-floor.sh"
}
# Assertions on the last run.
exit_is() {
if [ "$RC" -eq "$1" ]; then ok "$CASE: exit $1"
else bad "$CASE: exit $RC, expected $1"; sed 's/^/ | /' "$WORK/out"; fi
}
has() {
if grep -qF -- "$1" "$WORK/out"; then ok "$CASE: says '$1'"
else bad "$CASE: does not say '$1'"; fi
}
lacks() {
if grep -qF -- "$1" "$WORK/out"; then bad "$CASE: says '$1'"
else ok "$CASE: does not say '$1'"; fi
}
# unchecked_lists <name>: the name appears on an UNCHECKED line where it was
# met, and again in the "Not checked:" list at the end of the run, which is the
# part a reader of a long run sees.
unchecked_lists() {
if grep -E '^\s*UNCHECKED ' "$WORK/out" | grep -qF -- "$1"; then
ok "$CASE: reports $1 as not checked"
else
bad "$CASE: does not report $1 as not checked"
fi
if sed -n '/^ Not checked:$/,$p' "$WORK/out" | grep -qF -- "$1"; then
ok "$CASE: lists $1 at the end of the run"
else
bad "$CASE: does not list $1 at the end of the run"
fi
}
# no_advice: the above-the-floor advice is absent.
no_advice() { lacks "$ADVICE"; }
# build_deb <name> <file to place in usr/bin>
build_deb() {
local name="$1" payload="$2" root="$WORK/pkg-$1"
mkdir -p "$root/DEBIAN" "$root/usr/bin"
cp "$payload" "$root/usr/bin/"
chmod 755 "$root/usr/bin/"*
printf 'Package: %s\nVersion: 1.0\nArchitecture: all\nMaintainer: t <t@t>\nDescription: t\n' \
"$name" > "$root/DEBIAN/control"
dpkg-deb --root-owner-group -b "$root" "$WORK/$name.deb" >/dev/null 2>&1 \
|| cannot "dpkg-deb could not build a test package"
}
echo "check-glibc-floor cases ($HOSTELF needs glibc $NEED)"
printf 'not an ELF object\n' > "$WORK/notes.txt"
mkdir -p "$WORK/tarsrc" && cp "$HOSTELF" "$WORK/tarsrc/true"
tar -czf "$WORK/fips.tar.gz" -C "$WORK/tarsrc" true
printf 'not a package\n' > "$WORK/x.deb"
printf '#!/bin/sh\nexit 0\n' > "$WORK/setup-script"
build_deb scripts-only "$WORK/setup-script"
build_deb with-elf "$HOSTELF"
run_case "text file" "$NEED" "$WORK/notes.txt"
exit_is 2; has "could not check"; unchecked_lists notes.txt; no_advice
run_case "tarball" "$NEED" "$WORK/fips.tar.gz"
exit_is 2; has "unpack it"; unchecked_lists fips.tar.gz; no_advice
run_case "nonexistent path" "$NEED" "$WORK/missing-binary"
exit_is 2; has "does not exist"; unchecked_lists missing-binary; no_advice
if [ "$(id -u)" -eq 0 ]; then
echo " SKIP unreadable file: running as root, so permissions do not stop a read"
else
cp "$HOSTELF" "$WORK/locked" && chmod 000 "$WORK/locked"
run_case "unreadable file" "$NEED" "$WORK/locked"
exit_is 2; unchecked_lists locked; no_advice
fi
run_case "corrupt .deb then a good binary" "$NEED" "$WORK/x.deb" "$HOSTELF"
exit_is 2; has "could not check"; unchecked_lists x.deb; no_advice
has "ok $(basename "$HOSTELF") needs glibc $NEED"
run_case ".deb with only a script" "$NEED" "$WORK/scripts-only.deb"
exit_is 2; has "no ELF executables"; unchecked_lists scripts-only.deb; no_advice
run_case "binary at the floor" "$NEED" "$HOSTELF"
exit_is 0; has "glibc floor check passed"; lacks "could not check"; no_advice
run_case ".deb with a binary at the floor" "$NEED" "$WORK/with-elf.deb"
exit_is 0; has "glibc floor check passed"; no_advice
run_case "binary above the floor" 2.0 "$HOSTELF"
exit_is 1; has "above the declared floor 2.0"; has "$ADVICE"
run_case "binary above the floor beside a text file" 2.0 "$HOSTELF" "$WORK/notes.txt"
exit_is 1; has "$ADVICE"; unchecked_lists notes.txt
run_case "binary at the floor beside a text file" "$NEED" "$HOSTELF" "$WORK/notes.txt"
exit_is 2; has "ok $(basename "$HOSTELF") needs glibc $NEED"; unchecked_lists notes.txt
no_advice
# With FIPS_GLIBC_FLOOR empty the floor comes from packaging/build-floor.env.
# When that cannot supply one, there is nothing to check against: that is
# "could not run" (exit 2), never a pass and never a binary above the floor.
NOENV="$(floor_tree no-env)"
run_script "$NOENV" "no build-floor.env and no floor set" "" "$HOSTELF"
exit_is 2; has "no floor to check against"; lacks "glibc floor check passed"
no_advice
EMPTYENV="$(floor_tree empty-env 'FIPS_GLIBC_FLOOR=""')"
run_script "$EMPTYENV" "build-floor.env with an empty floor" "" "$HOSTELF"
exit_is 2; has "no floor to check against"; lacks "glibc floor check passed"
no_advice
NOFLOOR="$(floor_tree no-floor 'FIPS_BUILD_IMAGE="ubuntu:22.04"')"
run_script "$NOFLOOR" "build-floor.env declaring no floor" "" "$HOSTELF"
exit_is 2; has "no floor to check against"; lacks "glibc floor check passed"
no_advice
GOODENV="$(floor_tree good-env "FIPS_GLIBC_FLOOR=\"$NEED\"")"
run_script "$GOODENV" "floor read from build-floor.env" "" "$HOSTELF"
exit_is 0; has "declared floor: $NEED"; has "glibc floor check passed"
if [ "$FAILED" -ne 0 ]; then
echo "check-glibc-floor cases: $FAILED assertion(s) failed"
exit 1
fi
echo "check-glibc-floor cases: all passed"