From 50951a7bed3371ef1a2d52ade7972e7ca10524d7 Mon Sep 17 00:00:00 2001 From: DevomB Date: Wed, 7 Oct 2026 10:56:46 -0700 Subject: [PATCH 1/2] The ESP's committed-slot and version records are fsynced before they are renamed into place, and boot-success writes no record nothing reads On FAT only the rename is not atomic, so the kernels, BOOTX64.EFI and kryptik-recover's records are written to a new name, fsynced, then renamed. boot-success's committed-slot and kryptik-update's version-a/b skipped the fsync: a power cut after the rename could leave the record with a cluster that was never written, which the readers show as unknown and an apply then refuses as naming no committed slot. Both now fsync first. boot-success also copied version-X to version-committed on every commit. Nothing reads it, and kryptik-recover --commit-slot never updated it, so it could name the wrong release; it is no longer written. --- build/service-scripts/boot-success.sh | 3 +-- tools/tests/boot-success.sh | 3 ++- tools/update/kryptik-update | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build/service-scripts/boot-success.sh b/build/service-scripts/boot-success.sh index 95c30d7c..cd7c7cb9 100755 --- a/build/service-scripts/boot-success.sh +++ b/build/service-scripts/boot-success.sh @@ -83,8 +83,7 @@ commit_slot() { # commit_slot : make BOOTX64.EFI this slot's kernel cp "$src" "$dst.new" && sync -f "$dst.new" && mv -f "$dst.new" "$dst" && sync -f "$dst" && rc=0 [ "$rc" -eq 0 ] && say "committed: BOOTX64.EFI is now slot $1" fi - cp "$ESP_MNT/kryptik/version-$1" "$ESP_MNT/kryptik/version-committed" 2>/dev/null || true - printf '%s\n' "$1" > "$ESP_MNT/kryptik/committed-slot.new" && \ + 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" else say "no kernel for slot $1 on the ESP" diff --git a/tools/tests/boot-success.sh b/tools/tests/boot-success.sh index 45488d7e..898b6174 100755 --- a/tools/tests/boot-success.sh +++ b/tools/tests/boot-success.sh @@ -66,7 +66,7 @@ echo "umount $1" >> "$KTEST/calls" EOF cat > "$T/bin/sync" <<'EOF' #!/bin/sh -exit 0 +echo "sync $*" >> "$KTEST/syncs" EOF chmod +x "$T"/bin/* @@ -126,6 +126,7 @@ run_case commit b "" persistent 'b\narmed=1\n' $ALL; go check "healthy trial: committed" "$RESULT" "commit b" check "BOOTX64.EFI is now the slot b kernel" "$(cat "$KTEST/esp/EFI/BOOT/BOOTX64.EFI")" "kernel-b" 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 commit0 b "" persistent 'b\narmed=0\n' $ALL; go diff --git a/tools/update/kryptik-update b/tools/update/kryptik-update index f6cced5c..8c9390d7 100755 --- a/tools/update/kryptik-update +++ b/tools/update/kryptik-update @@ -372,6 +372,7 @@ cmd_apply() { 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" sync umount_esp From f5a15fc46fb9be1018217f468e5b0a9541ece90c Mon Sep 17 00:00:00 2001 From: DevomB Date: Wed, 7 Oct 2026 10:58:09 -0700 Subject: [PATCH 2/2] The design doc says every rename on the ESP follows a complete, fsynced copy: the version and committed-slot records are written as the kernels are It named two renames as the only non-atomic steps, and the version file and committed-slot were renamed without an fsync before them. --- docs/design/boot-and-updates.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design/boot-and-updates.md b/docs/design/boot-and-updates.md index ce27d40a..fe35d3ae 100644 --- a/docs/design/boot-and-updates.md +++ b/docs/design/boot-and-updates.md @@ -143,14 +143,14 @@ payloads come from [the update channel](update-channel.md) or by hand. cut short leaves nothing that `rollback` or `kryptik-recover --commit-slot` would take. Write the slot (`dd conv=fsync`) and read it back. 3. Put its kernel on the ESP as `.efi.new`, fsync, check it, rename; write - its version file. Keep the verified manifest and signature in + its version file the same way. Keep the verified manifest and signature in `/var/lib/kryptik/boot/release-/` for the [clock's floor](time.md). 4. Record the trial (`armed=0`), run `kryptik-efiboot set-next `, record `armed=1`, reboot. 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`, + 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 @@ -181,7 +181,7 @@ payloads come from [the update channel](update-channel.md) or by hand. who rewrites the ESP itself: the committed slot's name there is not signed. -Zone data is never written. On the FAT ESP the two renames are the only +Zone data is never written. On the FAT ESP the renames are the only non-atomic steps; each follows a complete, fsynced copy and leaves a system that boots either way. From the medium, `kryptik-recover` commits or rewrites a slot and restores the state header ([user guide](../user-guide.md)).