diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b678181e..3c62f6de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,13 @@ jobs: - name: No CRLF in scripts run: | - if grep -rlU $'\r' --include='*.sh' --include='Makefile' . ; then + # By name or by shebang: tools/kryptik and the run files have no extension. + git ls-files | while IFS= read -r f; do + case "$f" in *.sh|*.rs|*.py|Makefile|*/Makefile) ;; *) [ "$(head -c 2 "$f" 2>/dev/null)" = '#!' ] || continue ;; esac + if grep -qU $'\r' "$f"; then echo "$f"; fi + done > /tmp/crlf + if [ -s /tmp/crlf ]; then + cat /tmp/crlf echo "CRLF found - scripts will fail on Linux with 'bad interpreter'" exit 1 fi diff --git a/build/stages/05-kernel.sh b/build/stages/05-kernel.sh index 3b981a4e..c17c1258 100755 --- a/build/stages/05-kernel.sh +++ b/build/stages/05-kernel.sh @@ -19,7 +19,7 @@ export LFS_TGT # PATH stays the chroot's /usr/bin:/usr/sbin; never the /tools cross toolchain. -KRYPTIK_JOBS="${KRYPTIK_JOBS:-$(nproc)}" +KRYPTIK_JOBS="${KRYPTIK_JOBS:-$(kryptik_default_jobs)}" export MAKEFLAGS="-j${KRYPTIK_JOBS}" stage_contract "${BASH_SOURCE[0]}" "kernel-" gcc diff --git a/docs/decisions.md b/docs/decisions.md index daa88c9e..86492fc1 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -155,8 +155,9 @@ off the thread beside the kernel, since the kernel's own execution carries no cookie, and that is the leak the mitigations exist for. Closing it with SMT on means a flush on every kernel entry, which costs more than the threads give. No measurement changes which boundary the cookies leave open. The cookies -stay as defence in depth for a machine whose firmware has no SMT to turn off; -with no sibling thread online the kernel refuses the cookie (`ENODEV`) and +stay as defence in depth for when SMT is on all the same: plain `nosmt` lets +root turn it back on through `/sys/devices/system/cpu/smt/control`. With no +sibling thread online the kernel refuses the cookie (`ENODEV`) and `kryptikd status` says `no-smt`. ## ADR-012: Device firmware from linux-firmware, on the verified root @@ -190,8 +191,8 @@ it a machine runs whatever microcode its firmware last shipped, which on older machines means known, unfixed CPU vulnerabilities. **Cost:** about 135 MB after zstd (385 MB of files; Intel Wi-Fi is 56 MB and -amdgpu 38 MB compressed) on a 2.7 GB root image, and an input nobody here can -read. +amdgpu 38 MB compressed) on a root image of about 1.7 GB, and an input nobody +here can read. ## ADR-013: A driver is built in only when boot needs it diff --git a/tools/artifact-manifest.sh b/tools/artifact-manifest.sh index c2bd058a..94f3e159 100755 --- a/tools/artifact-manifest.sh +++ b/tools/artifact-manifest.sh @@ -89,13 +89,11 @@ emit_inputs() { emit_tree() { local hashes; hashes="$(mktemp)" local meta; meta="$(mktemp)" + local ferr; ferr="$(mktemp)" + local raw; raw="$(mktemp)" + # A RETURN trap outlives the function, so it clears itself. # shellcheck disable=SC2064 - trap "rm -f '$hashes' '$meta'" RETURN - - local ferr; ferr="$(mktemp)" - local raw; raw="$(mktemp)" - # shellcheck disable=SC2064 - trap "rm -f '$ferr' '$raw'" RETURN + trap "rm -f '$hashes' '$meta' '$ferr' '$raw'; trap - RETURN" RETURN # common.sh's ERR trap exits, and it fires even under set +e. local frc=0 diff --git a/tools/check-pin-reviews.sh b/tools/check-pin-reviews.sh index 80027d01..d842bb2a 100755 --- a/tools/check-pin-reviews.sh +++ b/tools/check-pin-reviews.sh @@ -14,8 +14,8 @@ set -uo pipefail SURVEY=""; REVIEWS="$(dirname "${BASH_SOURCE[0]}")/pin-reviews.tsv"; NO_HELD=0 while [[ $# -gt 0 ]]; do case "$1" in - --survey) SURVEY="${2:-}"; shift 2 ;; - --reviews) REVIEWS="${2:-}"; shift 2 ;; + --survey) SURVEY="${2:?--survey needs a file}"; shift 2 ;; + --reviews) REVIEWS="${2:?--reviews needs a file}"; shift 2 ;; --no-held) NO_HELD=1; shift ;; *) echo "unknown argument: $1" >&2; exit 2 ;; esac diff --git a/tools/git-hooks/pre-commit b/tools/git-hooks/pre-commit index 5251d2e4..3046a000 100755 --- a/tools/git-hooks/pre-commit +++ b/tools/git-hooks/pre-commit @@ -36,28 +36,32 @@ done < <(git diff --cached --name-only --diff-filter=ACM \ # build/lib/common.sh is sourced, not executed, and stays 644. +# The staged file's first line: tools/kryptik and the run files are scripts with no extension. +first_line() { git show ":$1" 2>/dev/null | head -1; } + # --- CRLF ------------------------------------------------------------------- # .gitattributes normalises line endings, but `add -f` or an editor can still # stage CRLF, which gives "bad interpreter: bash^M". while IFS= read -r f; do [[ -z "$f" ]] && continue + [[ "$f" =~ \.(sh|rs|py)$|(^|/)Makefile$ || "$(first_line "$f")" == '#!'* ]] || continue if git show ":${f}" 2>/dev/null | grep -qU $'\r'; then echo "pre-commit: ERROR ${f} contains CRLF; scripts must be LF" >&2 fail=1 fi -done < <(git diff --cached --name-only --diff-filter=ACM \ - | grep -E '\.(sh|rs)$|Makefile' || true) +done < <(git diff --cached --name-only --diff-filter=ACM) # --- shell syntax ----------------------------------------------------------- while IFS= read -r f; do [[ -z "$f" ]] && continue [[ -f "$f" ]] || continue + [[ "$f" == *.sh || "$(first_line "$f")" =~ ^#!.*[/\ ](ba|da)?sh([[:space:]]|$) ]] || continue if ! bash -n "$f" 2>/dev/null; then echo "pre-commit: ERROR ${f} has a shell syntax error" >&2 bash -n "$f" 2>&1 | sed 's/^/ /' >&2 fail=1 fi -done < <(git diff --cached --name-only --diff-filter=ACM | grep -E '\.sh$' || true) +done < <(git diff --cached --name-only --diff-filter=ACM) if [[ "$fail" -ne 0 ]]; then echo >&2 diff --git a/tools/image/install-test.sh b/tools/image/install-test.sh index 6fb22d0c..80c98911 100755 --- a/tools/image/install-test.sh +++ b/tools/image/install-test.sh @@ -77,7 +77,7 @@ deny "$P1" 'Kernel panic|Oops:' "no panic during the in # From the host side: the partition table the guest wrote. sfdisk -l "$DISK" 2>/dev/null | grep -E '^/|Disklabel' | sed 's/^/ /' if [[ "$(sfdisk -l "$DISK" 2>/dev/null | grep -c "^${DISK}")" -eq 4 ]]; then green "host sees four partitions on the target"; else red "host does not see four partitions"; fi -lbls="$(blkid -p -O 0 "$DISK" >/dev/null 2>&1; sfdisk -d "$DISK" 2>/dev/null | grep -o 'name="[^"]*"' | tr '\n' ' ')" +lbls="$(sfdisk -d "$DISK" 2>/dev/null | grep -o 'name="[^"]*"' | tr '\n' ' ')" if [[ "$lbls" == *kryptik-esp* && "$lbls" == *kryptik-a* && "$lbls" == *kryptik-b* && "$lbls" == *kryptik-state* ]]; then green "host sees the four partition labels"; else red "host labels: ${lbls}"; fi if [[ "$FAIL" -ne 0 ]]; then printf '\nphase 1 failed; not booting the result.\n%d passed, %d failed\n' "$PASS" "$FAIL"; exit 1; fi diff --git a/tools/install/kryptik-install.sh b/tools/install/kryptik-install.sh index d802adaf..acc72d27 100755 --- a/tools/install/kryptik-install.sh +++ b/tools/install/kryptik-install.sh @@ -59,8 +59,8 @@ done # --- every external tool, checked before the first write ------------------- missing="" -for tool in sfdisk partx blockdev blkid cryptsetup stty mkfs.ext4 dd sha256sum mount umount sync awk sed \ - readlink lsblk head cmp cp mkdir stat tr; do +for tool in sfdisk partx blockdev blkid cryptsetup veritysetup stty mkfs.ext4 dd sha256sum mount umount sync awk sed \ + readlink lsblk head cmp cp mkdir stat tr cut basename mv mountpoint rmdir chmod rm cat sleep; do command -v "$tool" >/dev/null 2>&1 || missing="${missing} ${tool}" done [ -z "$missing" ] || die "this system is missing:${missing} diff --git a/tools/source-notes.tsv b/tools/source-notes.tsv index c030acbe..6601d191 100644 --- a/tools/source-notes.tsv +++ b/tools/source-notes.tsv @@ -37,7 +37,6 @@ # means. # package kind location note -bc recipe-transformation build/recipes/bc.sh:s_bc s_bc() overwrites upstream's bc/fix-libmath_h with a locally authored sed script. Its output, libmath.h, is compiled into the shipped binary, so sources.lock does not fully describe what bc was built from. The recipe documents and justifies this, and verifies the result functionally (bc -l must evaluate s(0)), but the replacement file is in no lockfile. GNU bc 1.08 makes exactly this change upstream as bc/fix-libmath.sed, so moving the pin would remove the transformation rather than merely hide it. expat undesignated-signer https://libexpat.github.io/ The release signature verifies against primary key 3176EF7DB2367F1FCA4F306B1F9B0E909AF37285, which gentoo.org's Web Key Directory publishes over HTTPS for sping@gentoo.org - a route that does not depend on the signature, so this is NOT circular trust. What is missing is upstream's own statement: libexpat designates no release signer and publishes no fingerprint on its site, in SECURITY.md, or in its release notes. Verified absent on 2026-09-11. linux-firmware recipe-transformation build/recipes/firmware.sh:s_firmware The files build/config/firmware.list selects are stored zstd-compressed under /lib/firmware and the kernel decompresses them on load, so the bytes on the verified root are not the tarball's bytes (their decompressed content is). Shipping them uncompressed would remove this. libnl undesignated-signer https://github.com/thom311/libnl/releases The release tarball carries a detached .sig that verifies against 67DA3FAEBAE276BA58FC6CE314F18A98993AECD5, but libnl publishes no statement of who signs its releases: not on the release page, not in the repository, and the key is in no Web Key Directory. Checked 2026-09-19. A published fingerprint from upstream would remove this. diff --git a/tools/update/kryptik-recover b/tools/update/kryptik-recover index 944aeca3..b0452a4c 100755 --- a/tools/update/kryptik-recover +++ b/tools/update/kryptik-recover @@ -46,7 +46,14 @@ for lblname in kryptik-state kryptik-media; do done mountpoint -q /run/kryptik-recover 2>/dev/null && umount /run/kryptik-recover mkdir -p /run/kryptik-recover -cleanup() { mountpoint -q /run/kryptik-recover 2>/dev/null && umount /run/kryptik-recover; rmdir /run/kryptik-recover 2>/dev/null || true; } +# The ESP image first: it is a file on the medium. +cleanup() { + for d in /run/kryptik-recover-esp /run/kryptik-recover-media /run/kryptik-recover; do + # One that will not unmount must not keep the others mounted. + mountpoint -q "$d" 2>/dev/null && umount "$d" 2>/dev/null || true + rmdir "$d" 2>/dev/null || true + done +} trap cleanup EXIT INT TERM slot_dev() { case "$1" in a) echo "$SA" ;; b) echo "$SB" ;; *) die "slot must be a or b" ;; esac; } has_fs() { [ "$(dd if="$1" bs=1 skip=1080 count=2 status=none | od -An -tx1 | tr -d ' \n')" = "53ef" ]; } @@ -118,6 +125,12 @@ if [ -n "$RESTORE" ]; then kdir=/run/kryptik-recover-esp/EFI/kryptik ;; esac bytes="$(sed -n 's/^ "total_bytes": \([0-9]*\).*/\1/p' "$rj")"; sha="$(sed -n 's/^ "sha256": "\([0-9a-f]*\)".*/\1/p' "$rj")"; ver="$(sed -n 's/^ "version": "\([^"]*\)".*/\1/p' "$rj")" + # The medium's record, not signed material: each field's form is checked, as the installer does. + case "$bytes" in ''|*[!0-9]*) die "root.json: total_bytes is not a number" ;; esac + [ "${#bytes}" -le 15 ] || die "root.json: total_bytes is too large" + case "$sha" in *[!0-9a-f]*) die "root.json: sha256 is not a hash" ;; esac + [ "${#sha}" -eq 64 ] || die "root.json: sha256 is not a hash" + case "$ver" in ''|*[!A-Za-z0-9._-]*) die "root.json: version is not a version" ;; esac [ "$(blockdev --getsize64 "$sd")" -ge "$bytes" ] || die "slot $RESTORE is smaller than the medium's root image" # As kryptik-update does, the ESP stops naming the slot before it is # rewritten, and is unmounted meanwhile: a restore cut short leaves nothing