From 5200ef4ea1786d04a99aa7d0848fdc0d7e689c18 Mon Sep 17 00:00:00 2001 From: Andrew Dunn Date: Mon, 5 Oct 2026 13:49:24 -0400 Subject: [PATCH 1/2] ci: Drop logically bound images from the composefs test images The composefs backend doesn't install logically bound images, so the three that the test images bind have been missing from each composefs test system, and the bound image plans already skip composefs. The next change makes such an install fail. These images select composefs themselves and bcvk can't pass --bound-images, so remove the bindings from the composefs variants, including the published -uki dev images, until the backend supports them. Related: #2540 Assisted-by: AI Signed-off-by: Andrew Dunn --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index a36bc5638..ea876c5ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -348,6 +348,9 @@ fi # target-base), and so without --bootloader too, which bcvk only takes with it. if [[ "${variant}" == composefs* ]]; then printf '[install]\nbootloader = "%s"\n' "${bootloader}" > /usr/lib/bootc/install/80-composefs-bootloader.toml + # The composefs backend doesn't install logically bound images yet: + # https://github.com/bootc-dev/bootc/issues/2540 + rm -vf /usr/lib/bootc/bound-images.d/* fi if [[ "${boot_type}" == "uki" ]]; then From a23bce7b2a2b0e90f836db98ca818c8420de2887 Mon Sep 17 00:00:00 2001 From: Andrew Dunn Date: Mon, 5 Oct 2026 13:49:24 -0400 Subject: [PATCH 2/2] install: Reject logically bound images with the composefs backend The composefs install path never reads /usr/lib/bootc/bound-images.d, so an image with logically bound images installs without them and exits 0. Until the backend can install them, fail instead. The check runs in prepare_install beside the other composefs option checks, so it fails before to-disk partitions or to-filesystem wipes anything, and it covers to-existing-root and images that select composefs themselves. --bound-images skip still goes ahead without them, as on ostree, and a malformed bound-images.d now fails a composefs install with the same error as an ostree one. Related: #2540 Assisted-by: AI Signed-off-by: Andrew Dunn --- crates/lib/src/boundimage.rs | 2 +- crates/lib/src/install.rs | 78 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/lib/src/boundimage.rs b/crates/lib/src/boundimage.rs index 31b4e5fb3..c99aeb6fb 100644 --- a/crates/lib/src/boundimage.rs +++ b/crates/lib/src/boundimage.rs @@ -18,7 +18,7 @@ use crate::store::Storage; /// The path in a root for bound images; this directory should only contain /// symbolic links to `.container` or `.image` files. -const BOUND_IMAGE_DIR: &str = "usr/lib/bootc/bound-images.d"; +pub(crate) const BOUND_IMAGE_DIR: &str = "usr/lib/bootc/bound-images.d"; /// A subset of data parsed from a `.image` or `.container` file with /// the minimal information necessary to fetch the image. diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 8f9f80fc4..bf589b91f 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -438,6 +438,31 @@ impl InstallComposefsOpts { } Ok(()) } + + /// The composefs backend doesn't install logically bound images yet + /// (), so fail rather + /// than install a system that is missing them. + pub(crate) fn validate_bound_images( + &self, + bound_images: BoundImagesOpt, + root: &Dir, + ) -> Result<()> { + if !self.composefs_backend || bound_images == BoundImagesOpt::Skip { + return Ok(()); + } + let images = crate::boundimage::query_bound_images(root)?; + if !images.is_empty() { + let images = images + .iter() + .map(|i| i.image.as_str()) + .collect::>() + .join(", "); + anyhow::bail!( + "Logically bound images are not supported with the composefs backend (found {images}); use --bound-images skip to install without them" + ); + } + Ok(()) + } } #[cfg(feature = "install-to-disk")] @@ -1778,6 +1803,8 @@ async fn prepare_install( composefs_options.composefs_backend |= composefs_required || composefs_default; composefs_options.validate(config_opts.bootloader.as_ref())?; + composefs_options.validate_bound_images(config_opts.bound_images, &rootfs)?; + // Read the file eagerly so we error out early, and before the mount changes // below hide a file bind mounted under e.g. /tmp. We may re-exec further down // and run this again with those mounts in place, so pass the content to the @@ -3168,6 +3195,57 @@ mod tests { assert_eq!(c.block_opts.device, "/dev/vda"); } + #[test] + fn test_composefs_opts_validate_bound_images() -> Result<()> { + use crate::boundimage::BOUND_IMAGE_DIR; + let unbound = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; + unbound.create_dir_all(BOUND_IMAGE_DIR)?; + let bound = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; + bound.create_dir_all("usr/share/containers/systemd")?; + bound.write( + "usr/share/containers/systemd/app.image", + "[Image]\nImage=quay.io/example/app:latest\n", + )?; + bound.create_dir_all(BOUND_IMAGE_DIR)?; + bound.symlink_contents( + "/usr/share/containers/systemd/app.image", + format!("{BOUND_IMAGE_DIR}/app.image"), + )?; + + // (composefs_backend, bound_images, root binds an image, valid) + let cases = [ + (false, BoundImagesOpt::Stored, true, true), + (true, BoundImagesOpt::Stored, false, true), + (true, BoundImagesOpt::Stored, true, false), + (true, BoundImagesOpt::Pull, true, false), + (true, BoundImagesOpt::Skip, true, true), + ]; + for (composefs_backend, bound_images, binds, valid) in cases { + let opts = InstallComposefsOpts { + composefs_backend, + ..Default::default() + }; + let root = if binds { &bound } else { &unbound }; + assert_eq!( + opts.validate_bound_images(bound_images, root).is_ok(), + valid, + "{opts:?} {bound_images:?} {binds}" + ); + } + + let opts = InstallComposefsOpts { + composefs_backend: true, + ..Default::default() + }; + assert_eq!( + opts.validate_bound_images(BoundImagesOpt::Stored, &bound) + .unwrap_err() + .to_string(), + "Logically bound images are not supported with the composefs backend (found quay.io/example/app:latest); use --bound-images skip to install without them" + ); + Ok(()) + } + #[test] fn source_fetch_reference_uses_config_id_for_host_images() { let cases = [