From e87a7f4483d0f38ee078bd0ee9ad104d2036335d Mon Sep 17 00:00:00 2001 From: DevomB Date: Sun, 20 Sep 2026 00:04:32 -0700 Subject: [PATCH 1/2] Stage 01 no longer aborts on its first line, and the rule that clears a sysroot has a test that runs it the way the stage does Run 35495565368 (76f954a) died three minutes in at 01-toolchain.sh:358. The toolchain identity hashes the patch sets of glibc, gcc and binutils; only glibc has one, cat exits 1 for the two globs that match nothing, 2>/dev/null hides the message and not the status, and under common.sh's errexit, pipefail and ERR trap that ended the stage before its first step. Nothing was cleared and nothing was left half done. The cat now ends in `|| true`. It reached main because the block had only been exercised lifted out of the stage in a shell without the stage's options, where the same line is harmless. So the exercise is now a suite, tools/test-toolchain-identity.sh, and it runs the block under set -Eeuo pipefail with an ERR trap: fourteen rows covering an empty tree, the same toolchain, another toolchain (cleared, stamps archived, nothing written into the sysroot), a tree with no record, a tree with the repository visible inside it (refused, and the stand-in survives), a tree with something mounted under it, and the mount test against a space, brackets, a symlink, a sibling name and an unreadable mounts file. Against the version that broke CI it fails on its first row; against this one it passes 14 of 14. CI's lint job runs every tools/test-*.sh, so the next mistake of this kind stops there. --- Makefile | 3 ++ build/stages/01-toolchain.sh | 4 +- tools/run-tests.sh | 1 + tools/test-toolchain-identity.sh | 65 ++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100755 tools/test-toolchain-identity.sh diff --git a/Makefile b/Makefile index d391b33..4c67056 100644 --- a/Makefile +++ b/Makefile @@ -552,6 +552,9 @@ test: test-harness: @"$(TOOLS)"/test-step-errexit.sh +test-toolchain-identity: + @"$(TOOLS)"/test-toolchain-identity.sh + test-hardening: @"$(TOOLS)"/test-hardening-flags.sh diff --git a/build/stages/01-toolchain.sh b/build/stages/01-toolchain.sh index e2eefbd..c35a750 100755 --- a/build/stages/01-toolchain.sh +++ b/build/stages/01-toolchain.sh @@ -355,7 +355,9 @@ preflight # is cleared, with every stamp, before the first step. The record lives with # the stamps, not in the sysroot: the root image is a copy of the sysroot. toolchain_id="$({ printf '%s\n' "$V_BINUTILS" "$V_GCC" "$V_GLIBC" "$V_LINUX" "$V_MPFR" "$V_GMP" "$V_MPC" - cat "${KRYPTIK_ROOT}"/build/patches/{glibc,gcc,binutils}-*/SHA256SUMS 2>/dev/null; } | sha256_of_stdin)" + # Only glibc has a patch set today; cat fails on the two that do not + # exist, and under errexit and pipefail that failure ended the stage. + cat "${KRYPTIK_ROOT}"/build/patches/{glibc,gcc,binutils}-*/SHA256SUMS 2>/dev/null || true; } | sha256_of_stdin)" toolchain_marker="${STAMPS}/toolchain-id" # Anything mounted under DIR? Stage 03 binds /dev, /proc and THIS REPOSITORY diff --git a/tools/run-tests.sh b/tools/run-tests.sh index fc3d751..3643bb4 100755 --- a/tools/run-tests.sh +++ b/tools/run-tests.sh @@ -29,6 +29,7 @@ STRICT=0 # here is reproduced with `make `. SUITES=( "test-harness|tools/test-step-errexit.sh" + "test-toolchain-identity|tools/test-toolchain-identity.sh" "test-hardening|tools/test-hardening-flags.sh" "test-kernel-hardening|tools/test-check-kernel-hardening.sh" "test-services|tools/test-services.sh" diff --git a/tools/test-toolchain-identity.sh b/tools/test-toolchain-identity.sh new file mode 100755 index 0000000..320c4ce --- /dev/null +++ b/tools/test-toolchain-identity.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Tests for the rule at the top of build/stages/01-toolchain.sh that clears a +# sysroot another toolchain built. The block is lifted out of the stage and run +# under the stage's own shell options and an ERR trap: run without them, an +# earlier version of this test passed a block that aborted on its first line. +# The only substitution is the path of the mounts file. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT +MOUNTS="$W/mounts"; : > "$MOUNTS" +BLOCK="$(awk '/^# --- a cross toolchain is never rebuilt/ {on=1} /^step layout/ {on=0} on' "${ROOT}/build/stages/01-toolchain.sh" | sed "s#/proc/mounts#${MOUNTS}#g")" +[[ -n "$BLOCK" ]] || { echo "the block was not found in stage 01"; exit 1; } +PASS=0; FAIL=0 +ok() { PASS=$((PASS + 1)); echo " PASS $1"; } +bad() { FAIL=$((FAIL + 1)); echo " FAIL $1"; [[ -f "$W/out" ]] && sed 's/^/ /' "$W/out"; } + +# run GCC_VERSION: the block, in a shell set up as common.sh sets the stage's. +run() { + ( set -Eeuo pipefail; trap 'echo "ERR trap at line $LINENO"; exit 8' ERR + warn() { echo "warn: $*"; }; die() { echo "die: $*"; exit 9; } + sha256_of_stdin() { sha256sum | cut -d' ' -f1; } + KRYPTIK_ROOT="$ROOT" V_BINUTILS=1 V_GCC="$1" V_GLIBC=2.40 V_LINUX=1 V_MPFR=1 V_GMP=1 V_MPC=1 + eval "$BLOCK" ) > "$W/out" 2>&1 +} +populate() { mkdir -p "$LFS/usr/include" "$LFS/usr/lib" "$LFS/kryptik" "$STAMPS"; : > "$LFS/usr/include/pthread.h"; : > "$LFS/usr/lib/old.so"; : > "$STAMPS/01-gcc-pass1"; } +# expect NAME WANT_RC cleared|kept +expect() { + local state=kept; [[ -e "$LFS/usr/lib/old.so" ]] || state=cleared + if [[ "$RC" -eq "$2" && "$state" == "$3" ]]; then ok "$1"; else bad "$1 (exit $RC, sysroot $state)"; fi +} + +LFS="$W/a/sysroot"; STAMPS="$W/a/stamps"; mkdir -p "$STAMPS" +run 14; RC=$?; [[ "$RC" -eq 0 && -s "$STAMPS/toolchain-id" ]] && ok "an empty tree gets this toolchain's record" || bad "an empty tree (exit $RC)" +populate +run 14; RC=$?; expect "the same toolchain leaves the tree alone" 0 kept +run 15; RC=$?; expect "another toolchain clears it" 0 cleared +[[ -n "$(find "$STAMPS/legacy" -name 01-gcc-pass1 2>/dev/null)" ]] && ok "and archives the stamps" || bad "the stamps were not archived" +[[ ! -e "$LFS/.kryptik-toolchain" ]] && ok "nothing is written into the sysroot, which becomes the root image" || bad "a record was written into the sysroot" + +LFS="$W/b/sysroot"; STAMPS="$W/b/stamps"; populate +run 14; RC=$?; expect "a tree with no record is cleared" 0 cleared +LFS="$W/c/sysroot"; STAMPS="$W/c/stamps"; populate; : > "$LFS/kryptik/Makefile" +run 14; RC=$?; expect "a tree with the repository visible inside it is refused" 9 kept +[[ -e "$LFS/kryptik/Makefile" ]] && ok "and what stood in for the repository survives" || bad "THE STAND-IN FOR THE REPOSITORY WAS DELETED" +LFS="$W/d/sysroot"; STAMPS="$W/d/stamps"; populate +printf 'proc %s/proc proc rw 0 0\n' "$LFS" > "$MOUNTS" +run 14; RC=$?; expect "a tree with something mounted under it is refused" 9 kept + +# The mount test alone: /proc/mounts writes a space as \040 and names the +# resolved path, and the path is not a pattern. +mkdir -p "$W/m/with space/sysroot" "$W/m/br[ack]et/sysroot" "$W/m/real/sysroot" "$W/m/sysroot-other"; ln -s "$W/m/real" "$W/m/link" +for p in "$W/m/with space/sysroot/kryptik" "$W/m/br[ack]et/sysroot/kryptik" "$W/m/real/sysroot/kryptik" "$W/m/sysroot-other/x"; do + printf '/dev/sda1 %s ext4 rw 0 0\n' "${p// /\\040}" +done > "$MOUNTS" +eval "$(printf '%s\n' "$BLOCK" | awk '/^mounted_under\(\) \{/ {on=1} on {print} on && /^}/ {exit}')" +t() { local got=clear; mounted_under "$2" && got=mounted; [[ "$got" == "$3" ]] && ok "mount test: $1" || bad "mount test: $1 (got $got)"; } +t "a space in the path" "$W/m/with space/sysroot" mounted +t "brackets in the path" "$W/m/br[ack]et/sysroot" mounted +t "a path reached through a symlink" "$W/m/link/sysroot" mounted +t "a sibling whose name starts the same" "$W/m/sysroot" clear +rm -f "$MOUNTS" +t "an unreadable mounts file means mounted" "$W/a" mounted + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +[[ "$FAIL" -eq 0 ]] From 5e3d1ac894db293786a832f98e7afa50abe06700 Mon Sep 17 00:00:00 2001 From: DevomB Date: Sun, 20 Sep 2026 00:13:25 -0700 Subject: [PATCH 2/2] The toolchain identity test tells shellcheck its variables are read through eval CI's shellcheck is newer than the development host's and reports SC2034 for the versions the test sets for the lifted block. The suite itself passed in CI, 14 of 14; only the lint step failed. --- tools/test-toolchain-identity.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/test-toolchain-identity.sh b/tools/test-toolchain-identity.sh index 320c4ce..4648f84 100755 --- a/tools/test-toolchain-identity.sh +++ b/tools/test-toolchain-identity.sh @@ -19,6 +19,7 @@ run() { ( set -Eeuo pipefail; trap 'echo "ERR trap at line $LINENO"; exit 8' ERR warn() { echo "warn: $*"; }; die() { echo "die: $*"; exit 9; } sha256_of_stdin() { sha256sum | cut -d' ' -f1; } + # shellcheck disable=SC2034 # read by the block, through eval KRYPTIK_ROOT="$ROOT" V_BINUTILS=1 V_GCC="$1" V_GLIBC=2.40 V_LINUX=1 V_MPFR=1 V_GMP=1 V_MPC=1 eval "$BLOCK" ) > "$W/out" 2>&1 }