From b7565387eea8f7b072f82e5d5043354b9ec4446e Mon Sep 17 00:00:00 2001 From: DevomB Date: Tue, 6 Oct 2026 21:30:27 -0700 Subject: [PATCH] The ESP's records reach a terminal and update.log only in the shape Kryptik writes them: kryptik-update status and rollback and kryptik-recover printed committed-slot and version-a/b as found, so whoever held the disk could put terminal sequences on root's screen or a false version in status build/service-scripts/esp-records.sh reads them for every tool that shows them: esp_slot gives a or b, esp_version a release's version (MAJOR.MINOR.PATCH as release-keys.sh numbers a release, then a development build's dated parts), each from a bounded first line, and unknown otherwise; a missing record reads as the caller says. kryptik-update status and rollback, kryptik-recover --status and its commit message, and the medium's install verify line use them; boot-check lists the file. boot-success's read of committed-slot is left as #226 has it, a or b or nothing. tools/tests/esp-records.sh covers the shapes (terminal sequences, words after a version, a second line, overlong, empty, absent); tools/tests/update-esp.sh runs the tool's own cmd_status on rewritten records. --- build/recipes/boot-check.sh | 1 + build/service-scripts/esp-records.sh | 31 +++++++++++++++++++ build/service-scripts/installer-run.sh | 3 +- docs/design/boot-and-updates.md | 6 ++++ tools/tests/esp-records.sh | 43 ++++++++++++++++++++++++++ tools/tests/update-esp.sh | 9 ++++++ tools/update/kryptik-recover | 8 +++-- tools/update/kryptik-update | 11 ++++--- 8 files changed, 104 insertions(+), 8 deletions(-) create mode 100755 build/service-scripts/esp-records.sh create mode 100755 tools/tests/esp-records.sh diff --git a/build/recipes/boot-check.sh b/build/recipes/boot-check.sh index 8c19c9dd..03743013 100644 --- a/build/recipes/boot-check.sh +++ b/build/recipes/boot-check.sh @@ -88,6 +88,7 @@ s_boot_check() { chk "zone definitions" /usr/lib/kryptik/zones/work.toml chk "zone policies" /usr/lib/kryptik/zones/policy/work.seccomp chk "device helper" /usr/libexec/kryptik/devices.sh x + chk "ESP record reader" /usr/libexec/kryptik/esp-records.sh chk "boot scripts" /usr/libexec/kryptik/sysinit.sh x chk "test control helper" /usr/libexec/kryptik/testctl.sh chk "keyboard helper" /usr/libexec/kryptik/keyboard.sh diff --git a/build/service-scripts/esp-records.sh b/build/service-scripts/esp-records.sh new file mode 100755 index 00000000..0e120756 --- /dev/null +++ b/build/service-scripts/esp-records.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# The records Kryptik keeps on an ESP (kryptik/committed-slot, +# kryptik/version-a and version-b), read for a terminal or a log. An ESP is +# plain FAT that anyone holding the disk can rewrite, so a record is shown +# only in the shape Kryptik writes it; anything else reads as unknown. +# POSIX sh, sourced. +# esp_slot FILE [ABSENT] a or b +# esp_version FILE [ABSENT] a release's version +# A missing FILE reads as ABSENT (default unknown). + +esp_slot() { + [ -e "$1" ] || { echo "${2:-unknown}"; return 0; } + case "$(head -c 8 "$1" 2>/dev/null | sed -n 1p)" in + a) echo a ;; + b) echo b ;; + *) echo unknown ;; + esac +} + +# MAJOR.MINOR.PATCH as a release is numbered (build/lib/release-keys.sh), +# then the parts a development build adds: 0.1.20261007.1a2b3c4d.1 +esp_version() { + [ -e "$1" ] || { echo "${2:-unknown}"; return 0; } + ev="$(head -c 80 "$1" 2>/dev/null | sed -n 1p)" + if [ "${#ev}" -le 64 ] && printf '%s\n' "$ev" \ + | LC_ALL=C grep -E -x -q '(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.[0-9a-z]+)*'; then + printf '%s\n' "$ev" + else + echo unknown + fi +} diff --git a/build/service-scripts/installer-run.sh b/build/service-scripts/installer-run.sh index 4634564b..518b6238 100755 --- a/build/service-scripts/installer-run.sh +++ b/build/service-scripts/installer-run.sh @@ -3,6 +3,7 @@ # whose test control disk asks for it (testctl.sh). Users run kryptik-install. set -u . /usr/libexec/kryptik/testctl.sh +. /usr/libexec/kryptik/esp-records.sh say() { echo "KRYPTIK_INSTALL: $*"; } @@ -111,7 +112,7 @@ if [ "$rc" -eq 0 ]; then if [ -n "$esp" ] && mount -t vfat -o ro "$esp" /run/verify 2>/dev/null; then say "verify: esp_files=$(cd /run/verify && find . -type f | sort | tr '\n' ' ')" say "verify: bootx64_sha256=$(sha256sum /run/verify/EFI/BOOT/BOOTX64.EFI 2>/dev/null | cut -c1-64)" - say "verify: version_a=$(cat /run/verify/kryptik/version-a 2>/dev/null || echo none)" + say "verify: version_a=$(esp_version /run/verify/kryptik/version-a none)" umount /run/verify else say "verify: could not mount the ESP read-only" diff --git a/docs/design/boot-and-updates.md b/docs/design/boot-and-updates.md index 75b1f03a..bf084637 100644 --- a/docs/design/boot-and-updates.md +++ b/docs/design/boot-and-updates.md @@ -21,6 +21,12 @@ Partitions are found by GPT label, never by device name. | 3 | `kryptik-b` | slot B, empty after install | | 4 | `kryptik-state` | LUKS2 with ext4 inside, rest of disk: `/var`, the `/etc` upper layer, `/home`, zone volumes, update staging | +Nothing on the ESP is signed but the kernels. Its records reach a terminal +and `update.log` (`kryptik-update status` and `rollback`, +`kryptik-recover --status`) only in the shape Kryptik writes them, a slot +`a` or `b` and a release's version (`build/service-scripts/esp-records.sh`); +anything else reads as `unknown`. + The USB image holds `kryptik-esp` and `kryptik-media`; the ISO holds the ESP as its El Torito image and the root image at a sector offset (a `linear` dm target over `/dev/sr0`, verity on top). The root image is byte-identical on diff --git a/tools/tests/esp-records.sh b/tools/tests/esp-records.sh new file mode 100755 index 00000000..6219b5a3 --- /dev/null +++ b/tools/tests/esp-records.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# The ESP's records as the tools show them (build/service-scripts/esp-records.sh): +# in the shape Kryptik writes them, and as unknown otherwise. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=/dev/null +. "$ROOT/build/service-scripts/esp-records.sh" +PASS=0; FAIL=0 +check() { + if [[ "$2" == "$3" ]]; then printf ' PASS %s\n' "$1"; PASS=$((PASS + 1)) + else printf ' FAIL %s (got %q, want %q)\n' "$1" "$2" "$3"; FAIL=$((FAIL + 1)); fi +} +T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT +rec() { printf '%b' "$1" > "$T/rec"; } # rec BYTES: the record, backslash escapes read + +echo "-- committed-slot" +rec 'a\n'; check "slot a" "$(esp_slot "$T/rec")" a +rec 'b'; check "slot b, written without a newline" "$(esp_slot "$T/rec")" b +rec 'c\n'; check "a letter that is no slot" "$(esp_slot "$T/rec")" unknown +rec '\033]0;owned\007a\n'; check "a terminal sequence before the slot" "$(esp_slot "$T/rec")" unknown +rec 'a\r\n'; check "a carriage return after it" "$(esp_slot "$T/rec")" unknown +rec 'a; reboot\n'; check "more on the line" "$(esp_slot "$T/rec")" unknown +rec ''; check "an empty record" "$(esp_slot "$T/rec")" unknown +check "no record" "$(esp_slot "$T/absent")" unknown +check "no record, read as the caller says" "$(esp_slot "$T/absent" none)" none + +echo "-- version-a and version-b" +rec '1.0.0\n'; check "a release" "$(esp_version "$T/rec")" 1.0.0 +rec '1.0.10'; check "a release, written without a newline" "$(esp_version "$T/rec")" 1.0.10 +rec '0.1.20261007.1a2b3c4d\n'; check "a development build" "$(esp_version "$T/rec")" 0.1.20261007.1a2b3c4d +rec '0.1.20261007.1a2b3c4d.1\n'; check "the update suite's second build" "$(esp_version "$T/rec")" 0.1.20261007.1a2b3c4d.1 +rec '1.0\n'; check "two parts" "$(esp_version "$T/rec")" unknown +rec '01.0.0\n'; check "a leading zero" "$(esp_version "$T/rec")" unknown +rec '9.9.9 (newest)\n'; check "words after a version" "$(esp_version "$T/rec")" unknown +rec '1.0.0\033[2J\n'; check "a terminal sequence after a version" "$(esp_version "$T/rec")" unknown +rec '\n1.0.0\n'; check "a version on the second line" "$(esp_version "$T/rec")" unknown +rec "$(printf '1.%.0s' $(seq 40))0\n"; check "a version longer than any release's" "$(esp_version "$T/rec")" unknown +rec ''; check "an empty record" "$(esp_version "$T/rec")" unknown +check "no record" "$(esp_version "$T/absent")" unknown +check "no record, read as the caller says" "$(esp_version "$T/absent" none)" none + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +[[ "$FAIL" -eq 0 ]] diff --git a/tools/tests/update-esp.sh b/tools/tests/update-esp.sh index 48a233d9..74dd6dec 100755 --- a/tools/tests/update-esp.sh +++ b/tools/tests/update-esp.sh @@ -25,6 +25,7 @@ echo b > "$T/esp/kryptik/committed-slot" echo 'sync() { :; }' echo 'running_slot() { echo a; }; running_version() { echo 1.0; }; kryptik-efiboot() { :; }' echo "ESP_MNT='$T/esp'; LOCK='$T/lock'; B='$T/boot'; DEGRADED='$T/degraded'" + echo ". '$ROOT/build/service-scripts/esp-records.sh'" sed -n '/^mount_esp() {/,/^}/p; /^umount_esp() {/,/^}/p; /^ESP_MINE=/p; /^trap .*umount_esp/p; /^cmd_status() {/,/^}/p; /^other_slot() /p; /^unlist_slot() {/,/^}/p; /^cmd_rollback() {/,/^}/p' "$TOOL" } > "$T/esp.sh" @@ -48,6 +49,14 @@ sleep 0.5 out="$(bash -c "source '$T/esp.sh'; cmd_status" 2>&1)" wait "$holder" [[ "$out" == *"being applied"* && -z "$(calls)" ]] && ok "status leaves the ESP alone while an apply holds the lock" || bad "status under an apply: $(calls) / $out" +# Records rewritten by whoever held the disk: shown as unknown, never as written. +printf '\033]0;owned\007b\n' > "$T/esp/kryptik/committed-slot" +printf '9.9.9 (newest)\n' > "$T/esp/kryptik/version-a"; printf '1.0.0\n' > "$T/esp/kryptik/version-b" +fresh; out="$(bash -c "source '$T/esp.sh'; cmd_status" 2>&1)" +[[ "$out" == *"committed slot: unknown"* && "$out" == *"slot a: version unknown"* \ + && "$out" == *"slot b: version 1.0.0"* && "$out" != *$'\033'* ]] \ + && ok "status shows the ESP's records only in the shape Kryptik writes them" || bad "status of rewritten records: $out" +echo b > "$T/esp/kryptik/committed-slot"; rm -f "$T/esp/kryptik/version-a" "$T/esp/kryptik/version-b" echo "-- an apply cut short while it writes a slot" mkdir -p "$T/esp/EFI/kryptik" diff --git a/tools/update/kryptik-recover b/tools/update/kryptik-recover index 73bccf5c..4ac0e2d8 100755 --- a/tools/update/kryptik-recover +++ b/tools/update/kryptik-recover @@ -44,6 +44,8 @@ for lblname in kryptik-state kryptik-media; do case "$(readlink -f "$d")" in "$DISK"*) [ "$lblname" = kryptik-state ] && [ "$d" = "$ST" ] && continue; die "${DISK} is the medium; refusing" ;; esac done done +# The disk's ESP records are shown only in the shape Kryptik writes them. +. /usr/libexec/kryptik/esp-records.sh 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; } @@ -57,10 +59,10 @@ put() { printf '%s\n' "$2" > "$1.new" && sync -f "$1.new" && mv -f "$1.new" "$1" if [ "$STATUS" = 1 ]; then mount -t vfat -o ro "$ESP" /run/kryptik-recover || die "cannot mount the ESP" echo "disk $DISK" - echo "committed slot $(cat /run/kryptik-recover/kryptik/committed-slot 2>/dev/null || echo unknown)" + echo "committed slot $(esp_slot /run/kryptik-recover/kryptik/committed-slot)" for s in a b; do printf 'slot %s version %s, kernel %s, filesystem %s\n' "$s" \ - "$(cat "/run/kryptik-recover/kryptik/version-$s" 2>/dev/null || echo none)" \ + "$(esp_version "/run/kryptik-recover/kryptik/version-$s" none)" \ "$([ -f "/run/kryptik-recover/EFI/kryptik/kryptik-$s.efi" ] && echo present || echo absent)" \ "$(has_fs "$(slot_dev "$s")" && echo present || echo NONE)" done @@ -88,7 +90,7 @@ commit_slot() { # commit_slot SLOT (ESP mounted rw at /run/kryptik-recover) mv -f /run/kryptik-recover/EFI/BOOT/BOOTX64.EFI.new /run/kryptik-recover/EFI/BOOT/BOOTX64.EFI put /run/kryptik-recover/kryptik/committed-slot "$s" || die "cannot record slot $s as committed" sync - say "committed: BOOTX64.EFI is now slot $s ($(cat "/run/kryptik-recover/kryptik/version-$s" 2>/dev/null || echo unknown))" + say "committed: BOOTX64.EFI is now slot $s ($(esp_version "/run/kryptik-recover/kryptik/version-$s"))" # As boot-success does at each commit: Kryptik's firmware entries and # BootNext go, so the firmware boots BOOTX64.EFI, and the committed slot # gets its own entry on this disk's ESP, a second way to it. diff --git a/tools/update/kryptik-update b/tools/update/kryptik-update index c35dfb6d..b2f88385 100755 --- a/tools/update/kryptik-update +++ b/tools/update/kryptik-update @@ -56,7 +56,10 @@ case "${1:-}" in check-manifest|check-pointer|check-release) ;; *) # shellcheck source=/dev/null - . /usr/libexec/kryptik/devices.sh ;; + . /usr/libexec/kryptik/devices.sh + # The ESP's records, shown only in the shape Kryptik writes them. + # shellcheck source=/dev/null + . /usr/libexec/kryptik/esp-records.sh ;; esac running_slot() { sed -n 's/^slot=//p' /run/kryptik/boot-identity 2>/dev/null; } @@ -100,9 +103,9 @@ cmd_status() { exec 9>"$LOCK" if flock -s -n 9; then mount_esp - echo "committed slot: $(cat "$ESP_MNT/kryptik/committed-slot" 2>/dev/null || echo unknown)" + echo "committed slot: $(esp_slot "$ESP_MNT/kryptik/committed-slot")" for s in a b; do - printf 'slot %s: version %s, kernel %s\n' "$s" "$(cat "$ESP_MNT/kryptik/version-$s" 2>/dev/null || echo none)" \ + printf 'slot %s: version %s, kernel %s\n' "$s" "$(esp_version "$ESP_MNT/kryptik/version-$s" none)" \ "$([ -f "$ESP_MNT/EFI/kryptik/kryptik-$s.efi" ] && echo present || echo absent)" done umount_esp @@ -395,7 +398,7 @@ cmd_rollback() { [ -n "$target" ] || die "cannot tell which slot is running" mount_esp [ -f "$ESP_MNT/EFI/kryptik/kryptik-$target.efi" ] || die "slot $target has no kernel on the ESP; nothing to roll back to" - ver="$(cat "$ESP_MNT/kryptik/version-$target" 2>/dev/null || echo unknown)" + ver="$(esp_version "$ESP_MNT/kryptik/version-$target")" umount_esp tdev="$(slot_dev "$target")" || exit 1 [ -b "$tdev" ] || die "kryptik-$target ($tdev) is not a block device"