diff --git a/Containerfile.arch b/Containerfile.arch index 6df258f..5899086 100644 --- a/Containerfile.arch +++ b/Containerfile.arch @@ -168,7 +168,11 @@ RUN mkdir -p /rootfs/sysroot && \ # grub's module files (same code path as Ubuntu; Arch has 'grub', not 'grub2'). # --------------------------------------------------------------------------- FROM rootfs-common AS rootfs-grub -RUN pacman -r /rootfs --noconfirm -Syu grub efibootmgr +# -S --needed installs only the listed packages without a full system upgrade. +# Avoid -Syu here: upgrading would install a newer kernel in /usr/lib/modules/ +# without a matching initramfs (dracut ran in rootfs-common for the original +# kernel only), causing cfsctl prepare-boot to panic on the kernel-without-initramfs. +RUN pacman -r /rootfs --noconfirm -S --needed grub efibootmgr RUN rm -rf /rootfs/boot/* # --------------------------------------------------------------------------- @@ -177,7 +181,7 @@ RUN rm -rf /rootfs/boot/* # in rootfs-common. # --------------------------------------------------------------------------- FROM rootfs-common AS rootfs-uki -RUN pacman -r /rootfs --noconfirm -Syu systemd-ukify +RUN pacman -r /rootfs --noconfirm -S --needed systemd-ukify RUN rm -rf /rootfs/boot/* # --------------------------------------------------------------------------- @@ -186,7 +190,7 @@ RUN rm -rf /rootfs/boot/* # time. No shim needed: firmware verifies against the UEFI Signature Database. # --------------------------------------------------------------------------- FROM rootfs-common AS rootfs-uki-sb -RUN pacman -r /rootfs --noconfirm -Syu systemd-ukify sbsigntools openssl +RUN pacman -r /rootfs --noconfirm -S --needed systemd-ukify sbsigntools openssl RUN rm -rf /rootfs/boot/* # --------------------------------------------------------------------------- diff --git a/src/upgrade.rs b/src/upgrade.rs index ae762bf..99c69f3 100644 --- a/src/upgrade.rs +++ b/src/upgrade.rs @@ -116,6 +116,7 @@ pub fn run(reboot: bool) -> Result<()> { // without a writable efivarfs, e.g. container image builds). set_loader_conf_default(Path::new(EFI_ESP), &digest)?; bootctl_set_default(&digest)?; + prune_old_ukis(&digest)?; } else { patch_bls_entry(Path::new(BOOT_DIR), &digest, &image_ref)?; if !crate::install::has_grub2() { @@ -153,6 +154,29 @@ pub fn run(reboot: bool) -> Result<()> { } } +fn prune_old_ukis(new_digest: &str) -> Result<()> { + let running = current_composefs_digest(); + let keep: std::collections::HashSet<&str> = std::iter::once(new_digest) + .chain(running.as_deref()) + .collect(); + + for entry in fs::read_dir(EFI_LINUX_DIR).with_context(|| format!("reading {EFI_LINUX_DIR}"))? { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("efi") { + continue; + } + let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); + if !keep.contains(stem) { + println!("Pruning old UKI: {}", path.display()); + if let Err(e) = fs::remove_file(&path) { + eprintln!("warning: failed to prune {}: {e}", path.display()); + } + } + } + Ok(()) +} + /// Convert a Type 1 BLS entry (written by `prepare-boot` on the XBOOTLDR/`bootdir`) /// into a UKI .efi on the ESP (`esp`), where systemd-boot always scans. /// Reads the full `options` line — including the `composefs=?` token that diff --git a/tests/e2e.py b/tests/e2e.py index 0051fde..154bba3 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -439,16 +439,27 @@ def test_new_grub_entry_created(child): assert count >= 2, f"expected ≥2 BLS entries after upgrade, got {count}" -def test_new_uki_entry_created(child): - """Two or more UKI .efi files must exist after upgrade.""" +_STALE_UKI_STEM = "0" * 64 +_STALE_UKI = f"/boot/efi/EFI/Linux/{_STALE_UKI_STEM}.efi" + + +def plant_stale_uki(child): + """Plant a fake stale .efi (realistic hex digest stem) to give pruning something to delete.""" + rc, _ = run_cmd(child, f"touch {_STALE_UKI}") + assert rc == 0, "failed to plant stale UKI" + + +def test_uki_pruned(child): + """After upgrade: stale .efi gone, exactly 2 .efi files remain.""" + rc, _ = run_cmd(child, f"test ! -f {_STALE_UKI}") + assert rc == 0, "stale UKI was not pruned after upgrade" rc, out = run_cmd( child, "echo UKICOUNT:$(ls /boot/efi/EFI/Linux/*.efi 2>/dev/null | wc -l)" ) assert rc == 0, "could not count UKI entries" m = re.search(r"UKICOUNT:(\d+)", out) assert m, f"could not parse UKI count from output:\n{out!r}" - count = int(m.group(1)) - assert count >= 2, f"expected ≥2 UKI entries after upgrade, got {count}" + assert int(m.group(1)) == 2, f"expected exactly 2 UKIs after upgrade, got {out}" def test_upgraded_digest_active(child, previous_digest): @@ -541,10 +552,14 @@ def step(name, fn, *args): image_ref = ( f"docker://{REGISTRY_HOST}:{registry.port}/test-image:latest" ) + + if uki: + step("plant_stale_uki", plant_stale_uki, child) + step("switch_to_v2", test_switch, child, image_ref) if uki: - step("new_uki_entry_created", test_new_uki_entry_created, child) + step("uki_pruned", test_uki_pruned, child) else: step("new_grub_entry_created", test_new_grub_entry_created, child)