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
18 changes: 13 additions & 5 deletions build/service-scripts/boot-success.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ ident() { sed -n "s/^$1=//p" "$RUN/boot-identity" 2>/dev/null | head -1; }
other_slot() { case "$1" in a) echo b ;; b) echo a ;; esac; }
slot="$(ident slot)"; media="$(ident media)"; state="$(ident state)"
now() { date -Iseconds 2>/dev/null || date; }
# Drop FILE's clean pages, so the next read is what the device holds; best effort.
uncache() { dd if="$1" iflag=nocache count=0 status=none 2>/dev/null || :; } # uncache FILE
result() { printf '%s %s\n' "$*" "$(now)" > "$B/last-result.new" && mv -f "$B/last-result.new" "$B/last-result"; }

if [ -n "$media" ]; then
Expand Down Expand Up @@ -79,12 +81,18 @@ commit_slot() { # commit_slot <slot>: make BOOTX64.EFI this slot's kernel
if cmp -s "$src" "$dst"; then
say "BOOTX64.EFI already is slot $1"; rc=0
else
# Copy, fsync, then rename: on FAT only the rename is not atomic.
cp "$src" "$dst.new" && sync -f "$dst.new" && mv -f "$dst.new" "$dst" && sync -f "$dst" && rc=0
# Copy, fsync, compare as the device holds it, then rename: on FAT only the
# rename is not atomic, and a copy that landed wrong never becomes the boot file.
cp "$src" "$dst.new" && sync -f "$dst.new" && uncache "$dst.new" && cmp -s "$src" "$dst.new" && mv -f "$dst.new" "$dst" && sync -f "$dst" && rc=0
[ "$rc" -eq 0 ] && say "committed: BOOTX64.EFI is now slot $1"
fi
printf '%s\n' "$1" > "$ESP_MNT/kryptik/committed-slot.new" && sync -f "$ESP_MNT/kryptik/committed-slot.new" && \
mv -f "$ESP_MNT/kryptik/committed-slot.new" "$ESP_MNT/kryptik/committed-slot"
# The record is part of the commit, and follows the boot file: kryptik-update
# applies only from the slot it names, so it must not name one that does not boot.
if [ "$rc" -eq 0 ]; then
{ printf '%s\n' "$1" > "$ESP_MNT/kryptik/committed-slot.new" && sync -f "$ESP_MNT/kryptik/committed-slot.new" && \
mv -f "$ESP_MNT/kryptik/committed-slot.new" "$ESP_MNT/kryptik/committed-slot"; } \
|| { say "the committed-slot record could not be written; the trial stays, and the next boot commits again"; rc=1; }
fi
else
say "no kernel for slot $1 on the ESP"
fi
Expand Down Expand Up @@ -149,7 +157,7 @@ if [ -n "$trial" ]; then
say "$(kryptikd time committed "$B/release-$slot" 2>&1)"
else
result "commit-failed $slot"
say "slot $slot is healthy but the commit failed; the committed slot is unchanged"
say "slot $slot is healthy but the commit failed; the trial stays for the next boot"
fi
else
say "trial slot $slot came up UNHEALTHY:"
Expand Down
26 changes: 13 additions & 13 deletions docs/design/boot-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,19 @@ payloads come from [the update channel](update-channel.md) or by hand.
5. `boot-success` judges the trial slot: state persistent; eudev, seatd, the
launch daemon, the net zone and the login getty up; kryptikd finding kernel
support and the zones; an unambiguous ESP. Healthy: it copies the kernel
over `BOOTX64.EFI` (`.new`, fsync, rename), updates `committed-slot` alike,
clears the trial and forgets the entries, and the slot's kept manifest
raises the clock's floor if it is the newest. Unhealthy: it records that,
forgets the entries and reboots into the committed slot, `BootNext` being
spent. On a degraded state the trial record is out of reach, so
`committed-slot` says whether the boot is a trial. A trial that never comes
up also lands on the committed slot; the fallback records `trial.failed`
and forgets the entries, and the updater will not re-arm that payload
without `--retry`, which root gives (`su` from the administration login
on tty2; the refusal prints the command). Only a trial reboots; an
unhealthy committed slot is reported and left running, and so is a trial
on a degraded state whose ESP cannot be read, since nothing then says it
is one.
over `BOOTX64.EFI` (`.new`, fsync, compare, rename), updates
`committed-slot` alike, clears the trial and forgets the entries, and the
slot's kept manifest raises the clock's floor if it is the newest.
Unhealthy: it records that, forgets the entries and reboots into the
committed slot, `BootNext` being spent. On a degraded state the trial
record is out of reach, so `committed-slot` says whether the boot is a
trial. A trial that never comes up also lands on the committed slot; the
fallback records `trial.failed` and forgets the entries, and the updater
will not re-arm that payload without `--retry`, which root gives (`su` from
the administration login on tty2; the refusal prints the command). Only a
trial reboots; an unhealthy committed slot is reported and left running,
and so is a trial on a degraded state whose ESP cannot be read, since
nothing then says it is one.

The net zone is in the check on purpose: a release whose net zone cannot
come up could never fetch the release that fixes it. Whoever can crash
Expand Down
20 changes: 20 additions & 0 deletions tools/tests/boot-success.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ EOF
cat > "$T/bin/sync" <<'EOF'
#!/bin/sh
echo "sync $*" >> "$KTEST/syncs"
case "$*" in
*committed-slot.new) [ ! -e "$KTEST/sync_fails" ] ;;
*BOOTX64.EFI.new) [ ! -e "$KTEST/sync_fails_boot" ] ;;
esac
EOF
# cmp: the real one, but a copy of a kernel to BOOTX64.EFI.new can be made to read back wrong.
REAL_CMP="$(command -v cmp)"
cat > "$T/bin/cmp" <<EOF
#!/bin/sh
case "\$*" in *BOOTX64.EFI.new) [ -e "\$KTEST/copy_bad" ] && exit 1 ;; esac
exec "$REAL_CMP" "\$@"
EOF
chmod +x "$T"/bin/*

Expand Down Expand Up @@ -129,6 +140,15 @@ check "committed-slot records b" "$(cat "$KTEST/esp/kryptik/committed-slot")" "b
check "the new committed-slot record is fsynced under its temporary name" "$(grep -c -x "sync -f $KTEST/run/esp/kryptik/committed-slot.new" "$KTEST/syncs" 2>/dev/null)" "1"
check "the trial record is gone" "$([[ -e "$KTEST/boot/trial" ]] && echo present || echo gone)" "gone"
check "the trial's firmware entries and BootNext are forgotten after the commit, the committed slot gets its own, and its release goes to the clock's floor" "$CALLS" "mount -t vfat -o rw,nosuid,nodev,noexec /dev/vda1 $KTEST/run/esp umount $KTEST/run/esp efiboot forget efiboot ensure b kryptikd time committed $KTEST/boot/release-b "
run_case recfail b "" persistent 'b\narmed=1\n' $ALL; : > "$KTEST/sync_fails"; go
check "a committed-slot record that cannot be written fails the commit: the trial stays, for the next boot to commit again" \
"$RESULT|$(cat "$KTEST/esp/kryptik/committed-slot")|$([[ -e "$KTEST/boot/trial" ]] && echo kept || echo gone)|$(grep -c 'efiboot forget' "$KTEST/calls" 2>/dev/null)" "commit-failed b|a|kept|0"
run_case bootfail b "" persistent 'b\narmed=1\n' $ALL; : > "$KTEST/sync_fails_boot"; go
check "a boot file that cannot be replaced fails the commit, and the record goes on naming the slot BOOTX64.EFI boots" \
"$RESULT|$(cat "$KTEST/esp/EFI/BOOT/BOOTX64.EFI")|$(cat "$KTEST/esp/kryptik/committed-slot")|$([[ -e "$KTEST/boot/trial" ]] && echo kept || echo gone)" "commit-failed b|kernel-a|a|kept"
run_case copybad b "" persistent 'b\narmed=1\n' $ALL; : > "$KTEST/copy_bad"; go
check "a boot file whose copy reads back wrong is not renamed into place: the commit fails, the trial stays" \
"$RESULT|$(cat "$KTEST/esp/EFI/BOOT/BOOTX64.EFI")|$(cat "$KTEST/esp/kryptik/committed-slot")|$([[ -e "$KTEST/boot/trial" ]] && echo kept || echo gone)" "commit-failed b|kernel-a|a|kept"
run_case commit0 b "" persistent 'b\narmed=0\n' $ALL; go
check "a trial that booted before its armed=1 line was written is still a trial: committed" "$RESULT" "commit b"

Expand Down
7 changes: 7 additions & 0 deletions tools/update/kryptik-recover
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ slot_dev() { case "$1" in a) echo "$SA" ;; b) echo "$SB" ;; *) die "slot must be
has_fs() { [ "$(dd if="$1" bs=1 skip=1080 count=2 status=none | od -An -tx1 | tr -d ' \n')" = "53ef" ]; }
# Written, fsynced, renamed, as the kernels are: a power cut leaves the old file whole, not empty.
put() { printf '%s\n' "$2" > "$1.new" && sync -f "$1.new" && mv -f "$1.new" "$1"; } # put FILE LINE
# Drop FILE's clean pages, so the next read is what the device holds; best effort.
uncache() { dd if="$1" iflag=nocache count=0 status=none 2>/dev/null || :; } # uncache FILE

if [ "$STATUS" = 1 ]; then
mount -t vfat -o ro "$ESP" /run/kryptik-recover || die "cannot mount the ESP"
Expand Down Expand Up @@ -91,6 +93,8 @@ commit_slot() { # commit_slot SLOT (ESP mounted rw at /run/kryptik-recover)
has_fs "$(slot_dev "$s")" || die "slot $s holds no filesystem; use --restore-slot $s"
cp "$k" /run/kryptik-recover/EFI/BOOT/BOOTX64.EFI.new
sync -f /run/kryptik-recover/EFI/BOOT/BOOTX64.EFI.new
uncache /run/kryptik-recover/EFI/BOOT/BOOTX64.EFI.new
cmp -s "$k" /run/kryptik-recover/EFI/BOOT/BOOTX64.EFI.new || die "slot $s's kernel did not copy whole; BOOTX64.EFI is unchanged"
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
Expand Down Expand Up @@ -148,6 +152,9 @@ if [ -n "$RESTORE" ]; then
mount -t vfat -o rw "$ESP" /run/kryptik-recover || die "cannot mount the target ESP"
cp "$kdir/kryptik-$RESTORE.efi" "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi.new"
sync -f "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi.new"
uncache "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi.new"
cmp -s "$kdir/kryptik-$RESTORE.efi" "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi.new" \
|| die "the medium's kernel for slot $RESTORE did not copy whole; nothing was committed"
mv -f "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi.new" "/run/kryptik-recover/EFI/kryptik/kryptik-$RESTORE.efi"
put "/run/kryptik-recover/kryptik/version-$RESTORE" "$ver" || die "cannot record slot ${RESTORE}'s version"
commit_slot "$RESTORE"
Expand Down
16 changes: 11 additions & 5 deletions tools/update/kryptik-update
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ esp_dev() { own_part kryptik-esp; }
slot_dev() { own_part "kryptik-$1"; }
hdr() { awk -F': ' -v k="$2" '$1==k {print $2; exit}' "$1"; }

# Drop FILE's clean pages, so the next read is what the device holds; best effort.
uncache() { dd if="$1" iflag=nocache count=0 status=none 2>/dev/null || :; } # uncache FILE

mount_esp() {
dev="$(esp_dev)" || exit 1
mkdir -p "$ESP_MNT"
Expand Down Expand Up @@ -363,17 +366,20 @@ cmd_apply() {
mount_esp
mkdir -p "$ESP_MNT/EFI/kryptik" "$ESP_MNT/kryptik"
cp "$(payload_path "kryptik-$target.efi")" "$ESP_MNT/EFI/kryptik/kryptik-$target.efi.new" || die "staging the kernel on the ESP failed"
sync -f "$ESP_MNT/EFI/kryptik/kryptik-$target.efi.new"
# Check the staged kernel before it gets the name rollback and recovery use.
sync -f "$ESP_MNT/EFI/kryptik/kryptik-$target.efi.new" || die "the kernel staged on the ESP could not be flushed"
# Check the staged kernel, as the device holds it, before it gets the name
# rollback and recovery use.
uncache "$ESP_MNT/EFI/kryptik/kryptik-$target.efi.new"
want_k="$H_KA"; [ "$target" = b ] && want_k="$H_KB"
if [ "$(sha256sum "$ESP_MNT/EFI/kryptik/kryptik-$target.efi.new" | cut -c1-64)" != "$want_k" ]; then
rm -f "$ESP_MNT/EFI/kryptik/kryptik-$target.efi.new"
die "the kernel staged on the ESP does not hash to the manifest's kryptik-$target.efi"
fi
mv -f "$ESP_MNT/EFI/kryptik/kryptik-$target.efi.new" "$ESP_MNT/EFI/kryptik/kryptik-$target.efi"
printf '%s\n' "$VERSION" > "$ESP_MNT/kryptik/version-$target.new"
sync -f "$ESP_MNT/kryptik/version-$target.new"
mv -f "$ESP_MNT/kryptik/version-$target.new" "$ESP_MNT/kryptik/version-$target"
{ printf '%s\n' "$VERSION" > "$ESP_MNT/kryptik/version-$target.new" \
&& sync -f "$ESP_MNT/kryptik/version-$target.new" \
&& mv -f "$ESP_MNT/kryptik/version-$target.new" "$ESP_MNT/kryptik/version-$target"; } \
|| die "slot $target's version could not be recorded on the ESP"
sync
umount_esp
say "kernel for slot $target installed on the ESP (version $VERSION)"
Expand Down
Loading