diff --git a/Cargo.lock b/Cargo.lock index 790ee23a02..d6f22e9f62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,9 +192,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" [[package]] name = "bcvk-qemu" @@ -739,7 +739,7 @@ checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" [[package]] name = "composefs" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "anyhow", "composefs-ioctls", @@ -766,7 +766,7 @@ dependencies = [ [[package]] name = "composefs-boot" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "anyhow", "composefs", @@ -782,7 +782,7 @@ dependencies = [ [[package]] name = "composefs-ctl" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "anyhow", "cap-std-ext", @@ -811,7 +811,7 @@ dependencies = [ [[package]] name = "composefs-fuse" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "anyhow", "composefs", @@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "composefs-ioctls" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "rustix", "thiserror 2.0.18", @@ -835,11 +835,11 @@ dependencies = [ [[package]] name = "composefs-oci" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "anyhow", "async-compression", - "base64 0.22.1", + "base64 0.23.0", "bytes", "cap-std-ext", "composefs", @@ -869,7 +869,7 @@ dependencies = [ [[package]] name = "composefs-splitdirfdstream" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "rand 0.10.2", "rand_pcg", @@ -882,10 +882,10 @@ dependencies = [ [[package]] name = "composefs-storage" version = "0.7.0" -source = "git+https://github.com/composefs/composefs-rs?rev=0a819c351951864071aa9ec23d6594710bf3173f#0a819c351951864071aa9ec23d6594710bf3173f" +source = "git+https://github.com/composefs/composefs-rs?rev=0fbc853325ff92dbb842e715611bed7ddf9fe169#0fbc853325ff92dbb842e715611bed7ddf9fe169" dependencies = [ "anyhow", - "base64 0.22.1", + "base64 0.23.0", "cap-std", "cap-std-ext", "composefs-splitdirfdstream", @@ -1436,15 +1436,15 @@ dependencies = [ [[package]] name = "fuser" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a5eca878900c2e39e9e52fd797954b7fc39eeefc8558257114bfea6a698fcf" +checksum = "b82b6597d216503555ead6b358f341ef748869bf5c6fbae6a0cb9dd231baecfd" dependencies = [ "bitflags 2.11.1", "libc", "log", "memchr", - "nix 0.30.1", + "nix 0.31.2", "num_enum", "page_size", "parking_lot", @@ -2227,19 +2227,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nix" version = "0.31.2" @@ -2250,6 +2237,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -3492,7 +3480,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.2", ] [[package]] @@ -3513,7 +3501,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 1.0.2", ] [[package]] @@ -3522,7 +3510,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.2", ] [[package]] @@ -4103,6 +4091,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + [[package]] name = "winnow" version = "1.0.2" @@ -4299,19 +4293,20 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zlink" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0799145a2e15a8ea97e44771916f8630c870ee302c178a313269fc8e3b6f9736" +checksum = "dd5961645cac87b613e8b3c6fb1743a567e6459bc8c1d00f7bf9884020fc98d4" dependencies = [ + "zlink-core", "zlink-smol", "zlink-tokio", ] [[package]] name = "zlink-core" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2526043182746855a331d62e03c19964c7e1a714259ebe4764d1f5ce330ef01" +checksum = "0314bb8bc5a5693f4625a6ea9681731a5f4bcc0a952bfca28184f268db15dcc2" dependencies = [ "futures-util", "itoa", @@ -4322,25 +4317,36 @@ dependencies = [ "serde", "serde_json", "tracing", + "zlink-idl", "zlink-macros", ] +[[package]] +name = "zlink-idl" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7990f15bd88a2b37d55c5fc13a1dd662ee98674ec9c6ac904fe46066523932a" +dependencies = [ + "winnow 0.7.15", +] + [[package]] name = "zlink-macros" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cc16c0526f6721144e1cfeb37fec13e2058993f435015527ec27bf00c2234e" +checksum = "2fc623b8323797bf2d48333e7633e143077dd92cc66d9cca170014b66c43b1b5" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", + "zlink-idl", ] [[package]] name = "zlink-smol" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce083a1715cabf50e1af55e7444d7fe04c912cd2dc27cef0fe378e78d6158699" +checksum = "4bc4150b8af92eb0ae7007e0ced3eb2bcf8a7fa49457de2f30b11752ea538f46" dependencies = [ "async-broadcast", "async-channel", @@ -4353,9 +4359,9 @@ dependencies = [ [[package]] name = "zlink-tokio" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228414d41219fb9c8a02b6bd71a677ed8b585c461670101b5da09c06a0dd3f56" +checksum = "4c4fe0f232dd656bcb0594e9646b86c69f6257e3d5194f28715a044c4d10c8fa" dependencies = [ "futures-util", "pin-project-lite", diff --git a/Cargo.toml b/Cargo.toml index 393f6a37e1..8038bc7c86 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 = "0a819c351951864071aa9ec23d6594710bf3173f", default-features = false, features = [ +composefs-ctl = { git = "https://github.com/composefs/composefs-rs", rev = "0fbc853325ff92dbb842e715611bed7ddf9fe169", 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 c168c5c497..3bc58c8b10 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -924,6 +924,122 @@ fn write_pe_to_esp( Ok(boot_label) } +/// Scans `entries` for the primary UKI (`PEType::Uki`, not an addon) and +/// extracts the `composefs=` digest embedded in its kernel cmdline. +/// +/// Returns `Ok(None)` if there is no UKI entry (e.g. a BLS-only boot setup) — +/// there's nothing to validate against in that case. +/// +/// This mirrors the lookup [`write_pe_to_esp`] already does when writing the +/// UKI to the ESP; it's factored out here so callers can validate (and +/// repair) the freshly-generated boot image digest *before* it's used for +/// mounting, well before `write_pe_to_esp`'s own (too-late-to-repair) check +/// of the same thing runs. +fn find_expected_composefs_digest( + repo: &crate::store::ComposefsRepository, + entries: &[ComposefsBootEntry], +) -> Result> { + for entry in entries { + let ComposefsBootEntry::Type2(entry) = entry else { + continue; + }; + if !matches!(entry.pe_type, PEType::Uki) { + continue; + } + let mut uki_reader = match &entry.file { + RegularFile::External(id, ..) | RegularFile::ExternalNoVerity(id, ..) => { + std::fs::File::from(repo.open_object(id)?) + } + RegularFile::Inline(..) | RegularFile::Sparse(..) => { + anyhow::bail!("UKI file is not a regular external object") + } + }; + let cmdline = uki::get_cmdline_buffered(&mut uki_reader).context("Getting UKI cmdline")?; + let composefs_info = ComposefsBootCmdline::::from_cmdline(&cmdline) + .context("Parsing composefs=")? + .ok_or_else(|| anyhow::anyhow!("No composefs image in UKI cmdline"))?; + return Ok(Some(composefs_info.digest().clone())); + } + Ok(None) +} + +/// Validates that the freshly-generated boot image digest `computed_id` +/// matches what's embedded in the UKI (if any), and if not, searches for an +/// [`composefs_oci::XattrFiltering`] mode whose boot image digest does match +/// via [`composefs_oci::find_matching_boot_image`], before giving up. +/// +/// This handles images built with older (or newer) composefs-rs tooling +/// that computed their embedded UKI `composefs=` digest using a different +/// default xattr filtering mode than the one bootc's own build used. +#[context("Verifying composefs digest against UKI")] +pub(crate) fn ensure_correct_composefs_digest( + repo: &Arc, + manifest_digest: &composefs_oci::OciDigest, + computed_id: Sha512HashValue, + entries: &[ComposefsBootEntry], +) -> Result { + let Some(expected) = + find_expected_composefs_digest(repo, entries).context("Checking UKI composefs digest")? + else { + // No UKI (e.g. a BLS-only setup); nothing to cross-check. + return Ok(computed_id); + }; + if expected == computed_id { + // The UKI's expected digest already matches; no repair needed. + return Ok(computed_id); + } + // The UKI was built with a different xattr filtering mode and/or EROFS + // format version than the one bootc's own build used. Search for one + // whose boot image digest does match, for backward compatibility with + // older or newer image tooling. + tracing::info!( + "Freshly computed composefs digest ({computed_id:?}) doesn't match the digest \ + embedded in the UKI ({expected:?}); searching for an xattr filtering mode and/or \ + EROFS format version whose boot image matches, for backward compatibility with \ + older or newer image tooling" + ); + resolve_boot_image_match( + expected.clone(), + composefs_oci::find_matching_boot_image(repo, manifest_digest, &expected), + ) +} + +/// Interprets the result of searching for a boot image whose digest matches +/// `expected` (see [`composefs_oci::find_matching_boot_image`]): uses the +/// matching mode's digest if one was found, or fails with an error listing +/// every combination tried if not. +/// +/// Factored out from [`ensure_correct_composefs_digest`] purely so this +/// decision logic can be unit tested without a real repo or UKI fixture. +fn resolve_boot_image_match( + expected: Sha512HashValue, + find_matching_result: Result>, +) -> Result { + match find_matching_result.context( + "Searching for a boot image xattr filtering mode/format version matching the UKI digest", + )? { + composefs_oci::BootImageMatch::Found { + mode, + version, + digest, + } => { + tracing::info!( + "Boot image built with {mode:?} xattr filtering (EROFS {version:?}) matches \ + the UKI; using it" + ); + Ok(digest) + } + composefs_oci::BootImageMatch::NotFound(tried) => { + anyhow::bail!( + "The UKI's embedded composefs= digest ({expected:?}) doesn't match any of \ + {tried} supported xattr filtering mode/EROFS format version combinations. \ + The image may be corrupt, or was built with an incompatible composefs-rs \ + version." + ); + } + } +} + #[context("Writing Grub menuentry")] fn write_grub_uki_menuentry( root_path: Utf8PathBuf, @@ -1411,15 +1527,34 @@ pub(crate) async fn setup_composefs_boot( let repo = Arc::new(repo); // Generate the bootable EROFS image (idempotent). - let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest) - .context("Generating bootable EROFS image")?; + let generated_id = composefs_oci::generate_boot_image( + &repo, + &pull_result.manifest_digest, + &composefs_oci::OciTransformOptions::default(), + ) + .context("Generating bootable EROFS image")?; // Reconstruct the OCI filesystem to discover boot entries (kernel, initramfs, etc.). - let fs = composefs_oci::image::create_filesystem(&*repo, &pull_result.config_digest, None) - .context("Creating composefs filesystem for boot entry discovery")?; + let fs = composefs_oci::image::create_filesystem( + &*repo, + &pull_result.config_digest, + None, + &composefs_oci::OciTransformOptions::default(), + ) + .context("Creating composefs filesystem for boot entry discovery")?; let entries = get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?; + // If the UKI was built by tooling using a different xattr filtering + // mode, find the mode whose boot image matches the digest embedded in + // the UKI. + let id = ensure_correct_composefs_digest( + &repo, + &pull_result.manifest_digest, + generated_id, + &entries, + )?; + let composefs_mnt_fd = repo .mount(&id.to_hex()) .context("Failed to mount composefs image")?; @@ -1560,6 +1695,7 @@ pub(crate) async fn setup_composefs_boot( #[cfg(test)] mod tests { use super::*; + use composefs::erofs::format::FormatVersion; #[test] fn test_type1_filename_generation() { @@ -1672,4 +1808,72 @@ mod tests { "RHEL should sort before Fedora in descending order" ); } + + /// A distinct, non-`EMPTY` digest to use as "the other" digest in + /// `resolve_boot_image_match` tests. + fn other_digest() -> Sha512HashValue { + Sha512HashValue::from_hex("aa".repeat(64)).unwrap() + } + + #[test] + fn test_resolve_boot_image_match_found() { + let expected = other_digest(); + // A non-default mode, to make the test case meaningful. + let found_mode = composefs_oci::XattrFiltering::KeepUserXattrs; + + let result = resolve_boot_image_match( + expected.clone(), + Ok(composefs_oci::BootImageMatch::Found { + mode: found_mode, + version: FormatVersion::V2, + digest: expected.clone(), + }), + ); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_resolve_boot_image_match_error_paths() { + let expected = other_digest(); + // 2 xattr filtering modes x 2 EROFS format versions. + let combinations_tried = 4; + + enum FindMatching { + /// Succeeds, but no combination's digest matches `expected`. + NotFound, + /// The search itself fails. + Errors, + } + + // (find_matching behavior, substrings that must appear in the resulting error) + let cases = [ + ( + FindMatching::NotFound, + vec![format!("{expected:?}"), format!("{combinations_tried}")], + ), + ( + FindMatching::Errors, + vec![ + "search blew up".to_string(), + "Searching for a boot image xattr filtering mode/format version matching \ + the UKI digest" + .to_string(), + ], + ), + ]; + + for (find_matching, want_substrings) in cases { + let find_matching_result = match find_matching { + FindMatching::NotFound => { + Ok(composefs_oci::BootImageMatch::NotFound(combinations_tried)) + } + FindMatching::Errors => Err(anyhow::anyhow!("search blew up")), + }; + let result = resolve_boot_image_match(expected.clone(), find_matching_result); + let msg = format!("{:#}", result.unwrap_err()); + for want in &want_substrings { + assert!(msg.contains(want), "expected {msg:?} to contain {want:?}"); + } + } + } } diff --git a/crates/lib/src/bootc_composefs/digest.rs b/crates/lib/src/bootc_composefs/digest.rs index af7d3746b5..057f60fe4c 100644 --- a/crates/lib/src/bootc_composefs/digest.rs +++ b/crates/lib/src/bootc_composefs/digest.rs @@ -77,6 +77,7 @@ pub(crate) async fn compute_composefs_digest( dirfd, std::path::PathBuf::from("."), Some(repo.clone()), + &composefs::generic_tree::OciTransformOptions::default(), ) .await .context("Reading container root")?; diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index dc1c0658f7..44233dc0ba 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -55,6 +55,7 @@ use ostree_ext::containers_image_proxy; use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; +use crate::bootc_composefs::boot::ensure_correct_composefs_digest; use crate::bootc_composefs::progress; use crate::composefs_consts::BOOTC_TAG_PREFIX; use crate::install::{RootSetup, State}; @@ -393,15 +394,34 @@ pub(crate) async fn pull_composefs_repo( ); // Generate the bootable EROFS image (idempotent). - let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest) - .context("Generating bootable EROFS image")?; + let generated_id = composefs_oci::generate_boot_image( + &repo, + &pull_result.manifest_digest, + &composefs_oci::OciTransformOptions::default(), + ) + .context("Generating bootable EROFS image")?; // Get boot entries from the OCI filesystem (untransformed). - let fs = create_composefs_filesystem(&*repo, &pull_result.config_digest, None) - .context("Creating composefs filesystem for boot entry discovery")?; + let fs = create_composefs_filesystem( + &*repo, + &pull_result.config_digest, + None, + &composefs_oci::OciTransformOptions::default(), + ) + .context("Creating composefs filesystem for boot entry discovery")?; let entries = get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?; + // If the UKI was built by tooling using a different xattr filtering + // mode, find the mode whose boot image matches the digest embedded in + // the UKI. + let id = ensure_correct_composefs_digest( + &repo, + &pull_result.manifest_digest, + generated_id, + &entries, + )?; + // Unwrap the Arc to get the owned repo back. let mut repo = Arc::try_unwrap(repo).map_err(|_| { anyhow::anyhow!("BUG: Arc still has other references after pull completed") diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 8ee1a8ddd3..4b65146437 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -142,7 +142,12 @@ pub(crate) fn validate_update( let oci_digest: composefs_oci::OciDigest = img_digest .parse() .with_context(|| format!("Parsing config digest {img_digest}"))?; - let mut fs = create_filesystem(repo, &oci_digest, Some(config_verity))?; + let mut fs = create_filesystem( + repo, + &oci_digest, + Some(config_verity), + &composefs_oci::OciTransformOptions::default(), + )?; fs.transform_for_boot(&repo)?; let image_id = fs.compute_image_id(repo.erofs_version()); diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 31567f7f98..190c853d98 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -1962,6 +1962,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { &repo, &pull_result.config_digest, Some(&pull_result.config_verity), + &composefs_oci::OciTransformOptions::default(), ) .context("Populating fs")?; fs.transform_for_boot(&repo).context("Preparing for boot")?;