diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index c82d857f7..7934d73fe 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2351,6 +2351,35 @@ fn remove_all_except_loader_dirs(bootdir: &Dir, is_ostree: bool) -> Result<()> { Ok(()) } +/// Subdirectories of the EFI System Partition which hold the bootloader +/// installed by bootc (via bootupd or systemd-boot). +const ESP_BOOTLOADER_DIRS: &[&str] = &["EFI", "loader"]; + +/// Remove the bootloader content from the ESP, preserving everything else. +/// +/// The ESP is commonly shared with earlier boot stages or platform firmware +/// which bootc does not manage and cannot recreate. For example, on Apple +/// Silicon the Asahi installer stores the stage 2 bootloader (`m1n1/boot.bin`), +/// the device firmware payload (`vendorfw/`) and the U-Boot EFI variable store +/// (`ubootefi.var`) on the ESP; wiping them leaves the machine unbootable. +#[context("Removing bootloader content from EFI system partition")] +fn clean_esp_bootloader_dirs(efidir: &Dir) -> Result<()> { + for name in ESP_BOOTLOADER_DIRS { + let Some(subdir) = efidir + .open_dir_optional(name) + .with_context(|| format!("Opening {name}"))? + else { + continue; + }; + remove_all_in_dir_no_xdev(&subdir, false) + .with_context(|| format!("Removing directory contents: {name}"))?; + efidir + .remove_dir(name) + .with_context(|| format!("Removing directory: {name}"))?; + } + Ok(()) +} + #[context("Removing boot directory content")] fn clean_boot_directories(rootfs: &Dir, rootfs_path: &Utf8Path, is_ostree: bool) -> Result<()> { let bootdir = @@ -2365,13 +2394,14 @@ fn clean_boot_directories(rootfs: &Dir, rootfs_path: &Utf8Path, is_ostree: bool) // This should not remove /boot/efi note. remove_all_except_loader_dirs(&bootdir, is_ostree).context("Emptying /boot")?; - // TODO: we should also support not wiping the ESP. + // Only remove the bootloader content we are about to reinstall; the rest + // of the ESP may hold platform firmware we must not touch. if ARCH_USES_EFI { if let Some(efidir) = bootdir .open_dir_optional(crate::bootloader::EFI_DIR) .context("Opening /boot/efi")? { - remove_all_in_dir_no_xdev(&efidir, false).context("Emptying EFI system partition")?; + clean_esp_bootloader_dirs(&efidir)?; } } @@ -3050,6 +3080,43 @@ mod tests { Ok(()) } + // Verify that only the bootloader directories are removed from the ESP + // and that unrelated platform content is preserved. + #[test] + fn test_clean_esp_bootloader_dirs() -> Result<()> { + let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; + + // Owned by bootc (bootupd/grub and systemd-boot) + td.create_dir_all("EFI/BOOT")?; + td.write("EFI/BOOT/BOOTAA64.EFI", b"shim")?; + td.create_dir_all("EFI/fedora")?; + td.write("EFI/fedora/grub.cfg", b"cfg")?; + td.create_dir_all("loader/entries")?; + td.write("loader/entries/foo.conf", b"entry")?; + td.write("loader/loader.conf", b"loader")?; + + // Not owned by bootc: Apple Silicon (Asahi) boot chain and firmware + td.create_dir_all("m1n1")?; + td.write("m1n1/boot.bin", b"m1n1+u-boot")?; + td.create_dir_all("vendorfw")?; + td.write("vendorfw/firmware.cpio", b"firmware")?; + td.write("ubootefi.var", b"efivars")?; + + clean_esp_bootloader_dirs(&td).unwrap(); + + assert!(!td.exists("EFI")); + assert!(!td.exists("loader")); + assert_eq!(td.read("m1n1/boot.bin")?, b"m1n1+u-boot"); + assert_eq!(td.read("vendorfw/firmware.cpio")?, b"firmware"); + assert_eq!(td.read("ubootefi.var")?, b"efivars"); + + // Running against an ESP without bootloader content is a no-op + clean_esp_bootloader_dirs(&td).unwrap(); + assert_eq!(td.entries()?.count(), 3); + + Ok(()) + } + #[test] fn test_read_boot_fstab_entry() -> Result<()> { let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; diff --git a/docs/src/bootc-install.md b/docs/src/bootc-install.md index 6849c8adc..72722ec42 100644 --- a/docs/src/bootc-install.md +++ b/docs/src/bootc-install.md @@ -205,8 +205,12 @@ Set the environment variable `BOOTC_DIRECT_IO=on` to create the loopback device This is a variant of `install to-filesystem`, which maximizes convenience for using an existing Linux system, converting it into the target container image. Note that -the `/boot` (and `/boot/efi`) partitions *will be reinitialized* - so this is a -somewhat destructive operation for the existing Linux installation. +the `/boot` partition *will be reinitialized*, and the bootloader content on the +EFI System Partition (the `EFI/` and `loader/` directories) *will be replaced* - so +this is a somewhat destructive operation for the existing Linux installation. +Other content on the EFI System Partition (for example firmware or earlier boot +stages placed there by the platform, such as `m1n1/` and `vendorfw/` on Apple +Silicon systems installed with the Asahi installer) is preserved. Also, because the filesystem is reused, it's required that the target system kernel support the root storage setup already initialized.