From 18bf103f308df84d6d3be4d57fdf848436edb562 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 30 May 2026 18:12:11 -0400 Subject: [PATCH 1/4] deploy: Fix disallowed str::len on literal in user-agent test The test asserted `prefix.len() > "bootc/".len()`, which trips bootc's `disallowed_methods` clippy lint (str::len). This was masked while the lib test target failed to compile; assert a non-empty version suffix instead, which is clearer and lint-clean. Assisted-by: OpenCode (Claude Opus 4.5) Signed-off-by: Colin Walters --- crates/lib/src/deploy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index c15f6cb76..1efcbd711 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -1409,7 +1409,7 @@ mod tests { ); // Verify the version is present (not just "bootc/") assert!( - prefix.len() > "bootc/".len(), + prefix.strip_prefix("bootc/").is_some_and(|v| !v.is_empty()), "Version should be present after bootc/" ); } From cc0d25995c6de0c867435cbf9493b6b4993bbd44 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sun, 31 May 2026 10:58:42 -0400 Subject: [PATCH 2/4] xtask: Add --skip-bind-storage to run-tmt The `--bind-storage-ro` host container-storage passthrough relies on a libvirt-managed virtiofsd, which cannot run in some environments such as nested user namespaces or cloud/non-qemu setups. Plans that normally request bind-storage previously had no way to opt out short of editing plan metadata. Add a `--skip-bind-storage` flag (and matching `BOOTC_skip_bind_storage` env var) that forces those plans to run without the host container-storage mount. Default behavior is unchanged: bind-storage is still used wherever it is requested and supported. Plans that depend on a locally built upgrade image reaching the VM via bind-storage will be unable to perform the upgrade/switch step when this is set. Assisted-by: OpenCode (Claude Opus 4.5) Signed-off-by: Colin Walters --- crates/xtask/src/tmt.rs | 11 ++++++++-- crates/xtask/src/xtask.rs | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/crates/xtask/src/tmt.rs b/crates/xtask/src/tmt.rs index 795830343..a3ce5499e 100644 --- a/crates/xtask/src/tmt.rs +++ b/crates/xtask/src/tmt.rs @@ -456,8 +456,11 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { let mut opts = Vec::new(); - // If test wants bind storage and distro supports it, add --bind-storage-ro - if try_bind_storage && supports_bind_storage_ro { + // If test wants bind storage, the distro supports it, and it wasn't + // explicitly disabled, add --bind-storage-ro + let use_bind_storage = + try_bind_storage && supports_bind_storage_ro && !args.skip_bind_storage; + if use_bind_storage { opts.push(BCVK_OPT_BIND_STORAGE_RO.to_string()); // If upgrade image is provided, set it as an environment variable for tmt @@ -465,6 +468,10 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { if let Some(ref upgrade_img) = args.upgrade_image { tmt_env_vars.push(format!("{}={}", ENV_BOOTC_UPGRADE_IMAGE, upgrade_img)); } + } else if try_bind_storage && args.skip_bind_storage { + println!( + "Note: Test requests bind storage but --skip-bind-storage was set; running without host container-storage mount" + ); } else if try_bind_storage && !supports_bind_storage_ro { println!( "Note: Test wants bind storage but skipping on {} (missing systemd.extra-unit.* support)", diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 79f4ffff6..3c3d77737 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -45,6 +45,16 @@ fn out_of_sync_error(message: &str) -> Result<()> { anyhow::bail!("{}; run `just update-generated` to update it", message) } +/// Parse a `0`/`1` boolean from a CLI/env value so the flag can be driven from +/// the Justfile (e.g. `BOOTC_skip_bind_storage=1`). +fn parse_cli_bool(s: &str) -> std::result::Result { + match s { + "1" | "true" => Ok(true), + "0" | "false" => Ok(false), + other => Err(format!("invalid value '{other}' (expected 0, 1, true, or false)")), + } +} + /// Build tasks for bootc #[derive(Debug, Parser)] #[command(name = "xtask")] @@ -228,6 +238,24 @@ pub(crate) struct RunTmtArgs { #[clap(long)] pub(crate) upgrade_image: Option, + /// Skip the `--bind-storage-ro` host container-storage virtiofs mount even for + /// plans that request it. Useful where libvirt-managed virtiofsd cannot run + /// (nested user namespaces, cloud/non-qemu). Plans that depend on a locally + /// built upgrade image being available in-VM via bind-storage will not be able + /// to perform the upgrade/switch step. + /// + /// Takes `0`/`1`/`true`/`false` so it can be driven from the Justfile via + /// `BOOTC_skip_bind_storage=1`. A bare `--skip-bind-storage` means `1`. + #[arg( + long, + env = "BOOTC_skip_bind_storage", + num_args = 0..=1, + default_value_t = false, + default_missing_value = "1", + value_parser = parse_cli_bool, + )] + pub(crate) skip_bind_storage: bool, + /// Preserve VMs after test completion (useful for debugging) #[arg(long)] pub(crate) preserve_vm: bool, @@ -765,3 +793,18 @@ fn validate_composefs_digest(sh: &Shell, args: &ValidateComposefsDigestArgs) -> anyhow::bail!("Composefs digest mismatch"); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_cli_bool() { + assert_eq!(parse_cli_bool("1"), Ok(true)); + assert_eq!(parse_cli_bool("true"), Ok(true)); + assert_eq!(parse_cli_bool("0"), Ok(false)); + assert_eq!(parse_cli_bool("false"), Ok(false)); + assert!(parse_cli_bool("").is_err()); + assert!(parse_cli_bool("maybe").is_err()); + } +} From 2c5fbb58291c2c952e7593ed1f80412d2b08be89 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Mon, 25 May 2026 09:26:48 -0400 Subject: [PATCH 3/4] bootc: Adapt to a few composefs-rs API changes Assisted-by: OpenCode (Claude Sonnet 4) Signed-off-by: Colin Walters --- crates/etc-merge/src/lib.rs | 1 + crates/initramfs/src/lib.rs | 7 +++-- crates/lib/src/bootc_composefs/boot.rs | 11 +++++-- crates/lib/src/bootc_composefs/digest.rs | 36 ++++++++++++++------- crates/lib/src/bootc_composefs/repo.rs | 22 +++++++------ crates/lib/src/bootc_composefs/update.rs | 2 +- crates/lib/src/cli.rs | 40 ++++++++++++++++++++++-- crates/lib/src/install.rs | 5 +-- crates/lib/src/store/mod.rs | 18 ++++++----- crates/lib/src/testutils.rs | 9 +++--- crates/lib/src/ukify.rs | 11 +++++-- 11 files changed, 115 insertions(+), 47 deletions(-) diff --git a/crates/etc-merge/src/lib.rs b/crates/etc-merge/src/lib.rs index 0743435a2..0b78a0af5 100644 --- a/crates/etc-merge/src/lib.rs +++ b/crates/etc-merge/src/lib.rs @@ -52,6 +52,7 @@ impl From<(&cap_std::fs::Metadata, Xattrs)> for MyStat { st_uid: value.0.uid(), st_gid: value.0.gid(), st_mtim_sec: value.0.mtime(), + st_mtim_nsec: value.0.mtime_nsec() as u32, xattrs: value.1, }) } diff --git a/crates/initramfs/src/lib.rs b/crates/initramfs/src/lib.rs index c15dae95d..17c4a5783 100644 --- a/crates/initramfs/src/lib.rs +++ b/crates/initramfs/src/lib.rs @@ -28,7 +28,7 @@ use composefs::{ mountcompat::{overlayfs_set_fd, overlayfs_set_lower_and_data_fds, prepare_mount}, repository::Repository, }; -use composefs_boot::cmdline::get_cmdline_composefs; +use composefs_boot::cmdline::ComposefsCmdline; use composefs_ctl::composefs; use composefs_ctl::composefs_boot; @@ -463,7 +463,10 @@ pub fn setup_root(args: Args) -> Result<()> { config }; - let (image, insecure) = get_cmdline_composefs::(&cmdline)?; + let composefs = ComposefsCmdline::::from_cmdline(&cmdline)? + .context("No composefs= or composefs.digest.v1= karg found")?; + let image = composefs.digest(); + let insecure = composefs.is_insecure(); let new_root = match &args.root_fs { Some(path) => open_root_fs(path).context("Failed to clone specified root fs")?, diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index f4a020c5e..f09515e04 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -82,7 +82,9 @@ use composefs_boot::bootloader::{ BootEntry as ComposefsBootEntry, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT, PEType, UsrLibModulesVmlinuz, get_boot_resources, }; -use composefs_boot::{cmdline::get_cmdline_composefs, os_release::OsReleaseInfo, uki}; +use composefs_boot::{ + cmdline::ComposefsCmdline as BootComposefsCmdline, os_release::OsReleaseInfo, uki, +}; use composefs_ctl::composefs; use composefs_ctl::composefs_boot; use composefs_ctl::composefs_oci; @@ -811,8 +813,11 @@ fn write_pe_to_esp( if matches!(pe_type, PEType::Uki) { let cmdline = uki::get_cmdline_buffered(&mut uki_reader).context("Getting UKI cmdline")?; - let (composefs_cmdline, missing_verity_allowed_cmdline) = - get_cmdline_composefs::(&cmdline).context("Parsing composefs=")?; + let parsed_cmdline = BootComposefsCmdline::::from_cmdline(&cmdline) + .context("Parsing composefs=")? + .context("No composefs= or composefs.digest.v1= karg found in UKI cmdline")?; + let composefs_cmdline = parsed_cmdline.digest().clone(); + let missing_verity_allowed_cmdline = parsed_cmdline.is_insecure(); // If the UKI cmdline does not match what the user has passed as cmdline option // NOTE: This will only be checked for new installs and now upgrades/switches diff --git a/crates/lib/src/bootc_composefs/digest.rs b/crates/lib/src/bootc_composefs/digest.rs index 074423e25..c160c7209 100644 --- a/crates/lib/src/bootc_composefs/digest.rs +++ b/crates/lib/src/bootc_composefs/digest.rs @@ -10,7 +10,9 @@ use camino::Utf8Path; use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::Dir; use composefs::dumpfile; -use composefs::fsverity::{Algorithm, FsVerityHashValue}; +use composefs::erofs::format::FormatVersion; +use composefs::fsverity::FsVerityHashValue; +use composefs::repository::RepositoryConfig; use composefs_boot::BootOps as _; use composefs_ctl::composefs; use composefs_ctl::composefs_boot; @@ -20,21 +22,26 @@ use crate::store::ComposefsRepository; /// Creates a temporary composefs repository for computing digests. /// +/// The `erofs_version` controls which EROFS format the digest is computed for: +/// use `FormatVersion::V1` to get a `composefs.digest.v1=` karg (V1 EROFS, C-tool +/// compatible) or `FormatVersion::V2` for the legacy `composefs=` karg. +/// /// Returns the TempDir guard (must be kept alive for the repo to remain valid) /// and the repository wrapped in Arc. #[fn_error_context::context("Creating new temp composefs repo")] -pub(crate) fn new_temp_composefs_repo() -> Result<(TempDir, Arc)> { +pub(crate) fn new_temp_composefs_repo( + erofs_version: FormatVersion, +) -> Result<(TempDir, Arc)> { let td_guard = tempfile::tempdir_in("/var/tmp")?; let td_path = td_guard.path(); let td_dir = Dir::open_ambient_dir(td_path, cap_std::ambient_authority())?; td_dir.create_dir("repo")?; let repo_dir = td_dir.open_dir("repo")?; - let (mut repo, _created) = - ComposefsRepository::init_path(&repo_dir, ".", Algorithm::SHA512, false) - .context("Init cfs repo")?; - // We don't need to hard require verity on the *host* system, we're just computing a checksum here - repo.set_insecure(); + let mut config = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512).set_insecure(); + config.erofs_formats = composefs::erofs::format::FormatConfig::single(erofs_version); + let (repo, _created) = + ComposefsRepository::init_path(&repo_dir, ".", config).context("Init cfs repo")?; Ok((td_guard, Arc::new(repo))) } @@ -58,13 +65,14 @@ pub(crate) fn new_temp_composefs_repo() -> Result<(TempDir, Arc, ) -> Result { if path.as_str() == "/" { anyhow::bail!("Cannot operate on active root filesystem; mount separate target instead"); } - let (_td_guard, repo) = new_temp_composefs_repo()?; + let (_td_guard, repo) = new_temp_composefs_repo(erofs_version)?; // Read filesystem from path, transform for boot, compute digest let dirfd: OwnedFd = rustix::fs::open( @@ -81,7 +89,7 @@ pub(crate) async fn compute_composefs_digest( .await .context("Reading container root")?; fs.transform_for_boot(&repo).context("Preparing for boot")?; - let id = fs.compute_image_id(); + let id = fs.compute_image_id(erofs_version); let digest = id.to_hex(); if let Some(dumpfile_path) = write_dumpfile_to { @@ -135,7 +143,9 @@ mod tests { // Compute the digest let path = Utf8Path::from_path(td.path()).unwrap(); - let digest = compute_composefs_digest(path, None).await.unwrap(); + let digest = compute_composefs_digest(path, FormatVersion::V2, None) + .await + .unwrap(); // Verify it's a valid hex string of expected length (SHA-512 = 128 hex chars) assert_eq!( @@ -150,7 +160,9 @@ mod tests { ); // Verify consistency - computing twice on the same filesystem produces the same result - let digest2 = compute_composefs_digest(path, None).await.unwrap(); + let digest2 = compute_composefs_digest(path, FormatVersion::V2, None) + .await + .unwrap(); assert_eq!( digest, digest2, "Digest should be consistent across multiple computations" @@ -159,7 +171,7 @@ mod tests { #[tokio::test] async fn test_compute_composefs_digest_rejects_root() { - let result = compute_composefs_digest(Utf8Path::new("/"), None).await; + let result = compute_composefs_digest(Utf8Path::new("/"), FormatVersion::V2, None).await; assert!(result.is_err()); let err = result.unwrap_err(); let found = err.chain().any(|e| { diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index fe58e201b..eeb958b9d 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -99,16 +99,18 @@ pub(crate) async fn initialize_composefs_repository( crate::store::ensure_composefs_dir(rootfs_dir)?; - let (mut repo, _created) = crate::store::ComposefsRepository::init_path( - rootfs_dir, - "composefs", - composefs::fsverity::Algorithm::SHA512, - !allow_missing_fsverity, - ) - .context("Failed to initialize composefs repository")?; - if allow_missing_fsverity { - repo.set_insecure(); - } + let config = { + let c = + composefs::repository::RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512); + if allow_missing_fsverity { + c.set_insecure() + } else { + c + } + }; + let (repo, _created) = + crate::store::ComposefsRepository::init_path(rootfs_dir, "composefs", config) + .context("Failed to initialize composefs repository")?; let imgref: containers_image_proxy::ImageReference = state .source diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 79c823159..633bf1a2e 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -141,7 +141,7 @@ pub(crate) fn validate_update( let mut fs = create_filesystem(repo, &oci_digest, Some(config_verity))?; fs.transform_for_boot(&repo)?; - let image_id = fs.compute_image_id(); + let image_id = fs.compute_image_id(composefs::erofs::format::FormatVersion::V2); let all_deployments = host.all_composefs_deployments()?; diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 9a7c307ff..782bed106 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -17,6 +17,7 @@ use clap::CommandFactory; use clap::Parser; use clap::ValueEnum; use composefs::dumpfile; +use composefs::erofs::format::FormatVersion; use composefs::fsverity; use composefs::fsverity::FsVerityHashValue; use composefs_ctl::composefs; @@ -438,6 +439,14 @@ pub(crate) enum ContainerOpts { #[clap(long)] allow_missing_verity: bool, + /// EROFS format version to use when computing the composefs digest. + /// + /// V1 produces a `composefs.digest.v1=` karg (C-tool compatible). + /// V2 produces the legacy `composefs=` karg (composefs-rs native). + /// Must match the format version used when images were committed to the repository. + #[clap(hide = true, long, default_value = "v2")] + erofs_version: ErofsVersionArg, + /// Write a dumpfile to this path #[clap(long)] write_dumpfile_to: Option, @@ -484,6 +493,24 @@ pub(crate) enum ContainerOpts { }, } +/// EROFS format version for `bootc container ukify --erofs-version`. +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub(crate) enum ErofsVersionArg { + /// V1 EROFS (C-tool compatible, `composefs.digest.v1=` karg). + V1, + /// V2 EROFS (composefs-rs native, `composefs=` karg). Default. + V2, +} + +impl From for FormatVersion { + fn from(v: ErofsVersionArg) -> Self { + match v { + ErofsVersionArg::V1 => FormatVersion::V1, + ErofsVersionArg::V2 => FormatVersion::V2, + } + } +} + #[derive(Debug, Clone, ValueEnum, PartialEq, Eq)] pub(crate) enum ExportFormat { /// Export as tar archive @@ -1841,7 +1868,12 @@ async fn run_from_opt(opt: Opt) -> Result<()> { path, write_dumpfile_to, } => { - let digest = compute_composefs_digest(&path, write_dumpfile_to.as_deref()).await?; + let digest = compute_composefs_digest( + &path, + FormatVersion::V2, + write_dumpfile_to.as_deref(), + ) + .await?; println!("{digest}"); Ok(()) } @@ -1849,7 +1881,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { write_dumpfile_to, image, } => { - let (_td_guard, repo) = new_temp_composefs_repo()?; + let (_td_guard, repo) = new_temp_composefs_repo(FormatVersion::V2)?; let mut proxycfg = crate::deploy::new_proxy_config(); @@ -1892,7 +1924,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { ) .context("Populating fs")?; fs.transform_for_boot(&repo).context("Preparing for boot")?; - let id = fs.compute_image_id(); + let id = fs.compute_image_id(composefs::erofs::format::FormatVersion::V2); println!("{}", id.to_hex()); if let Some(path) = write_dumpfile_to.as_deref() { @@ -1908,6 +1940,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { rootfs, kargs, allow_missing_verity, + erofs_version, write_dumpfile_to, kernel_dir, args, @@ -1940,6 +1973,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { &args, kernel, allow_missing_verity, + erofs_version.into(), write_dumpfile_to.as_deref(), ) .await diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index d49c6a350..4ff6ab385 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2023,8 +2023,9 @@ async fn install_to_filesystem_impl( let (cfs_repo, _created) = crate::store::ComposefsRepository::init_path( &rootfs.physical_root, crate::store::COMPOSEFS, - composefs_ctl::composefs::fsverity::Algorithm::SHA512, - false, + composefs_ctl::composefs::repository::RepositoryConfig::new( + composefs_ctl::composefs::fsverity::Algorithm::SHA512, + ), )?; crate::deploy::check_disk_space_composefs( &cfs_repo, diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 653fd2a25..26ee48404 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -111,6 +111,7 @@ use ostree_ext::{gio, ostree}; use rustix::fs::Mode; use composefs::fsverity::Sha512HashValue; +use composefs::repository::RepositoryConfig; use composefs_ctl::composefs; use crate::bootc_composefs::backwards_compat::bcompat_boot::prepend_custom_prefix; @@ -618,16 +619,19 @@ impl Storage { let ostree = self.get_ostree()?; let ostree_repo = &ostree.repo(); let ostree_verity = ostree_ext::fsverity::is_verity_enabled(ostree_repo)?; - let (mut composefs, _created) = ComposefsRepository::init_path( - self.physical_root.open_dir(COMPOSEFS)?, - ".", - composefs::fsverity::Algorithm::SHA512, - ostree_verity.enabled, - )?; + let config = { + let c = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512); + if !ostree_verity.enabled { + c.set_insecure() + } else { + c + } + }; if !ostree_verity.enabled { tracing::debug!("Setting insecure mode for composefs repo"); - composefs.set_insecure(); } + let (composefs, _created) = + ComposefsRepository::init_path(self.physical_root.open_dir(COMPOSEFS)?, ".", config)?; let composefs = Arc::new(composefs); let r = Arc::clone(self.composefs.get_or_init(|| composefs)); Ok(r) diff --git a/crates/lib/src/testutils.rs b/crates/lib/src/testutils.rs index 3712c7e44..73b01d760 100644 --- a/crates/lib/src/testutils.rs +++ b/crates/lib/src/testutils.rs @@ -164,14 +164,15 @@ impl TestRoot { // Initialize the composefs repo (creates meta.json) let repo_dir = root.open_dir("composefs")?; - let (mut repo, _created) = ComposefsRepository::init_path( + let (repo, _created) = ComposefsRepository::init_path( &repo_dir, ".", - composefs_ctl::composefs::fsverity::Algorithm::SHA512, - false, + composefs_ctl::composefs::repository::RepositoryConfig::new( + composefs_ctl::composefs::fsverity::Algorithm::SHA512, + ) + .set_insecure(), ) .context("Initializing composefs repo")?; - repo.set_insecure(); let mut test_root = Self { root, diff --git a/crates/lib/src/ukify.rs b/crates/lib/src/ukify.rs index 523836217..9a85ef863 100644 --- a/crates/lib/src/ukify.rs +++ b/crates/lib/src/ukify.rs @@ -13,6 +13,9 @@ use camino::Utf8Path; use cap_std_ext::cap_std::fs::Dir; use fn_error_context::context; +use composefs::erofs::format::FormatVersion; +use composefs_ctl::composefs; + use crate::bootc_composefs::digest::compute_composefs_digest; use crate::bootc_composefs::status::ComposefsCmdline; use crate::kernel::KernelInternal; @@ -33,6 +36,7 @@ pub(crate) async fn build_ukify( args: &[OsString], kernel: Option, allow_missing_fsverity: bool, + erofs_version: FormatVersion, write_dumpfile_to: Option<&Utf8Path>, ) -> Result<()> { // Warn if --karg is used (temporary workaround) @@ -97,7 +101,8 @@ pub(crate) async fn build_ukify( } // Compute the composefs digest - let composefs_digest = compute_composefs_digest(rootfs, write_dumpfile_to).await?; + let composefs_digest = + compute_composefs_digest(rootfs, erofs_version, write_dumpfile_to).await?; // Get kernel arguments from kargs.d let mut cmdline = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?; @@ -152,7 +157,7 @@ mod tests { let tempdir = tempfile::tempdir().unwrap(); let path = Utf8Path::from_path(tempdir.path()).unwrap(); - let result = build_ukify(path, &[], &[], None, false, None).await; + let result = build_ukify(path, &[], &[], None, false, FormatVersion::V2, None).await; assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( @@ -174,7 +179,7 @@ mod tests { ) .unwrap(); - let result = build_ukify(path, &[], &[], None, false, None).await; + let result = build_ukify(path, &[], &[], None, false, FormatVersion::V2, None).await; assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( From 3b90847863db13cd331f975e18a9f61f58b14a04 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 30 May 2026 16:59:52 -0400 Subject: [PATCH 4/4] composefs: Generate both V1 and V2 EROFS images on install composefs-rs landed support for V1 EROFS, which we need to enable composefs on RHEL9. Make new installs produce both V1 and V2 EROFS images for committed composefs images so a deployment can be booted via either the composefs= (V2) or composefs.digest.v1= (V1) karg. Karg generation and digest computation default to V2 for now; this change only ensures both digests are available on disk. Assisted-by: OpenCode (Claude Sonnet 4.6) Signed-off-by: Colin Walters --- crates/initramfs/src/lib.rs | 2 +- crates/lib/src/bootc_composefs/boot.rs | 86 +++++++++++---- crates/lib/src/bootc_composefs/digest.rs | 4 +- crates/lib/src/bootc_composefs/gc.rs | 24 ++-- crates/lib/src/bootc_composefs/repo.rs | 9 +- crates/lib/src/bootc_composefs/update.rs | 133 +++++++++++++++++------ crates/lib/src/cli.rs | 4 +- crates/lib/src/install.rs | 15 ++- crates/lib/src/store/mod.rs | 45 ++++++-- crates/xtask/src/xtask.rs | 4 +- 10 files changed, 240 insertions(+), 86 deletions(-) diff --git a/crates/initramfs/src/lib.rs b/crates/initramfs/src/lib.rs index 17c4a5783..10c6b9492 100644 --- a/crates/initramfs/src/lib.rs +++ b/crates/initramfs/src/lib.rs @@ -464,7 +464,7 @@ pub fn setup_root(args: Args) -> Result<()> { }; let composefs = ComposefsCmdline::::from_cmdline(&cmdline)? - .context("No composefs= or composefs.digest.v1= karg found")?; + .context("No composefs= or composefs.digest= karg found")?; let image = composefs.digest(); let insecure = composefs.is_insecure(); diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index f09515e04..6f32ac166 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -785,6 +785,12 @@ struct UKIInfo { version: Option, os_id: Option, boot_digest: String, + /// The composefs image digest parsed from (and validated against) the UKI's + /// own cmdline. This is the authoritative deployment key for UKI boots: + /// setup-root opens `state/deploy/` using the karg baked into the UKI, + /// so the deploy directory must be named after exactly this value regardless + /// of which EROFS format (V1 or V2) was sealed. + composefs_cmdline: Sha512HashValue, } /// Writes a PortableExecutable to ESP along with any PE specific or Global addons @@ -795,6 +801,7 @@ fn write_pe_to_esp( file_path: &Utf8Path, pe_type: PEType, uki_id: &Sha512HashValue, + boot_ids: &[&Sha512HashValue], missing_fsverity_allowed: bool, mounted_efi: impl AsRef, ) -> Result> { @@ -815,7 +822,7 @@ fn write_pe_to_esp( let parsed_cmdline = BootComposefsCmdline::::from_cmdline(&cmdline) .context("Parsing composefs=")? - .context("No composefs= or composefs.digest.v1= karg found in UKI cmdline")?; + .context("No composefs= or composefs.digest= karg found in UKI cmdline")?; let composefs_cmdline = parsed_cmdline.digest().clone(); let missing_verity_allowed_cmdline = parsed_cmdline.is_insecure(); @@ -835,11 +842,9 @@ fn write_pe_to_esp( _ => { /* no-op */ } } - if composefs_cmdline != *uki_id { - anyhow::bail!( - "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})" - ); - } + parsed_cmdline + .validate_digest(boot_ids.iter().copied()) + .context("Validating UKI composefs digest")?; uki_reader.seek(SeekFrom::Start(0))?; let osrel = uki::get_text_section_buffered(&mut uki_reader, ".osrel")?; @@ -856,6 +861,7 @@ fn write_pe_to_esp( version: parsed_osrel.get_version(), os_id: parsed_osrel.get_value(&["ID"]), boot_digest, + composefs_cmdline, }); } @@ -1071,8 +1077,9 @@ pub(crate) fn setup_composefs_uki_boot( setup_type: BootSetupType, repo: crate::store::ComposefsRepository, id: &Sha512HashValue, + boot_ids: &[&Sha512HashValue], entries: Vec>, -) -> Result { +) -> Result<(String, Sha512HashValue)> { let (root_path, esp_device, bootloader, missing_fsverity_allowed, uki_addons) = match setup_type { BootSetupType::Setup((root_setup, state, postfetch)) => { @@ -1152,7 +1159,8 @@ pub(crate) fn setup_composefs_uki_boot( &entry.file, utf8_file_path, entry.pe_type, - &id, + id, + boot_ids, missing_fsverity_allowed, esp_mount.dir.path(), )?; @@ -1169,6 +1177,13 @@ pub(crate) fn setup_composefs_uki_boot( let boot_digest = uki_info.boot_digest.clone(); + // The deploy key for a UKI boot is the composefs digest baked into the UKI + // cmdline (already validated against `boot_ids` in `write_pe_to_esp`). + // setup-root opens `state/deploy/` using that same karg, so we must + // key the deployment off exactly this value -- whether the UKI was sealed + // with the V2 (default) or V1 EROFS digest. + let deploy_id = uki_info.composefs_cmdline.clone(); + match bootloader { Bootloader::Grub => { write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)? @@ -1179,7 +1194,7 @@ pub(crate) fn setup_composefs_uki_boot( Bootloader::None => unreachable!("Checked at install time"), }; - Ok(boot_digest) + Ok((boot_digest, deploy_id)) } /// A composefs image attached to a temporary directory with the ESP and a @@ -1350,6 +1365,15 @@ pub(crate) async fn setup_composefs_boot( let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest) .context("Generating bootable EROFS image")?; + // Open the OCI image to read both stored boot EROFS digests. The UKI may + // have been sealed with either the V1 or V2 boot image digest, so we need + // both for verification. + let oci_img = + composefs_oci::oci_image::OciImage::open(&*repo, &pull_result.manifest_digest, None) + .context("Opening OCI image to read boot image refs")?; + let boot_id_v1 = oci_img.boot_image_ref_v1().cloned(); + let boot_id_v2 = oci_img.boot_image_ref_v2().cloned(); + // 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")?; @@ -1402,25 +1426,49 @@ pub(crate) async fn setup_composefs_boot( ) })?; - let boot_digest = match boot_type { - BootType::Bls => setup_composefs_bls_boot( - BootSetupType::Setup((&root_setup, &state, &postfetch)), - repo, - &id, - entry, - mounted_root.dir(), - )?, + // The deployment key is the hash that setup-root looks for in + // state/deploy/, derived from the composefs karg. The two boot types + // establish that karg differently: + // + // * BLS: bootc writes the karg itself, so we are free to choose the key. + // Prefer the V2 digest (matching the default EROFS format), falling back + // to the primary id for legacy repos with no separate V2 boot ref. + // + // * UKI: the karg is baked into the UKI at seal time, so the key is + // whatever digest the UKI carries. `setup_composefs_uki_boot` parses and + // validates that against the boot refs and returns it, so we override the + // provisional value below. This keeps us correct whether the UKI was + // sealed with the V2 (default) or V1 EROFS digest. + let provisional_deploy_id = boot_id_v2.as_ref().cloned().unwrap_or_else(|| id.clone()); + + // Collect whichever boot image refs exist; the UKI cmdline may carry either. + let boot_ids_owned: Vec = + [boot_id_v1, boot_id_v2].into_iter().flatten().collect(); + let boot_ids: Vec<&Sha512HashValue> = boot_ids_owned.iter().collect(); + + let (boot_digest, deploy_id) = match boot_type { + BootType::Bls => ( + setup_composefs_bls_boot( + BootSetupType::Setup((&root_setup, &state, &postfetch)), + repo, + &provisional_deploy_id, + entry, + mounted_root.dir(), + )?, + provisional_deploy_id, + ), BootType::Uki => setup_composefs_uki_boot( BootSetupType::Setup((&root_setup, &state, &postfetch)), repo, - &id, + &provisional_deploy_id, + &boot_ids, entries, )?, }; write_composefs_state( &root_setup.physical_root_path, - &id, + &deploy_id, &crate::spec::ImageReference::from(state.target_imgref.clone()), None, boot_type, diff --git a/crates/lib/src/bootc_composefs/digest.rs b/crates/lib/src/bootc_composefs/digest.rs index c160c7209..402948b58 100644 --- a/crates/lib/src/bootc_composefs/digest.rs +++ b/crates/lib/src/bootc_composefs/digest.rs @@ -23,8 +23,8 @@ use crate::store::ComposefsRepository; /// Creates a temporary composefs repository for computing digests. /// /// The `erofs_version` controls which EROFS format the digest is computed for: -/// use `FormatVersion::V1` to get a `composefs.digest.v1=` karg (V1 EROFS, C-tool -/// compatible) or `FormatVersion::V2` for the legacy `composefs=` karg. +/// use `FormatVersion::V1` to get a `composefs.digest=v1-sha256-12:` karg (V1 EROFS, +/// C-tool compatible) or `FormatVersion::V2` for the legacy `composefs=` karg. /// /// Returns the TempDir guard (must be kept alive for the repo to remain valid) /// and the repository wrapped in Arc. diff --git a/crates/lib/src/bootc_composefs/gc.rs b/crates/lib/src/bootc_composefs/gc.rs index d8bafd467..95f234de0 100644 --- a/crates/lib/src/bootc_composefs/gc.rs +++ b/crates/lib/src/bootc_composefs/gc.rs @@ -402,16 +402,20 @@ pub(crate) async fn composefs_gc( ref_digest, None, ) { - if let Some(img_ref) = img.image_ref() { - if img_ref.to_hex() == *verity { - tracing::info!( - "Deployment {verity} has no manifest_digest in origin; \ - found matching manifest {ref_digest} via image_ref" - ); - live_manifest_digests.push(ref_digest.clone()); - found_manifest = true; - break; - } + // Check both V1 and V2 slots: the deployment verity + // may have been produced under either format. + let img_ref_hex = img + .image_ref_v1() + .or_else(|| img.image_ref_v2()) + .map(|id| id.to_hex()); + if img_ref_hex.as_deref() == Some(verity.as_str()) { + tracing::info!( + "Deployment {verity} has no manifest_digest in origin; \ + found matching manifest {ref_digest} via image_ref" + ); + live_manifest_digests.push(ref_digest.clone()); + found_manifest = true; + break; } } } diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index eeb958b9d..668aca1b8 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -99,7 +99,7 @@ pub(crate) async fn initialize_composefs_repository( crate::store::ensure_composefs_dir(rootfs_dir)?; - let config = { + let mut config = { let c = composefs::repository::RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512); if allow_missing_fsverity { @@ -108,6 +108,13 @@ pub(crate) async fn initialize_composefs_repository( c } }; + // Generate both V1 and V2 EROFS images so a deployment can be booted via + // either the composefs= legacy shorthand (V2) or composefs.digest= (V1/V2) + // karg. This makes both digests available for any boot path. + config.erofs_formats = composefs::erofs::format::FormatConfig { + default: composefs::erofs::format::FormatVersion::V1, + extra: [composefs::erofs::format::FormatVersion::V2].into(), + }; let (repo, _created) = crate::store::ComposefsRepository::init_path(rootfs_dir, "composefs", config) .context("Failed to initialize composefs repository")?; diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 633bf1a2e..2488bd307 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -141,13 +141,18 @@ pub(crate) fn validate_update( let mut fs = create_filesystem(repo, &oci_digest, Some(config_verity))?; fs.transform_for_boot(&repo)?; - let image_id = fs.compute_image_id(composefs::erofs::format::FormatVersion::V2); + // Match against both EROFS format ids: a deployment committed by a repo + // that generates both formats may be recorded under either the V1 + // (composefs.digest=v1-...) or V2 (composefs=, legacy shorthand) digest. + // compute_image_id is a cheap in-memory EROFS generation. + let id_v1 = fs.compute_image_id(composefs::erofs::format::FormatVersion::V1); + let id_v2 = fs.compute_image_id(composefs::erofs::format::FormatVersion::V2); let all_deployments = host.all_composefs_deployments()?; let found_depl = all_deployments .iter() - .find(|d| d.deployment.verity == image_id.to_hex()); + .find(|d| d.deployment.verity == id_v1.to_hex() || d.deployment.verity == id_v2.to_hex()); if let Some(collision) = found_depl { if is_switch { @@ -195,10 +200,13 @@ pub(crate) fn validate_update( .open_dir(STATE_DIR_RELATIVE) .context("Opening state dir")?; - if state_dir.exists(image_id.to_hex()) { - state_dir - .remove_dir_all(image_id.to_hex()) - .context("Removing state")?; + // New deployments are keyed by V1; check both in case an older deployment + // was recorded under V2. + for id in [&id_v1, &id_v2] { + let hex = id.to_hex(); + if state_dir.exists(&hex) { + state_dir.remove_dir_all(&hex).context("Removing state")?; + } } Ok(UpdateAction::Proceed) @@ -237,6 +245,34 @@ async fn apply_upgrade( Ok(()) } +/// Refuse to deploy if `deploy_id` collides with an existing deployment +/// (booted, staged, rollback, or pinned). +/// +/// `deploy_id` names the `state/deploy/` directory whose `/etc` is +/// seeded from the deploying image. Two images from different sources can +/// produce identical content (hence the same fs-verity digest); silently +/// reusing such a directory would graft one image's `/etc` onto another, so we +/// bail instead. +/// +/// This complements `validate_update`, which only checks `switch` (and `Skip`s +/// for `upgrade`); the `img_pulled == None` path also reaches `do_upgrade` +/// without going through it, so the guard is enforced here against the real +/// deploy key. +fn ensure_no_deploy_collision(host: &Host, deploy_id: &Sha512HashValue) -> Result<()> { + let deploy_hex = deploy_id.to_hex(); + if let Some(collision) = host + .all_composefs_deployments()? + .iter() + .find(|d| d.deployment.verity == deploy_hex) + { + anyhow::bail!( + "Target image has the same fs-verity digest as the existing {:?} deployment.", + collision.ty, + ); + } + Ok(()) +} + /// Performs the Update or Switch operation #[context("Performing Upgrade Operation")] pub(crate) async fn do_upgrade( @@ -264,56 +300,83 @@ pub(crate) async fn do_upgrade( ) .await?; - // If the target image produces the same fs-verity digest as any existing - // deployment (booted, staged, rollback, or pinned), error out. Two images - // from different sources can have identical content; we cannot silently reuse - // an existing state directory whose /etc was seeded from a different image. - let all_deployments = host.all_composefs_deployments()?; - if let Some(collision) = all_deployments - .iter() - .find(|d| d.deployment.verity == id.to_hex()) - { - anyhow::bail!( - "Target image has the same fs-verity digest as the existing {:?} deployment.", - collision.ty, - ); - } - let Some(entry) = entries.iter().next() else { anyhow::bail!("No boot entries!"); }; + let boot_type = BootType::from(entry); + + // Mounting just needs *a* valid bootable EROFS to read boot resources from; + // V1 and V2 are byte-distinct serializations of the same filesystem, so `id` + // (the repo-default boot image from generate_boot_image) always works and is + // guaranteed to exist. let mounted_fs = Dir::reopen_dir( &repo .mount(&id.to_hex()) .context("Failed to mount composefs image")?, )?; - let boot_type = BootType::from(entry); - - let boot_digest = match boot_type { - BootType::Bls => setup_composefs_bls_boot( - BootSetupType::Upgrade((storage, booted_cfs, &host)), - repo, - &id, - entry, - &mounted_fs, - )?, + // Open the OCI image to read both stored boot EROFS digests. The UKI may + // have been sealed with either the V1 or V2 boot image digest, so we need + // both for verification. + let manifest_oci_digest: composefs_oci::OciDigest = manifest_digest + .parse() + .with_context(|| format!("Parsing manifest digest {manifest_digest}"))?; + let oci_img = composefs_oci::oci_image::OciImage::open(&repo, &manifest_oci_digest, None) + .context("Opening OCI image to read boot image refs")?; + let boot_id_v1 = oci_img.boot_image_ref_v1().cloned(); + let boot_id_v2 = oci_img.boot_image_ref_v2().cloned(); + let boot_ids_owned: Vec = [boot_id_v1, boot_id_v2.clone()] + .into_iter() + .flatten() + .collect(); + let boot_ids: Vec<&Sha512HashValue> = boot_ids_owned.iter().collect(); + + // The deployment key must equal the digest in the next-boot composefs karg + // (see setup_composefs_boot for the full rationale). Provisional value for + // BLS (where bootc writes the karg from this same id); overridden for UKI by + // the digest the UKI cmdline actually carries. + let provisional_deploy_id = boot_id_v2.clone().unwrap_or_else(|| id.clone()); + + // Early collision check against the provisional key, before any ESP or + // bootloader writes. This catches BLS (where the provisional is the final + // key) and the common V2-sealed UKI case, avoiding leaving orphaned ESP + // entries behind on a bail. A UKI sealed with a non-default digest is still + // caught by the authoritative re-check below, once its real key is known. + ensure_no_deploy_collision(host, &provisional_deploy_id)?; + + let (boot_digest, deploy_id) = match boot_type { + BootType::Bls => ( + setup_composefs_bls_boot( + BootSetupType::Upgrade((storage, booted_cfs, &host)), + repo, + &provisional_deploy_id, + entry, + &mounted_fs, + )?, + provisional_deploy_id, + ), BootType::Uki => setup_composefs_uki_boot( BootSetupType::Upgrade((storage, booted_cfs, &host)), repo, - &id, + &provisional_deploy_id, + &boot_ids, entries, )?, }; + // Authoritative collision check against the final deploy key. For UKI this + // may differ from the provisional checked above (the UKI may carry a + // non-default digest), so this is the load-bearing guarantee. + ensure_no_deploy_collision(host, &deploy_id)?; + write_composefs_state( &Utf8PathBuf::from("/sysroot"), - &id, + &deploy_id, imgref, Some(StagedDeployment { - depl_id: id.to_hex(), + depl_id: deploy_id.to_hex(), finalization_locked: opts.download_only, }), boot_type, @@ -335,7 +398,7 @@ pub(crate) async fn do_upgrade( ) .await?; - apply_upgrade(storage, booted_cfs, &id.to_hex(), opts).await + apply_upgrade(storage, booted_cfs, &deploy_id.to_hex(), opts).await } #[context("Upgrading composefs")] diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 782bed106..0dd3789d6 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -441,7 +441,7 @@ pub(crate) enum ContainerOpts { /// EROFS format version to use when computing the composefs digest. /// - /// V1 produces a `composefs.digest.v1=` karg (C-tool compatible). + /// V1 produces a `composefs.digest=v1-sha256-12:` karg (C-tool compatible). /// V2 produces the legacy `composefs=` karg (composefs-rs native). /// Must match the format version used when images were committed to the repository. #[clap(hide = true, long, default_value = "v2")] @@ -496,7 +496,7 @@ pub(crate) enum ContainerOpts { /// EROFS format version for `bootc container ukify --erofs-version`. #[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] pub(crate) enum ErofsVersionArg { - /// V1 EROFS (C-tool compatible, `composefs.digest.v1=` karg). + /// V1 EROFS (C-tool compatible, `composefs.digest=v1-sha256-12:` karg). V1, /// V2 EROFS (composefs-rs native, `composefs=` karg). Default. V2, diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 4ff6ab385..276b78c86 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2019,13 +2019,20 @@ async fn install_to_filesystem_impl( let imgref = &state.source.imageref; let img_manifest_config = get_container_manifest_and_config(&imgref).await?; crate::store::ensure_composefs_dir(&rootfs.physical_root)?; - // Use init_path since the repo may not exist yet during install + // Use init_path since the repo may not exist yet during install. + // Generate both V1 and V2 EROFS images (see initialize_composefs_repository); + // this config must match the one used there since it re-inits the same repo. + let mut config = composefs_ctl::composefs::repository::RepositoryConfig::new( + composefs_ctl::composefs::fsverity::Algorithm::SHA512, + ); + config.erofs_formats = composefs_ctl::composefs::erofs::format::FormatConfig { + default: composefs_ctl::composefs::erofs::format::FormatVersion::V1, + extra: [composefs_ctl::composefs::erofs::format::FormatVersion::V2].into(), + }; let (cfs_repo, _created) = crate::store::ComposefsRepository::init_path( &rootfs.physical_root, crate::store::COMPOSEFS, - composefs_ctl::composefs::repository::RepositoryConfig::new( - composefs_ctl::composefs::fsverity::Algorithm::SHA512, - ), + config, )?; crate::deploy::check_disk_space_composefs( &cfs_repo, diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 26ee48404..06f62671a 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -111,7 +111,7 @@ use ostree_ext::{gio, ostree}; use rustix::fs::Mode; use composefs::fsverity::Sha512HashValue; -use composefs::repository::RepositoryConfig; +use composefs::repository::{RepositoryConfig, RepositoryOpenError}; use composefs_ctl::composefs; use crate::bootc_composefs::backwards_compat::bcompat_boot::prepend_custom_prefix; @@ -619,19 +619,42 @@ impl Storage { let ostree = self.get_ostree()?; let ostree_repo = &ostree.repo(); let ostree_verity = ostree_ext::fsverity::is_verity_enabled(ostree_repo)?; - let config = { - let c = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512); - if !ostree_verity.enabled { - c.set_insecure() - } else { - c - } - }; if !ostree_verity.enabled { tracing::debug!("Setting insecure mode for composefs repo"); } - let (composefs, _created) = - ComposefsRepository::init_path(self.physical_root.open_dir(COMPOSEFS)?, ".", config)?; + + // This is a runtime open-or-create path: the repository was almost + // always already initialized at install time, with a format + // (V2_ONLY or BOTH) chosen then. init_path() asserts that the + // requested config exactly matches the on-disk meta.json, so a + // static config here would fail to open repos created with the + // other format. Prefer open_path(), which adopts whatever format + // is on disk, and only fall back to init_path() (with BOTH, to + // match the install path) when the repository does not yet exist. + let composefs_dir = self.physical_root.open_dir(COMPOSEFS)?; + let composefs = match ComposefsRepository::open_path(&composefs_dir, ".") { + Ok(mut repo) => { + if !ostree_verity.enabled { + repo.set_insecure(); + } + repo + } + Err(RepositoryOpenError::MetadataMissing) => { + let mut config = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512); + config.erofs_formats = composefs::erofs::format::FormatConfig { + default: composefs::erofs::format::FormatVersion::V1, + extra: [composefs::erofs::format::FormatVersion::V2].into(), + }; + let config = if !ostree_verity.enabled { + config.set_insecure() + } else { + config + }; + let (repo, _created) = ComposefsRepository::init_path(&composefs_dir, ".", config)?; + repo + } + Err(e) => return Err(e.into()), + }; let composefs = Arc::new(composefs); let r = Arc::clone(self.composefs.get_or_init(|| composefs)); Ok(r) diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 3c3d77737..924cf24a3 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -51,7 +51,9 @@ fn parse_cli_bool(s: &str) -> std::result::Result { match s { "1" | "true" => Ok(true), "0" | "false" => Ok(false), - other => Err(format!("invalid value '{other}' (expected 0, 1, true, or false)")), + other => Err(format!( + "invalid value '{other}' (expected 0, 1, true, or false)" + )), } }