Skip to content

Commit e12c8ed

Browse files
committed
Restore signed boot files when USB boot policy changes fail
1 parent de72b64 commit e12c8ed

5 files changed

Lines changed: 320 additions & 9 deletions

File tree

‎bin/omarchy-usb-authorization-boot‎

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,11 @@ usb_authorization_rewrite_boot_cmdlines() {
328328
fi
329329

330330
mv -f "$temporary" "$config"
331-
if ! limine-enroll-config; then
331+
if ! usb_authorization_enroll_config "$config" "${config%/*}"; then
332332
mv -f "$backup" "$config"
333-
limine-enroll-config >/dev/null 2>&1 || true
333+
if ! usb_authorization_secure_boot_enabled; then
334+
limine-enroll-config >/dev/null 2>&1 || true
335+
fi
334336
echo "Restored $config because its bootloader verification data could not be updated." >&2
335337
return 1
336338
fi
@@ -391,6 +393,109 @@ usb_authorization_verify_signature() {
391393
fi
392394
}
393395

396+
# Read the same architecture and enrollment setting as the installed Limine tool.
397+
usb_authorization_limine_settings() (
398+
source /usr/lib/limine/limine-common-functions
399+
load_config || return 1
400+
printf '%s %s\n' "$(limine_efi_arch | tr '[:upper:]' '[:lower:]')" "${ENABLE_ENROLL_LIMINE_CONFIG:-no}"
401+
)
402+
403+
usb_authorization_verify_bootloader() (
404+
local config="$1" esp="$2" architecture enrollment image check digest
405+
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
406+
[[ -n $architecture ]] || return 1
407+
image="$esp/EFI/limine/limine_$architecture.efi"
408+
usb_authorization_verify_signature "$image" || return 1
409+
check=$(mktemp) || return 1
410+
trap 'rm -f "$check"' EXIT
411+
cp "$image" "$check" || return 1
412+
if [[ $enrollment == "yes" ]]; then
413+
digest=$(b2sum "$config") || return 1
414+
limine enroll-config "$check" "${digest%% *}" --quiet || return 1
415+
else
416+
limine enroll-config "$check" --reset --quiet || return 1
417+
fi
418+
# Enrollment edits fixed bytes in the executable. Reapplying the expected
419+
# value must be a no-op, including when enrollment is explicitly disabled.
420+
if ! cmp -s "$image" "$check"; then
421+
echo "Limine's signed executable does not match the configured menu enrollment." >&2
422+
return 1
423+
fi
424+
)
425+
426+
usb_authorization_enroll_config() {
427+
limine-enroll-config || return 1
428+
if usb_authorization_secure_boot_enabled; then
429+
usb_authorization_verify_bootloader "$1" "$2" || return 1
430+
fi
431+
}
432+
433+
# Kernel rebuild hooks also reset and re-sign Limine. Checkpoint the complete
434+
# boot files, not just the menu: rebuilding can overwrite images it references.
435+
# The outer process holds the real boot lock; the private mount namespace gives
436+
# nested Limine tools their own lock inode so they cannot deadlock against it.
437+
usb_authorization_boot_transaction() (
438+
local action="$1" state architecture enrollment index manifest changed=0 committed=0 restored=1
439+
local machine_id="$(</etc/machine-id)"
440+
local cache="${2:-/var/cache/boot/$machine_id}"
441+
local -a paths=("$boot_path" "$cache" "$limine_defaults" "$drop_in")
442+
exec 8>/run/lock/boot-partition.lock
443+
flock --timeout 30 8 || return 1
444+
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
445+
[[ -n $architecture ]] || return 1
446+
usb_authorization_verify_signature "$boot_path/EFI/limine/limine_$architecture.efi" || return 1
447+
state=$(mktemp -d /var/tmp/omarchy-usb-boot.XXXXXXXXXX) || return 1
448+
trap '
449+
if (( changed && ! committed )); then
450+
echo "USB boot policy failed; restoring the previous boot files and settings." >&2
451+
for ((index=0; index<${#paths[@]}; index++)); do
452+
if [[ -d $state/$index && ! -L $state/$index ]]; then
453+
mkdir -p "${paths[index]}" && cp -a "$state/$index/." "${paths[index]}/" || restored=0
454+
elif [[ -e $state/$index || -L $state/$index ]]; then
455+
cp -a --remove-destination "$state/$index" "${paths[index]}" || restored=0
456+
else
457+
rm -rf -- "${paths[index]}" || restored=0
458+
fi
459+
done
460+
# New unreferenced images are harmless, but a newly created cached backup
461+
# manifest must not reintroduce the failed policy on a later recovery.
462+
for manifest in snapshots.json snapshots.json.old; do
463+
[[ -e $state/0/$machine_id/limine_history/$manifest ]] ||
464+
rm -f "$boot_path/$machine_id/limine_history/$manifest" || restored=0
465+
[[ -e $state/1/lss/$manifest ]] || rm -f "$cache/lss/$manifest" || restored=0
466+
done
467+
sync -f "$boot_path" || restored=0
468+
fi
469+
if (( restored )); then
470+
rm -rf "$state"
471+
else
472+
echo "Boot recovery was incomplete. Preserve and restore the checkpoint at $state before rebooting." >&2
473+
fi
474+
' EXIT
475+
trap 'exit 1' HUP INT TERM
476+
for ((index=0; index<${#paths[@]}; index++)); do
477+
if [[ -d ${paths[index]} ]]; then
478+
cp -a "${paths[index]}/." "$state/$index" || return 1
479+
elif [[ -e ${paths[index]} || -L ${paths[index]} ]]; then
480+
cp -a "${paths[index]}" "$state/$index" || return 1
481+
fi
482+
done
483+
: >"$state/lock"
484+
changed=1
485+
usb_authorization_run_transaction "$state" "$action" || return 1
486+
usb_authorization_verify_bootloader "$limine_conf" "$boot_path" || return 1
487+
sync -f "$boot_path" || return 1
488+
committed=1
489+
)
490+
491+
usb_authorization_run_transaction() {
492+
unshare --mount --propagation private /bin/bash -c '
493+
mount --bind "$1/lock" /run/lock/boot-partition.lock || exit 1
494+
source "$2"
495+
usb_authorization_main "$3"
496+
' bash "$1" "${BASH_SOURCE[0]}" "$2"
497+
}
498+
394499
# Called with Limine's boot-partition lock held. Prepare signed copies of
395500
# conflicting historical UKIs before changing the selected boot policy.
396501
usb_authorization_reconcile_archived_images() (
@@ -409,7 +514,6 @@ usb_authorization_reconcile_archived_images() (
409514
for ((index=0; index<${#originals[@]}; index++)); do
410515
cp --preserve=mode,ownership "$staging/original.$index" "${originals[index]}" || true
411516
done
412-
limine-enroll-config >/dev/null 2>&1 || true
413517
fi
414518
rm -rf "$staging"
415519
' EXIT
@@ -544,7 +648,7 @@ usb_authorization_reconcile_archived_images() (
544648
for ((index=0; index<${#originals[@]}; index++)); do
545649
mv -f "$staging/updated.$index" "${originals[index]}" || return 1
546650
done
547-
limine-enroll-config || return 1
651+
usb_authorization_enroll_config "$config" "$esp" || return 1
548652
committed=1
549653
)
550654

@@ -586,11 +690,11 @@ usb_authorization_sync_and_verify() (
586690
# Verify the regenerated menu, so synchronization cannot mask stale stored
587691
# command lines with a one-time edit to limine.conf.
588692
limine-snapper-sync --no-mutex --no-hooks || return 1
589-
limine-enroll-config || return 1
693+
usb_authorization_enroll_config "$config" "$esp" || return 1
590694
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id"
591695
)
592696

593-
main() {
697+
usb_authorization_main() {
594698
local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary
595699

596700
if (( EUID != 0 )); then
@@ -680,6 +784,26 @@ main() {
680784
esac
681785
}
682786

787+
main() {
788+
if (( EUID != 0 )); then
789+
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
790+
return 1
791+
fi
792+
case "${1:-}" in
793+
enable|disable)
794+
if usb_authorization_secure_boot_enabled; then
795+
usb_authorization_boot_transaction "$1"
796+
else
797+
usb_authorization_main "$1"
798+
fi
799+
;;
800+
*)
801+
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
802+
return 2
803+
;;
804+
esac
805+
}
806+
683807
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
684808
main "$@"
685809
fi

‎manual/48-security.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ For machines whose disk unlock and recovery never depend on any USB devices, _Se
2525

2626
Older snapshots created before USBGuard was installed and enabled keep all USB disabled even after startup, including keyboards, network adapters, and storage. Trusting the devices connected now does not make them available in those snapshots. Enable boot-time protection only if you can unlock and recover the machine without any USB devices. _Remove > Security > USB at Boot_ restores the normal early-boot behavior while keeping USBGuard active after startup.
2727

28-
With Secure Boot enabled, changing USB boot protection may require access to your signing keys to update older snapshot boot images. If an image uses a custom measured-boot policy or multiple boot profiles, rebuild it with its original signing setup before retrying.
28+
With Secure Boot enabled, changing USB boot protection requires access to your signing keys and enough free space on the system drive for a temporary copy of the boot files. If signing fails, Omarchy restores the previous boot files and settings. If an image uses a custom measured-boot policy or multiple boot profiles, rebuild it with its original signing setup before retrying.
2929

3030
## Changing your passwords
3131

‎test/shell.d/usb-authorization-test.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ CONF
295295
source "$ROOT/bin/omarchy-usb-authorization-boot"
296296
limine-mkinitcpio() { return 0; }
297297
limine-enroll-config() { return 0; }
298+
# Signed bootloader enrollment and rollback have a dedicated real-PE suite.
299+
usb_authorization_verify_bootloader() { return 0; }
298300

299301
if usb_authorization_rebuild_and_verify enabled "$boot_conf" "$scratch" "$machine_id" enabled 2>/dev/null; then
300302
fail "boot authorization rejects Limine's false-success status when entries were not rebuilt"

‎test/shell.d/usb-boot-archives-test.sh‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ sbattach() {
2929
omarchy-cmd-missing() { return 1; }
3030
limine-enroll-config() { [[ ${ENROLL_FAIL:-0} != 1 ]]; }
3131
usb_authorization_secure_boot_enabled() { [[ ${SECURE_BOOT:-1} == 1 ]]; }
32+
# Bootloader enrollment/rollback is exercised with real EFI binaries in its own suite.
33+
usb_authorization_verify_bootloader() { return 0; }
3234

3335
fixture() {
3436
local embedded="$1" hash digest image_base
@@ -235,7 +237,7 @@ if [[ ${USB_TEST_MAIN:-0} == 1 ]]; then
235237
usb_authorization_enable_snapshot_setting "$limine_defaults"
236238
fi
237239
cp "$limine_defaults" "$esp/defaults.before"
238-
if (export SIGN_FAIL=1; main "$action") >"$esp/main-failed" 2>&1; then
240+
if (export SIGN_FAIL=1; usb_authorization_main "$action") >"$esp/main-failed" 2>&1; then
239241
fail "$action must stop if archive preparation fails"
240242
fi
241243
cmp "$limine_defaults" "$esp/defaults.before" || fail "$action failure must precede snapshot-setting changes"
@@ -244,7 +246,7 @@ if [[ ${USB_TEST_MAIN:-0} == 1 ]]; then
244246
else
245247
[[ $(<"$drop_in") == "$setting" ]] || fail "disable preflight failure must preserve the boot setting"
246248
fi
247-
(main "$action")
249+
(usb_authorization_main "$action")
248250
if [[ $action == enable ]]; then
249251
[[ $(<"$drop_in") == "$setting" ]] || fail "successful enable sets the boot policy"
250252
else

0 commit comments

Comments
 (0)