From 56518157731066bed9c45197e069aef2dc1da713 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 03:06:11 -0700 Subject: [PATCH 1/2] Small fixes in the installer, the recovery tool, the hooks and CI's checks - kryptik-install checks veritysetup and the coreutils it calls before the first write; without veritysetup it failed after partitioning with a misleading "does not verify". - kryptik-recover unmounts the medium and its ESP image too when it dies mid-restore, and checks root.json's fields for form as the installer does. - check-pin-reviews: --survey or --reviews given last no longer loops forever. - artifact-manifest: one RETURN trap that removes all four temporary files and then itself; the second trap had replaced the first and stayed set. - The pre-commit hook and CI's CRLF step find scripts by shebang as well, so tools/kryptik, kryptik-update, kryptik-recover and the run files are checked. - source-notes: the bc row described a shim 72ab0b0 removed. - install-test: a blkid nothing read; stage 05 takes the same RAM-capped job count as the other stages. - decisions.md: ADR-011 says why the cookies stay, and ADR-012 gives the root image's real size. --- .github/workflows/ci.yml | 8 +++++++- build/stages/05-kernel.sh | 2 +- docs/decisions.md | 9 +++++---- tools/artifact-manifest.sh | 10 ++++------ tools/check-pin-reviews.sh | 4 ++-- tools/git-hooks/pre-commit | 10 +++++++--- tools/image/install-test.sh | 2 +- tools/install/kryptik-install.sh | 4 ++-- tools/source-notes.tsv | 1 - tools/update/kryptik-recover | 14 +++++++++++++- 10 files changed, 42 insertions(+), 22 deletions(-) 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..285e388b 100755 --- a/tools/update/kryptik-recover +++ b/tools/update/kryptik-recover @@ -46,7 +46,13 @@ 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 + mountpoint -q "$d" 2>/dev/null && umount "$d" + 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 +124,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 From b9b3f8366701a74f774fc76d571ceaed539e1b24 Mon Sep 17 00:00:00 2001 From: DevomB Date: Mon, 5 Oct 2026 08:45:07 -0700 Subject: [PATCH 2/2] kryptik-recover's cleanup goes on past a mount that will not unmount --- tools/update/kryptik-recover | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/update/kryptik-recover b/tools/update/kryptik-recover index 285e388b..b0452a4c 100755 --- a/tools/update/kryptik-recover +++ b/tools/update/kryptik-recover @@ -49,7 +49,8 @@ mkdir -p /run/kryptik-recover # 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 - mountpoint -q "$d" 2>/dev/null && umount "$d" + # 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 }