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..4648f84 --- /dev/null +++ b/tools/test-toolchain-identity.sh @@ -0,0 +1,66 @@ +#!/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; } + # 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 +} +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 ]]