From b666169426b55613b23a0fbd5da14bc5acffc197 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 19 Aug 2026 10:33:13 -0400 Subject: [PATCH] Update composefs-rs to v0.9.0 tag Switch from pinning a specific commit rev to pinning the v0.9.0 git tag, which is easier to track against upstream releases. Update to handle the two UKI addon kinds. Assisted-by: https://github.com/cgwalters/cgwalters#llms Signed-off-by: Colin Walters --- Cargo.lock | 51 +++++----- Cargo.toml | 2 +- crates/lib/src/bootc_composefs/boot.rs | 133 +++++++++++++++++++------ crates/lib/src/bootc_composefs/gc.rs | 5 +- 4 files changed, 133 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b99032d576..fea8a621b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -738,8 +738,8 @@ checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" [[package]] name = "composefs" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ "anyhow", "composefs-ioctls", @@ -747,12 +747,12 @@ dependencies = [ "hex", "log", "once_cell", + "openssl", "rand 0.10.2", "rustix", "serde", "serde_json", "serde_repr", - "sha2 0.11.0", "tempfile", "thiserror 2.0.19", "tokio", @@ -765,8 +765,8 @@ dependencies = [ [[package]] name = "composefs-boot" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ "anyhow", "composefs", @@ -781,8 +781,8 @@ dependencies = [ [[package]] name = "composefs-ctl" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ "anyhow", "cap-std-ext", @@ -810,8 +810,8 @@ dependencies = [ [[package]] name = "composefs-fuse" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ "anyhow", "composefs", @@ -825,8 +825,8 @@ dependencies = [ [[package]] name = "composefs-ioctls" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ "rustix", "thiserror 2.0.19", @@ -834,8 +834,8 @@ dependencies = [ [[package]] name = "composefs-oci" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ "anyhow", "async-compression", @@ -855,7 +855,6 @@ dependencies = [ "rustix", "serde", "serde_json", - "sha2 0.11.0", "tar-core", "thiserror 2.0.19", "tokio", @@ -868,21 +867,21 @@ dependencies = [ [[package]] name = "composefs-splitdirfdstream" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ + "openssl", "rand 0.10.2", "rand_pcg", "rustix", - "sha2 0.11.0", "thiserror 2.0.19", "tokio", ] [[package]] name = "composefs-storage" -version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" +version = "0.9.0" +source = "git+https://github.com/composefs/composefs-rs?tag=v0.9.0#90f30e8abbcb5af60421b6b749cbf189b2a8895e" dependencies = [ "anyhow", "base64 0.23.0", @@ -1295,7 +1294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1937,7 +1936,7 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2255,7 +2254,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2881,7 +2880,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3315,10 +3314,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3328,7 +3327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8038bc7c86..b5a4607940 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ clap_mangen = { version = "0.3.0" } # pulls), which drags in rustls-webpki/untrusted/webpki-root-certs whose # licenses aren't in our cargo-deny allow list. Re-enable once that's sorted. # See: https://github.com/bootc-dev/bootc/pull/2295 -composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "0fbc853325ff92dbb842e715611bed7ddf9fe169", default-features = false, features = [ +composefs-ctl = { git = "https://github.com/composefs/composefs-rs", tag = "v0.9.0", default-features = false, features = [ "pre-6.15", "pre-6.16", "oci", diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 9909aa0c06..aed2760f28 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -136,6 +136,23 @@ const AUTH_EXT: &str = "auth"; /// This is relative to the ESP pub(crate) const BOOTC_UKI_DIR: &str = "EFI/Linux/bootc"; +/// Directory (relative to the ESP) where systemd-stub looks for UKI addons that apply +/// to *every* UKI, as opposed to addons scoped to a single UKI (which live alongside +/// it under [`BOOTC_UKI_DIR`]). Unlike per-UKI addons, these aren't tied to a single +/// deployment, so they're neither namespaced by deployment verity nor cleaned up by GC. +/// +/// TODO: This directory is shared, unscoped machine state (any systemd-stub UKI on the +/// ESP will load whatever's here), but we currently treat it like deployment-owned +/// content: we blindly overwrite same-named files with no ownership tracking, we only +/// (re)install addons on `install` (not on upgrade, see `uki_addons` being hardcoded to +/// `None` for `BootSetupType::Upgrade` below), and GC never removes stale entries here. +/// Before recommending this feature for real use we should track which files here are +/// bootc-owned, reconcile that set on every upgrade (installing newly-selected addons, +/// removing ones we own that are no longer selected/present), and decide/document how +/// this interacts with deployment rollback (a global addon update isn't reverted by +/// rolling back to an older deployment). +pub(crate) const GLOBAL_UKI_ADDONS_DIR: &str = "loader/addons"; + #[derive(thiserror::Error, Debug)] #[error("The UKI has the wrong composefs= parameter (is '{actual}', should be '{expected}')")] pub(crate) struct UKIDigestMismatch { @@ -852,6 +869,41 @@ struct UKIInfo { boot_digest: String, } +/// Determines the directory (under `mounted_efi`) that a PE binary should be written to. +/// +/// - A UKI, or an addon scoped to a single UKI, is namespaced under [`BOOTC_UKI_DIR`] by +/// the deployment's verity digest, so it doesn't collide with other deployments. +/// - A global UKI addon applies to every UKI, so it's written to the shared +/// [`GLOBAL_UKI_ADDONS_DIR`] instead. +fn pe_output_dir( + pe_type: &PEType, + mounted_efi: &Path, + file_path: &Utf8Path, + uki_id: &Sha512HashValue, +) -> PathBuf { + if matches!(pe_type, PEType::GlobalUkiAddon) { + return mounted_efi.join(GLOBAL_UKI_ADDONS_DIR); + } + + let efi_linux_path = mounted_efi.join(BOOTC_UKI_DIR); + + match file_path.parent() { + Some(parent) if parent.as_str().ends_with(EFI_ADDON_DIR_EXT) => { + let dir_name = get_uki_addon_dir_name(&uki_id.to_hex()); + let renamed_path = parent + .parent() + .map(|p| p.join(&dir_name)) + .unwrap_or(dir_name.into()); + + efi_linux_path.join(renamed_path) + } + + Some(parent) => efi_linux_path.join(parent), + + None => efi_linux_path, + } +} + /// Writes a PortableExecutable to ESP along with any PE specific or Global addons #[context("Writing {file_path} to ESP")] fn write_pe_to_esp( @@ -934,39 +986,15 @@ fn write_pe_to_esp( }); } - let efi_linux_path = mounted_efi.as_ref().join(BOOTC_UKI_DIR); - create_dir_all(&efi_linux_path).context("Creating bootc UKI directory")?; - - let final_pe_path = match file_path.parent() { - Some(parent) => { - let renamed_path = match parent.as_str().ends_with(EFI_ADDON_DIR_EXT) { - true => { - let dir_name = get_uki_addon_dir_name(&uki_id.to_hex()); - - parent - .parent() - .map(|p| p.join(&dir_name)) - .unwrap_or(dir_name.into()) - } - - false => parent.to_path_buf(), - }; - - let full_path = efi_linux_path.join(renamed_path); - create_dir_all(&full_path)?; - - full_path - } - - None => efi_linux_path, - }; + let final_pe_path = pe_output_dir(&pe_type, mounted_efi.as_ref(), file_path, uki_id); + create_dir_all(&final_pe_path).with_context(|| format!("Creating {final_pe_path:?}"))?; let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority()) .with_context(|| format!("Opening {final_pe_path:?}"))?; let pe_name = match pe_type { PEType::Uki => &get_uki_name(&uki_id.to_hex()), - PEType::UkiAddon => file_path + PEType::UkiAddon | PEType::GlobalUkiAddon => file_path .components() .last() .ok_or_else(|| anyhow::anyhow!("Failed to get UKI Addon file name"))? @@ -1182,6 +1210,10 @@ pub(crate) fn setup_composefs_uki_boot( esp_dev.path(), bootloader, booted_cfs.cmdline.allow_missing_fsverity, + // TODO: We never (re)install UKI addons on upgrade, only on initial + // `install`. This is especially relevant for global addons (see the + // TODO on `GLOBAL_UKI_ADDONS_DIR`): if a newer image changes or drops + // one, the ESP copy is never reconciled. None, ) } @@ -1199,8 +1231,9 @@ pub(crate) fn setup_composefs_uki_boot( } ComposefsBootEntry::Type2(entry) => { - // If --uki-addon is not passed, we don't install any addon - if matches!(entry.pe_type, PEType::UkiAddon) { + // If --uki-addon is not passed, we don't install any addon (whether + // it's scoped to this UKI or a global one) + if matches!(entry.pe_type, PEType::UkiAddon | PEType::GlobalUkiAddon) { let Some(addons) = uki_addons else { continue; }; @@ -1703,6 +1736,48 @@ pub(crate) async fn setup_composefs_boot( mod tests { use super::*; + #[test] + fn test_pe_output_dir() { + let mounted_efi = Path::new("/esp"); + let uki_id = Sha512HashValue::EMPTY; + let uki_hex = uki_id.to_hex(); + + // Boot entry paths are relative to the directory they were discovered in + // (e.g. "/boot/EFI/Linux" or "/boot/loader/addons"), not absolute filesystem paths. + + // A UKI itself always lands directly in BOOTC_UKI_DIR. + assert_eq!( + pe_output_dir(&PEType::Uki, mounted_efi, Utf8Path::new("foo.efi"), &uki_id), + mounted_efi.join(BOOTC_UKI_DIR) + ); + + // A per-UKI addon (nested under a `.efi.extra.d` directory) gets + // renamed into a directory namespaced by the UKI's verity digest. + assert_eq!( + pe_output_dir( + &PEType::UkiAddon, + mounted_efi, + Utf8Path::new("foo.efi.extra.d/bar.addon.efi"), + &uki_id + ), + mounted_efi + .join(BOOTC_UKI_DIR) + .join(get_uki_addon_dir_name(&uki_hex)) + ); + + // A global UKI addon is written to the shared addons directory, not + // namespaced by any particular UKI's verity digest. + assert_eq!( + pe_output_dir( + &PEType::GlobalUkiAddon, + mounted_efi, + Utf8Path::new("bar.addon.efi"), + &uki_id + ), + mounted_efi.join(GLOBAL_UKI_ADDONS_DIR) + ); + } + #[test] fn test_type1_filename_generation() { // Test basic os_id without hyphens diff --git a/crates/lib/src/bootc_composefs/gc.rs b/crates/lib/src/bootc_composefs/gc.rs index 5856035e6f..5c1b5bf275 100644 --- a/crates/lib/src/bootc_composefs/gc.rs +++ b/crates/lib/src/bootc_composefs/gc.rs @@ -138,8 +138,9 @@ fn delete_kernel_initrd(storage: &Storage, dir_to_delete: &str, dry_run: bool) - fn delete_uki(storage: &Storage, uki_id: &str, dry_run: bool) -> Result<()> { let esp_mnt = storage.require_esp()?; - // NOTE: We don't delete global addons here - // Which is fine as global addons don't belong to any single deployment + // NOTE: We don't delete global addons here (see `GLOBAL_UKI_ADDONS_DIR`) + // Which is fine as global addons don't belong to any single deployment, but it also + // means they're never cleaned up at all: see the TODO on `GLOBAL_UKI_ADDONS_DIR`. let uki_dir = esp_mnt.fd.open_dir(BOOTC_UKI_DIR)?; for entry in uki_dir.entries_utf8()? {