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
10 changes: 7 additions & 3 deletions Containerfile.arch
Original file line number Diff line number Diff line change
Expand Up @@ -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/*

# ---------------------------------------------------------------------------
Expand All @@ -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/*

# ---------------------------------------------------------------------------
Expand All @@ -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/*

# ---------------------------------------------------------------------------
Expand Down
24 changes: 24 additions & 0 deletions src/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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=?<hash>` token that
Expand Down
25 changes: 20 additions & 5 deletions tests/e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
Loading