Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/recipes/boot-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions build/service-scripts/esp-records.sh
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion build/service-scripts/installer-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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: $*"; }

Expand Down Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion docs/design/boot-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,7 +128,9 @@ payloads come from [the update channel](update-channel.md) or by hand.
`--recovery` (still signed: an authorised downgrade); every file's hash and
size, with nothing unlisted; and `root.json`'s root hash embedded in both
kernels (`grep -a -F`). Refuse while the state is degraded, another update
runs or a trial is armed.
runs or a trial is armed, and on any slot but the one the ESP names as
committed: from a slot booted from outside, the target would be the one
release known to boot.
2. Take the inactive slot's kernel and version file off the ESP, so an apply
cut short leaves nothing that `rollback` or `kryptik-recover --commit-slot`
would take. Write the slot (`dd conv=fsync`) and read it back.
Expand Down
43 changes: 43 additions & 0 deletions tools/tests/esp-records.sh
Original file line number Diff line number Diff line change
@@ -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 ]]
33 changes: 31 additions & 2 deletions tools/tests/update-esp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ 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"
/^other_slot() /p; /^unlist_slot() {/,/^}/p; /^cmd_rollback() {/,/^}/p; /^from_committed() {/,/^}/p' "$TOOL"
} > "$T/esp.sh"
for f in mount_esp umount_esp cmd_status other_slot unlist_slot cmd_rollback; do
for f in mount_esp umount_esp cmd_status other_slot unlist_slot cmd_rollback from_committed; do
grep -q "^$f() {" "$T/esp.sh" || { echo "could not extract $f from $TOOL"; exit 1; }
done
calls() { [[ ! -e "$T/calls" ]] || tr '\n' ' ' < "$T/calls"; }
Expand All @@ -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"
Expand Down Expand Up @@ -77,5 +86,25 @@ a="$(grep -n 'arm_trial "$target"' <<< "$body" | head -1 | cut -d: -f1)"
&& ok "apply forgets the slot's kept manifest before writing it, and keeps the new one after it verifies" \
|| bad "apply: kept manifest removed at line '${f}', slot written at '${w}', verified at '${v}', kept at '${k}', armed at '${a}'"

echo "-- an apply runs from the committed slot only"
echo a > "$T/esp/kryptik/committed-slot"
fresh; out="$(bash -c "source '$T/esp.sh'; from_committed a && echo GOES-ON" 2>&1)"
[[ "$out" == *GOES-ON* && "$(calls)" == "mount umount " ]] \
&& ok "from the committed slot it goes on, the ESP read and unmounted" || bad "from the committed slot: $(calls) / $out"
echo b > "$T/esp/kryptik/committed-slot"
fresh; out="$(bash -c "source '$T/esp.sh'; from_committed a && echo GOES-ON" 2>&1)"; rc=$?
[[ "$rc" -ne 0 && "$out" != *GOES-ON* && "$out" == *"slot a is running, and the ESP names b as the committed slot"* && "$(calls)" == "mount umount " ]] \
&& ok "from another slot it is refused, both named, the ESP unmounted" || bad "from another slot: rc=$rc / $(calls) / $out"
printf 'a\033[2J\n' > "$T/esp/kryptik/committed-slot"
fresh; out="$(bash -c "source '$T/esp.sh'; from_committed a && echo GOES-ON" 2>&1)"; rc=$?
[[ "$rc" -ne 0 && "$out" == *"names unknown as the committed slot"* && "$out" != *$'\033'* ]] \
&& ok "an ESP whose record is not a slot refuses it too, without printing the record" || bad "a record that is no slot: rc=$rc / $out"
echo b > "$T/esp/kryptik/committed-slot"
c="$(grep -n 'from_committed "$slot"' <<< "$body" | head -1 | cut -d: -f1)"
p="$(grep -n 'verify_payload "$dir"' <<< "$body" | head -1 | cut -d: -f1)"
[[ -n "$c" && -n "$p" && -n "$u" && "$c" -lt "$p" && "$c" -lt "$u" ]] \
&& ok "apply asks before it verifies the payload, and so before it writes" \
|| bad "apply: committed slot checked at line '${c}', payload verified at '${p}', first write at '${u}'"

printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
[[ "$FAIL" -eq 0 ]]
8 changes: 5 additions & 3 deletions tools/update/kryptik-recover
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 18 additions & 4 deletions tools/update/kryptik-update
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -291,6 +294,16 @@ cmd_check_release() {
printf 'version: %s\ncreated: %s\n' "$VERSION" "$created"
}

# An apply runs from the committed slot only. From another, booted from
# outside (boot-success reports it uncommitted), its target would be the one
# release known to boot, and a downgrade would be judged against an older one.
from_committed() { # from_committed RUNNING-SLOT
mount_esp
committed="$(esp_slot "$ESP_MNT/kryptik/committed-slot")"
umount_esp
[ "$committed" = "$1" ] || die "slot $1 is running, and the ESP names $committed as the committed slot. Boot the committed slot, or make this one committed from the install medium (kryptik-recover --commit-slot $1), then apply again"
}

# The ESP stops naming SLOT's release: its kernel and version go, so an apply
# cut short leaves nothing that rollback or kryptik-recover --commit-slot
# would take. The apply puts both back once the slot verifies.
Expand Down Expand Up @@ -327,6 +340,7 @@ cmd_apply() {
if [ "$retry" != 1 ] && [ -f "$B/trial.failed" ]; then
die "the last trial of slot $(cat "$B/trial.failed") failed to boot. To try it again, run as root: kryptik-update apply $dir --retry"
fi
from_committed "$slot"

say "running slot $slot ($(running_version)); target slot $target"
verify_payload "$dir" "$recovery"
Expand Down Expand Up @@ -395,7 +409,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"
Expand Down
Loading