testsAndMisc/linux_configuration/scripts/boot_recovery/install.sh
Krzysztof Rudnicki 8e24ff093f Add boot-repair: offline recovery for a stale EFI System Partition
On 2026-07-30 a kernel upgrade left the machine in systemd emergency mode
on three consecutive boots. The upgrade replaced /usr/lib/modules/<old>
with <new>, but the new kernel never reached the ESP, so the firmware kept
booting the OLD vmlinuz whose module tree had just been deleted. Nothing
modular could load after pivot-root — including vfat, which meant /boot
itself could not be mounted, local-fs.target failed, and systemd dropped to
an emergency shell. pacman reported the transaction as successful.

Root cause: pacman-git with ParallelDownloads > 1 segfaults every forked
alpm hook child, silently skipping 90-mkinitcpio-install and depmod.

boot-repair is self-contained (no repo libs — it must run in the emergency
shell) and fully offline (missing modules are recovered from
/var/cache/pacman/pkg). It repairs by default; --dry-run inspects.

It detects and fixes: read-only root, noauto/nofail fstab drift, orphaned
kernel files shadowing an unmounted ESP, unloadable vfat, unmounted ESP,
missing modules.dep, ESP kernel != installed tree, stale initramfs, and
ParallelDownloads > 1.

Two pacman hooks make the failure impossible rather than merely
recoverable: 05-boot-mounted-guard aborts a kernel transaction when the
ESP is unmounted (AbortOnFail), and 99-boot-autorepair fixes ESP drift
after the transaction, before the machine is ever rebooted.

Safety: a module tree counts as usable only if it has BOTH vmlinuz and
kernel/, so the metadata-only wreckage this bug leaves — which sorts
newer — is never chosen as the target. Files under /boot are deleted only
while it is not a mountpoint AND contains no EFI/ or loader/; deleting
after mounting would destroy the real kernel.

Verified:
- 31/31 fixture tests (--root against throwaway trees, no privileges)
- 11/11 reversible live tests on the real machine: unmounted the real ESP,
  planted shadow files, confirmed they were deleted and the ESP then
  mounted with EFI/, loader/ and kernel 7.1.5-arch1-2 intact
- Gate proven against a real transaction with the ESP unmounted:
  "failed to commit transaction", no packages upgraded, nothing written
- shellcheck + shfmt clean, pre-commit passing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:48:56 +02:00

80 lines
2.7 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Install boot-repair and its pacman gate hooks.
# ============================================================================
#
# Installs:
# /usr/local/sbin/boot-repair the repair tool
# /etc/pacman.d/hooks/05-boot-mounted-guard.hook pre-transaction gate
# /etc/pacman.d/hooks/99-boot-autorepair.hook post-transaction fix
#
# boot-repair must live on the ROOT filesystem: by definition the ESP is
# unreachable when it is needed.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
readonly SCRIPT_DIR
readonly SBIN_DEST="/usr/local/sbin/boot-repair"
readonly HOOK_DIR="/etc/pacman.d/hooks"
if [[ $EUID -ne 0 ]]; then
echo "This installer needs root; re-running with sudo..."
exec sudo "$0" "$@"
fi
echo "=== boot-repair installer ==="
# 1. Dependencies. Everything used is in `base`, so this is a verification
# rather than an install — but it must fail loudly if something is absent,
# since the tool's whole point is working when the system is broken.
echo "[1/4] Checking required tools..."
missing=()
for tool in findmnt depmod modprobe tar install awk sed grep find sort; do
command -v "$tool" >/dev/null 2>&1 || missing+=("$tool")
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo " Missing: ${missing[*]}"
echo " Installing base utilities..."
pacman -S --needed --noconfirm util-linux kmod tar coreutils gawk sed grep findutils
fi
# mkinitcpio is required to rebuild the initramfs.
if ! command -v mkinitcpio >/dev/null 2>&1; then
echo " Installing mkinitcpio..."
pacman -S --needed --noconfirm mkinitcpio
fi
echo " All required tools present."
# 2. The script itself.
echo "[2/4] Installing $SBIN_DEST..."
install -Dm755 "$SCRIPT_DIR/boot-repair" "$SBIN_DEST"
echo " Installed."
# 3. Pacman hooks.
echo "[3/4] Installing pacman hooks into $HOOK_DIR..."
mkdir -p "$HOOK_DIR"
for hook in "$SCRIPT_DIR"/hooks/*.hook; do
install -Dm644 "$hook" "$HOOK_DIR/$(basename "$hook")"
echo " $(basename "$hook")"
done
# 4. Prove the installed copy runs. A recovery tool that was never executed is
# not installed, it is merely copied.
echo "[4/4] Verifying the installed copy..."
if "$SBIN_DEST" --dry-run; then
echo " boot-repair reports the system is consistent."
else
echo " boot-repair found problems (see above). Repair with: sudo $SBIN_DEST"
fi
echo "=== Installation complete ==="
echo
echo "Usage:"
echo " sudo boot-repair # repair now"
echo " sudo boot-repair --dry-run # report only"
echo
echo "In emergency mode the root filesystem is already mounted, so:"
echo " $SBIN_DEST"