From 43230660f192a392bf3537f7e64636d10640997b Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 15 Apr 2026 16:05:00 +0530 Subject: [PATCH 1/6] ukify: Support dumpfile with bootc ukify Add a flag to create a dumpfile for `bootc ukify` command. This is extremely helpful for debugging Signed-off-by: Pragyan Poudyal --- crates/lib/src/cli.rs | 13 ++++++++++++- crates/lib/src/ukify.rs | 7 ++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index b1752c2c2..9866a0346 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -438,6 +438,10 @@ pub(crate) enum ContainerOpts { #[clap(long)] allow_missing_verity: bool, + /// Write a dumpfile to this path + #[clap(long)] + write_dumpfile_to: Option, + /// Additional arguments to pass to ukify (after `--`). #[clap(last = true)] args: Vec, @@ -1791,8 +1795,15 @@ async fn run_from_opt(opt: Opt) -> Result<()> { rootfs, kargs, allow_missing_verity, + write_dumpfile_to, args, - } => crate::ukify::build_ukify(&rootfs, &kargs, &args, allow_missing_verity), + } => crate::ukify::build_ukify( + &rootfs, + &kargs, + &args, + allow_missing_verity, + write_dumpfile_to.as_deref(), + ), ContainerOpts::Export { format, target, diff --git a/crates/lib/src/ukify.rs b/crates/lib/src/ukify.rs index 8e3b82b67..40ad491b3 100644 --- a/crates/lib/src/ukify.rs +++ b/crates/lib/src/ukify.rs @@ -31,6 +31,7 @@ pub(crate) fn build_ukify( extra_kargs: &[String], args: &[OsString], allow_missing_fsverity: bool, + write_dumpfile_to: Option<&Utf8Path>, ) -> Result<()> { // Warn if --karg is used (temporary workaround) if !extra_kargs.is_empty() { @@ -78,7 +79,7 @@ pub(crate) fn build_ukify( } // Compute the composefs digest - let composefs_digest = compute_composefs_digest(rootfs, None)?; + let composefs_digest = compute_composefs_digest(rootfs, write_dumpfile_to)?; // Get kernel arguments from kargs.d let mut cmdline = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?; @@ -131,7 +132,7 @@ mod tests { let tempdir = tempfile::tempdir().unwrap(); let path = Utf8Path::from_path(tempdir.path()).unwrap(); - let result = build_ukify(path, &[], &[], false); + let result = build_ukify(path, &[], &[], false, None); assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( @@ -149,7 +150,7 @@ mod tests { fs::create_dir_all(tempdir.path().join("boot/EFI/Linux")).unwrap(); fs::write(tempdir.path().join("boot/EFI/Linux/test.efi"), b"fake uki").unwrap(); - let result = build_ukify(path, &[], &[], false); + let result = build_ukify(path, &[], &[], false, None); assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( From 5f3be0ad6c2be4cbf41ab0c75bf3a13b50df6d41 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 15 Apr 2026 16:07:14 +0530 Subject: [PATCH 2/6] cli/status: Show FsVerity enforcement status For the status command for composefs backend, in verbose mode, show whether FsVerity is enforced or not. This is also helpful for us in tests for UKI as while building a UKI we'd want to know whether the current system has FsVerity enforced or not. Reading `/proc/cmdline` is an option, but a concrete API helps immensely Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/status.rs | 13 +++++++++++-- crates/lib/src/spec.rs | 2 ++ crates/lib/src/status.rs | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 58a93638a..f5a17ab8c 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -450,6 +450,7 @@ async fn boot_entry_from_composefs_deployment( storage: &Storage, origin: tini::Ini, verity: &str, + missing_verity_allowed: bool, ) -> Result { let image = match origin.get::("origin", ORIGIN_CONTAINER) { Some(img_name_from_config) => { @@ -502,6 +503,7 @@ async fn boot_entry_from_composefs_deployment( boot_type, bootloader: get_bootloader()?, boot_digest, + missing_verity_allowed, }), soft_reboot_capable: false, }; @@ -784,8 +786,15 @@ async fn composefs_deployment_status_from( let ini = tini::Ini::from_string(&config) .with_context(|| format!("Failed to parse file {verity_digest}.origin as ini"))?; - let mut boot_entry = - boot_entry_from_composefs_deployment(storage, ini, &verity_digest).await?; + let mut boot_entry = boot_entry_from_composefs_deployment( + storage, + ini, + &verity_digest, + // We will either have verity enforced or not (possible but we don't allow it) + // There won't be two deployments with one enforcing verity and one not + cmdline.allow_missing_fsverity, + ) + .await?; // SAFETY: boot_entry.composefs will always be present let boot_type_from_origin = boot_entry.composefs.as_ref().unwrap().boot_type; diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index 15428d451..f06d056d1 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -270,6 +270,8 @@ pub struct BootEntryComposefs { /// The sha256sum of vmlinuz + initrd /// Only `Some` for Type1 boot entries pub boot_digest: Option, + /// Whether fs-verity validation is optional + pub missing_verity_allowed: bool, } /// A bootable entry diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index ec3cd4e1d..c6c3e8232 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -606,6 +606,27 @@ fn write_download_only( Ok(()) } +fn write_fsverity_enforcement( + mut out: impl Write, + entry: &crate::spec::BootEntry, + prefix_len: usize, +) -> Result<()> { + if let Some(cfs) = &entry.composefs { + write_row_name(&mut out, "FsVerity", prefix_len)?; + writeln!( + out, + "{}", + if cfs.missing_verity_allowed { + "Not Enforced" + } else { + "Enforced" + } + )?; + }; + + Ok(()) +} + /// Render cached update information, showing what update is available. /// /// This is populated by a previous `bootc upgrade --check` that found @@ -734,6 +755,8 @@ fn human_render_slot( // Show soft-reboot capability write_soft_reboot(&mut out, entry, prefix_len)?; + write_fsverity_enforcement(&mut out, entry, prefix_len)?; + // Show download-only lock status write_download_only(&mut out, slot, entry, prefix_len)?; } From 7caf888c427f9af8d4b9509cd47e2aa73650f181 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 15 Apr 2026 16:28:51 +0530 Subject: [PATCH 3/6] tests: Add integration tests for unsealed UKI For unsealed UKIs now we install systemd-ukify in our container images and also copy our UKI build scripts in the image to help us build UKIs in our tests. We don't yet have all tests for sealed UKIs because we don't have a proper way of passing our keys to the test VMs A nu shell function wraps all container image definitions and updates them to also build for UKI images Update tests to also work with UKIs Signed-off-by: Pragyan Poudyal --- Dockerfile | 33 +++++++++++++++- Justfile | 2 +- contrib/packaging/install-rpm-and-setup | 2 +- contrib/packaging/switch-to-sdboot | 2 +- tmt/plans/integration.fmf | 7 ++++ tmt/tests/booted/tap.nu | 38 +++++++++++++++++++ .../booted/test-download-only-upgrade.nu | 10 +++-- .../booted/test-image-pushpull-upgrade.nu | 10 +++-- tmt/tests/booted/test-image-upgrade-reboot.nu | 9 +++-- .../test-install-to-filesystem-var-mount.sh | 37 ++++++++++++++++-- tmt/tests/booted/test-rollback.nu | 5 ++- .../booted/test-soft-reboot-selinux-policy.nu | 6 +-- tmt/tests/booted/test-soft-reboot.nu | 12 +++--- tmt/tests/booted/test-upgrade-tag.nu | 10 +++-- 14 files changed, 150 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index ce5170955..6cd2df42a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,10 +57,31 @@ FROM scratch as base COPY --from=target-base /target-rootfs/ / # SKIP_CONFIGS=1 skips LBIs, test kargs, and install configs (for FCOS testing) ARG SKIP_CONFIGS +ARG boot_type +ARG seal_state # Use tmpfs for /run and /tmp with bind mounts inside to avoid leaking mount stubs into the image RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ - --mount=type=bind,from=src,src=/src/hack,target=/run/hack \ + --mount=type=bind,from=src,src=/src/hack,target=/run/hack <<-EOF + set -ex + cd /run/hack/ && SKIP_CONFIGS="${SKIP_CONFIGS}" ./provision-derived.sh + + pkgs_to_install=() + if [[ "${seal_state}" == "sealed" ]]; then + pkgs_to_install+=(sbsigntools) + fi + + # Install systemd-ukify and systemd-boot for UKIs + # This also installs systemd-boot for the grub UKI case which is not ideal... + if [[ "${boot_type}" == "uki" ]]; then + pkgs_to_install+=(systemd-ukify) + fi + + if [[ ${#pkgs_to_install[@]} -gt 0 ]]; then + dnf install -y "${pkgs_to_install[@]}" + fi +EOF + # Note we don't do any customization here yet # Mark this as a test image LABEL bootc.testimage="1" @@ -165,14 +186,24 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp FROM base as base-penultimate ARG variant ARG bootloader +ARG boot_type + # Switch to a signed systemd-boot, if configured RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ --mount=type=bind,from=sdboot-signed,src=/,target=/run/sdboot-signed </dev/null; then fi # First install the unsigned systemd-boot RPM to get the package in place -rpm -Uvh "${src}"/*.rpm +rpm -Uvh --replacepkgs "${src}"/*.rpm # Now find where it installed the binary and override with our signed version sdboot=$(ls /usr/lib/systemd/boot/efi/systemd-boot*.efi) diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index ce7469453..0c2de4689 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -196,6 +196,13 @@ execute: test: - /tmt/tests/tests/test-35-composefs-gc +/plan-35-composefs-gc-uki: + summary: Test composefs garbage collection for UKI + discover: + how: fmf + test: + - /tmt/tests/tests/test-35-composefs-gc-uki + /plan-35-upgrade-preflight-disk-check: summary: Verify pre-flight disk space check rejects images with inflated layer sizes discover: diff --git a/tmt/tests/booted/tap.nu b/tmt/tests/booted/tap.nu index 34a5fc085..b8b0b3f7e 100644 --- a/tmt/tests/booted/tap.nu +++ b/tmt/tests/booted/tap.nu @@ -74,3 +74,41 @@ rm -vrf /usr/lib/bootc/bound-images.d ($cmd) " } + +export def make_uki_containerfile [containerfile: string] { + let is_cfs = (is_composefs) + + if not $is_cfs { + return $containerfile + } + + let st = bootc status --json | from json + let is_uki = ($st.status.booted.composefs.bootType | str downcase) == "uki" + + if not $is_uki { + return $containerfile + } + + let allow_missing_verity = $st.status.booted.composefs.missingVerityAllowed + # TODO: Handle sealed UKI + let seal_state = "unsealed" + + let uki_stuff = $" + FROM base as base-final + RUN rm -rf /boot/EFI/Linux/*.efi + + FROM base as sealed-uki + RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \\ + --mount=type=bind,from=base-final,src=/,target=/run/target \\ + /usr/bin/seal-uki /run/target /out /run/secrets ($allow_missing_verity) ($seal_state) + + FROM base-final + + # Copy the sealed UKI and finalize the image remove raw kernel, create symlinks + RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \\ + --mount=type=bind,from=sealed-uki,src=/,target=/run/sealed-uki \\ + /usr/bin/finalize-uki /run/sealed-uki/out + " + + return $"($containerfile)\n($uki_stuff)" +} diff --git a/tmt/tests/booted/test-download-only-upgrade.nu b/tmt/tests/booted/test-download-only-upgrade.nu index 3f2bd7611..59dcdc3f5 100644 --- a/tmt/tests/booted/test-download-only-upgrade.nu +++ b/tmt/tests/booted/test-download-only-upgrade.nu @@ -46,9 +46,10 @@ def initial_build [] { "v1" | save testing-bootc-upgrade-apply # A simple derived container (v1) that adds a file - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base COPY testing-bootc-upgrade-apply /usr/share/testing-bootc-upgrade-apply -" | save Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save Dockerfile # Build it podman build -t $imgsrc . @@ -74,9 +75,10 @@ def second_boot [] { # Create test file v2 on host "v2" | save --force testing-bootc-upgrade-apply - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base COPY testing-bootc-upgrade-apply /usr/share/testing-bootc-upgrade-apply -" | save --force Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save --force Dockerfile podman build -t $imgsrc . # Now upgrade with --download-only (should set deployment to download-only mode) diff --git a/tmt/tests/booted/test-image-pushpull-upgrade.nu b/tmt/tests/booted/test-image-pushpull-upgrade.nu index aa79374e9..457349db6 100644 --- a/tmt/tests/booted/test-image-pushpull-upgrade.nu +++ b/tmt/tests/booted/test-image-pushpull-upgrade.nu @@ -44,10 +44,11 @@ def initial_build [] { mkdir usr/lib/bootc/kargs.d { kargs: $kargsv0 } | to toml | save usr/lib/bootc/kargs.d/05-testkargs.toml # A simple derived container that adds a file, but also injects some kargs - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base COPY usr/ /usr/ RUN echo test content > /usr/share/blah.txt -" | save Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save Dockerfile # Build it podman build -t localhost/bootc-derived . # Just sanity check it @@ -165,10 +166,11 @@ def second_boot [] { mkdir usr/lib/bootc/kargs.d { kargs: $kargsv1 } | to toml | save usr/lib/bootc/kargs.d/05-testkargs.toml - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base COPY usr/ /usr/ RUN echo test content2 > /usr/share/blah.txt -" | save Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save Dockerfile # Build it podman build -t localhost/bootc-derived . let booted_digest = $booted.imageDigest diff --git a/tmt/tests/booted/test-image-upgrade-reboot.nu b/tmt/tests/booted/test-image-upgrade-reboot.nu index 4343aa3c7..4fea5fea9 100644 --- a/tmt/tests/booted/test-image-upgrade-reboot.nu +++ b/tmt/tests/booted/test-image-upgrade-reboot.nu @@ -48,9 +48,12 @@ def initial_build [] { bootc image copy-to-storage # A simple derived container that adds a file - "FROM localhost/bootc -RUN touch /usr/share/testing-bootc-upgrade-apply -" | save Dockerfile + ( + tap make_uki_containerfile " + FROM localhost/bootc as base + RUN touch /usr/share/testing-bootc-upgrade-apply + ") | save Dockerfile + # Build it podman build -t $imgsrc . } diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index 4499b5be5..5025b0a76 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -26,9 +26,35 @@ bootc image copy-to-storage # Build a derived image that removes LBIs cat > /tmp/Containerfile.drop-lbis <<'EOF' -FROM localhost/bootc +FROM localhost/bootc as base RUN rm -rf /usr/lib/bootc/bound-images.d/* EOF + +is_composefs=$(bootc status --json | jq '.status.booted.composefs') +boot_type=$(bootc status --json | jq -r '.status.booted.composefs.bootType' | tr '[:upper:]' '[:lower:]') + +if [[ $is_composefs != "null" && $boot_type == "uki" ]]; then + allow_missing_verity=$(bootc status --json | jq -r '.status.booted.composefs.missingVerityAllowed') + seal_state="unsealed" + + cat >> /tmp/Containerfile.drop-lbis <<-EOF + FROM base as base-final + RUN rm -rf /boot/EFI/Linux/*.efi + + FROM base as sealed-uki + RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=bind,from=base-final,src=/,target=/run/target \ + /usr/bin/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state + + FROM base-final + + # Copy the sealed UKI and finalize the image remove raw kernel, create symlinks + RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=bind,from=sealed-uki,src=/,target=/run/sealed-uki \ + /usr/bin/finalize-uki /run/sealed-uki/out +EOF +fi + podman build -t "$TARGET_IMAGE" -f /tmp/Containerfile.drop-lbis # Create a 15GB sparse disk image in /var/tmp (not /tmp which may be tmpfs) @@ -116,13 +142,16 @@ mount | grep /var/mnt/target || true df -h /var/mnt/target /var/mnt/target/boot /var/mnt/target/boot/efi /var/mnt/target/var COMPOSEFS_BACKEND=() - -is_composefs=$(bootc status --json | jq '.status.booted.composefs') +KARGS=("--karg=root=UUID=$ROOT_UUID") if [[ $is_composefs != "null" ]]; then COMPOSEFS_BACKEND+=("--composefs-backend") tune2fs -O verity /dev/BL/var02 tune2fs -O verity /dev/BL/root02 + + if [[ $boot_type == "uki" ]]; then + KARGS=() + fi fi # Run bootc install to-filesystem from within the container image under test @@ -136,7 +165,7 @@ podman run \ bootc install to-filesystem \ --disable-selinux \ "${COMPOSEFS_BACKEND[@]}" \ - --karg=root=UUID="$ROOT_UUID" \ + "${KARGS[@]}" \ --root-mount-spec=UUID="$ROOT_UUID" \ --boot-mount-spec=UUID="$BOOT_UUID" \ /target diff --git a/tmt/tests/booted/test-rollback.nu b/tmt/tests/booted/test-rollback.nu index 0f2e2ee89..0afe377ac 100644 --- a/tmt/tests/booted/test-rollback.nu +++ b/tmt/tests/booted/test-rollback.nu @@ -42,9 +42,10 @@ def initial_switch [] { bootc image copy-to-storage print "Building derived container" - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base RUN echo 'This is the rollback target image' > /usr/share/bootc-rollback-marker -" | save Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save Dockerfile podman build -t $imgsrc . print $"Built derived image: ($imgsrc)" diff --git a/tmt/tests/booted/test-soft-reboot-selinux-policy.nu b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu index c8645f592..97db58bc1 100644 --- a/tmt/tests/booted/test-soft-reboot-selinux-policy.nu +++ b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu @@ -88,8 +88,8 @@ gpgkey=($gpgkey) # Installing a policy module will change the compiled policy checksum # Following Colin's suggestion and the composefs-rs example # We create a minimal policy module and install it - $" -FROM localhost/bootc + (tap make_uki_containerfile $" +FROM localhost/bootc as base ($repo_copy) # Install tools needed to build and install SELinux policy modules @@ -117,7 +117,7 @@ RUN < /usr/share/testfile-for-soft-reboot.txt -" | save Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save Dockerfile # Build it podman build -t localhost/bootc-derived . @@ -59,10 +60,11 @@ def second_boot [] { #assert equal (systemctl show -P SoftRebootsCount) "1" # A new derived with new kargs which should stop the soft reboot. - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base RUN echo test content > /usr/share/testfile-for-soft-reboot.txt -RUN echo 'kargs = ["foo1=bar2"]' | tee /usr/lib/bootc/kargs.d/00-foo1bar2.toml > /dev/null -" | save Dockerfile +RUN echo 'kargs = [\"foo1=bar2\"]' | tee /usr/lib/bootc/kargs.d/00-foo1bar2.toml > /dev/null +" + (tap make_uki_containerfile $dockerfile) | save Dockerfile # Build it podman build -t localhost/bootc-derived . diff --git a/tmt/tests/booted/test-upgrade-tag.nu b/tmt/tests/booted/test-upgrade-tag.nu index 18fdb3505..2125762c8 100644 --- a/tmt/tests/booted/test-upgrade-tag.nu +++ b/tmt/tests/booted/test-upgrade-tag.nu @@ -26,9 +26,10 @@ def initial_build [] { bootc image copy-to-storage # Build v1 image - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base RUN echo v1 content > /usr/share/bootc-tag-test.txt -" | save Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save Dockerfile podman build -t localhost/bootc-tag-test:v1 . # Verify v1 content @@ -39,9 +40,10 @@ RUN echo v1 content > /usr/share/bootc-tag-test.txt bootc switch --transport containers-storage localhost/bootc-tag-test:v1 # Build v2 image (different content) - use --force to overwrite Dockerfile - "FROM localhost/bootc + let dockerfile = $"FROM localhost/bootc as base RUN echo v2 content > /usr/share/bootc-tag-test.txt -" | save --force Dockerfile +" + (tap make_uki_containerfile $dockerfile) | save --force Dockerfile podman build -t localhost/bootc-tag-test:v2 . # Verify v2 content From d30c6bc0b174211c7ccaaaf1ddba0283c8a3289f Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 15 Apr 2026 16:34:27 +0530 Subject: [PATCH 4/6] test/gc: Add test for Composefs UKI GC Also, fix a logic error in the BLS GC test where we were checking for the non-existence of a non-existent path Explicitly disable composefs gc tests for ostree Signed-off-by: Pragyan Poudyal --- tmt/plans/integration.fmf | 14 +- tmt/tests/booted/test-composefs-gc-uki.nu | 167 ++++++++++++++++++++++ tmt/tests/booted/test-composefs-gc.nu | 16 ++- tmt/tests/tests.fmf | 5 + 4 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 tmt/tests/booted/test-composefs-gc-uki.nu diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index 0c2de4689..e8c36401d 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -196,13 +196,6 @@ execute: test: - /tmt/tests/tests/test-35-composefs-gc -/plan-35-composefs-gc-uki: - summary: Test composefs garbage collection for UKI - discover: - how: fmf - test: - - /tmt/tests/tests/test-35-composefs-gc-uki - /plan-35-upgrade-preflight-disk-check: summary: Verify pre-flight disk space check rejects images with inflated layer sizes discover: @@ -253,4 +246,11 @@ execute: test: - /tmt/tests/tests/test-40-install-karg-delete extra-fixme_skip_if_composefs: true + +/plan-41-composefs-gc-uki: + summary: Test composefs garbage collection for UKI + discover: + how: fmf + test: + - /tmt/tests/tests/test-41-composefs-gc-uki # END GENERATED PLANS diff --git a/tmt/tests/booted/test-composefs-gc-uki.nu b/tmt/tests/booted/test-composefs-gc-uki.nu new file mode 100644 index 000000000..b82c449bd --- /dev/null +++ b/tmt/tests/booted/test-composefs-gc-uki.nu @@ -0,0 +1,167 @@ +# number: 41 +# tmt: +# summary: Test composefs garbage collection for UKI +# duration: 30m + +use std assert +use tap.nu + +if not (tap is_composefs) { + exit 0 +} + +# bootc status +let st = bootc status --json | from json +let booted = $st.status.booted.image + +let uki_prefix = "bootc_composefs-" + +let is_uki = (($st.status.booted.composefs.bootType | str downcase) == "uki") + +if not $is_uki { + exit 0 +} + +# Create a large file in a new container image, then bootc switch to the image +def first_boot [] { + bootc image copy-to-storage + + mut containerfile = $" + FROM localhost/bootc as base + RUN dd if=/dev/zero of=/usr/share/large-test-file bs=1k count=1337 + RUN echo 'large-file-marker' | dd of=/usr/share/large-test-file conv=notrunc + " + + $containerfile = (tap make_uki_containerfile $containerfile) + + echo $containerfile | podman build -t localhost/bootc-first . -f - + + let current_time = (date now) + + bootc switch --transport containers-storage localhost/bootc-first + + # Find the large file's verity and save it + # nu has its own built in find which sucks, so we use the other one + # TODO: Replace this with some concrete API + # See: https://github.com/composefs/composefs-rs/pull/236 + let file_path = ( + /usr/bin/find /sysroot/composefs/objects -type f -size 1337k -newermt ($current_time | format date "%Y-%m-%d %H:%M:%S") + | xargs grep -lx "large-file-marker" + ) + + echo $file_path | save /var/large-file-marker-objpath + cat /var/large-file-marker-objpath + + echo $st.status.booted.composefs.verity | save /var/boot0-verity + + tmt-reboot +} + +# Create a container image derived from the first boot image +def second_boot [] { + mkdir /var/tmp/efi + mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi + + assert equal $booted.image.image "localhost/bootc-first" + assert ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot0-verity).efi" | path exists) + + echo $st.status.booted.composefs.verity | save /var/boot1-verity + + let path = cat /var/large-file-marker-objpath + + assert ($path | path exists) + + mut containerfile = echo " + FROM localhost/bootc as base + RUN echo 'second' > /usr/share/second + " + + $containerfile = (tap make_uki_containerfile $containerfile) + + echo $containerfile | podman build -t localhost/bootc-second . -f - + + bootc switch --transport containers-storage localhost/bootc-second + + tmt-reboot +} + +def third_boot [] { + mkdir /var/tmp/efi + mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi + + assert equal $booted.image.image "localhost/bootc-second" + assert ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot0-verity).efi" | path exists) + assert ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot1-verity).efi" | path exists) + + echo $st.status.booted.composefs.verity | save /var/boot2-verity + + # this is not deleted yet + let path = cat /var/large-file-marker-objpath + assert ($path | path exists) + + mut containerfile = echo " + FROM localhost/bootc as base + RUN echo 'third' > /usr/share/third + " + + $containerfile = (tap make_uki_containerfile $containerfile) + + echo $containerfile | podman build -t localhost/bootc-third . -f - + + bootc switch --transport containers-storage localhost/bootc-third + + tmt-reboot +} + + +def fourth_boot [] { + mkdir /var/tmp/efi + mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi + + assert equal $booted.image.image "localhost/bootc-third" + assert (not ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot0-verity).efi" | path exists)) + assert ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot1-verity).efi" | path exists) + assert ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot2-verity).efi" | path exists) + + echo $st.status.booted.composefs.verity | save /var/boot3-verity + + mut containerfile = " + FROM localhost/bootc as base + RUN echo 'another file' > /usr/share/another-one + " + + $containerfile = (tap make_uki_containerfile $containerfile) + + echo $containerfile | podman build -t localhost/bootc-final . -f - + + bootc switch --transport containers-storage localhost/bootc-final + tap ok +} + +def fifth_boot [] { + mkdir /var/tmp/efi + mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi + + assert equal $booted.image.image "localhost/bootc-final" + + assert (not ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot1-verity).efi" | path exists)) + assert ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot2-verity).efi" | path exists) + assert ($"/var/tmp/efi/EFI/Linux/bootc/($uki_prefix)(cat /var/boot3-verity).efi" | path exists) + + # We had this in boot1 (second boot) + let path = cat /var/large-file-marker-objpath + assert (not ($path | path exists)) + tap ok +} + +def main [] { + match $env.TMT_REBOOT_COUNT? { + null | "0" => first_boot, + "1" => second_boot, + "2" => third_boot, + "3" => fourth_boot, + "4" => fifth_boot, + $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, + } +} + diff --git a/tmt/tests/booted/test-composefs-gc.nu b/tmt/tests/booted/test-composefs-gc.nu index c8f93fea9..c62fdccfc 100644 --- a/tmt/tests/booted/test-composefs-gc.nu +++ b/tmt/tests/booted/test-composefs-gc.nu @@ -6,13 +6,17 @@ use std assert use tap.nu +if not (tap is_composefs) { + exit 0 +} + # bootc status let st = bootc status --json | from json let booted = $st.status.booted.image let dir_prefix = "bootc_composefs-" -if not (tap is_composefs) or ($st.status.booted.composefs.bootType | str downcase) == "uki" { +if ($st.status.booted.composefs.bootType | str downcase) == "uki" { exit 0 } @@ -52,11 +56,9 @@ def second_boot [] { assert equal $booted.image.image "localhost/bootc-derived" let path = cat /var/large-file-marker-objpath - assert ($path | path exists) # Create another image with a different initrd so we can test kernel + initrd cleanup - echo " FROM localhost/bootc @@ -89,13 +91,9 @@ def second_boot [] { tmt-reboot } -# The large file should've been GC'd as we switched to an image derived from the original one def third_boot [] { assert equal $booted.image.image "localhost/bootc-derived-initrd" - let path = cat /var/large-file-marker-objpath - assert (not ($"/sysroot/composefs/objects/($path)" | path exists)) - # Also assert we have two different kernel + initrd pairs let booted_verity = (bootc status --json | from json).status.booted.composefs.verity @@ -158,6 +156,10 @@ def fifth_boot [] { mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi } + # The large file should be GC'd in the previous switch + let path = cat /var/large-file-marker-objpath + assert (not ($path | path exists)) + assert equal $booted.image.image "localhost/bootc-final" assert (not ((cat /var/to-be-deleted-kernel | path exists))) diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index 0d081d5ef..53476f784 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -153,3 +153,8 @@ check: summary: Test bootc install --karg-delete duration: 30m test: nu booted/test-install-karg-delete.nu + +/test-41-composefs-gc-uki: + summary: Test composefs garbage collection for UKI + duration: 30m + test: nu booted/test-composefs-gc-uki.nu From 4e2622508da53adbacac16b5cb84b62f125b70b6 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 17 Apr 2026 15:33:49 +0530 Subject: [PATCH 5/6] test: Skip `soft-reboot-selinux-policy` for UKI We skip this test for UKI as the container image that's built has different mtime for /var and /etc which results in different digests inside the UKI (computed with stitched overlayfs) vs in the final layer ``` 5c5 < /etc 0 40755 94 0 0 0 1776409773.0 - - - security.selinux=system_u:object_r:etc_t:s0 --- > /etc 0 40755 94 0 0 0 1776409786.0 - - - security.selinux=system_u:object_r:etc_t:s0 55015c55015 < /var 0 40755 7 0 0 0 1776409756.0 - - - security.selinux=system_u:object_r:var_t:s0 --- > /var 0 40755 7 0 0 0 1776409786.0 - - - security.selinux=system_u:object_r:var_t:s0 ``` Probably a different issue as we use "FROM scratch" but Ref: https://github.com/composefs/composefs-rs/issues/132 Signed-off-by: Pragyan Poudyal --- crates/xtask/src/tmt.rs | 51 +++++++++++++++++++ .../booted/test-soft-reboot-selinux-policy.nu | 3 ++ 2 files changed, 54 insertions(+) diff --git a/crates/xtask/src/tmt.rs b/crates/xtask/src/tmt.rs index 2fc9b9db8..94136fc07 100644 --- a/crates/xtask/src/tmt.rs +++ b/crates/xtask/src/tmt.rs @@ -23,6 +23,7 @@ const FIELD_SUMMARY: &str = "summary"; const FIELD_ADJUST: &str = "adjust"; const FIELD_FIXME_SKIP_IF_COMPOSEFS: &str = "fixme_skip_if_composefs"; +const FIELD_FIXME_SKIP_IF_UKI: &str = "fixme_skip_if_uki"; // bcvk options const BCVK_OPT_BIND_STORAGE_RO: &str = "--bind-storage-ro"; @@ -246,6 +247,7 @@ fn verify_ssh_connectivity(sh: &Shell, port: u16, key_path: &Utf8Path) -> Result struct PlanMetadata { try_bind_storage: bool, skip_if_composefs: bool, + skip_if_uki: bool, } /// Parse integration.fmf to extract extra-try_bind_storage for all plans @@ -286,6 +288,7 @@ fn parse_plan_metadata( .and_modify(|m| m.try_bind_storage = b) .or_insert(PlanMetadata { try_bind_storage: b, + skip_if_uki: false, skip_if_composefs: false, }); } @@ -301,6 +304,23 @@ fn parse_plan_metadata( .and_modify(|m| m.skip_if_composefs = b) .or_insert(PlanMetadata { skip_if_composefs: b, + skip_if_uki: false, + try_bind_storage: false, + }); + } + } + + if let Some(skip_if_uki) = plan_data.get(&serde_yaml::Value::String(format!( + "extra-{}", + FIELD_FIXME_SKIP_IF_UKI + ))) { + if let Some(b) = skip_if_uki.as_bool() { + plan_metadata + .entry(plan_name.to_string()) + .and_modify(|m| m.skip_if_uki = b) + .or_insert(PlanMetadata { + skip_if_uki: b, + skip_if_composefs: false, try_bind_storage: false, }); } @@ -407,6 +427,16 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { }); } + if matches!(args.boot_type, crate::BootType::Uki) { + plans.retain(|plan| { + !plan_metadata + .iter() + .find(|(key, _)| plan.ends_with(key.as_str())) + .map(|(_, v)| v.skip_if_uki) + .unwrap_or(false) + }); + } + if plans.len() < original_plans_count { println!( "Filtered from {} to {} plan(s) based on arguments: {:?}", @@ -910,6 +940,8 @@ struct TestDef { try_bind_storage: bool, /// Whether to skip this test for composefs backend skip_if_composefs: bool, + /// Whether to skip this test for images with UKI + skip_if_uki: bool, /// TMT fmf attributes to pass through (summary, duration, adjust, etc.) tmt: serde_yaml::Value, } @@ -1011,12 +1043,24 @@ pub(crate) fn update_integration() -> Result<()> { .and_then(|v| v.as_bool()) .unwrap_or(false); + let skip_if_uki = metadata + .extra + .as_mapping() + .and_then(|m| { + m.get(&serde_yaml::Value::String( + FIELD_FIXME_SKIP_IF_UKI.to_string(), + )) + }) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + tests.push(TestDef { number: metadata.number, name: display_name, test_command, try_bind_storage, skip_if_composefs, + skip_if_uki, tmt: metadata.tmt, }); } @@ -1154,6 +1198,13 @@ pub(crate) fn update_integration() -> Result<()> { ); } + if test.skip_if_uki { + plan_value.insert( + serde_yaml::Value::String(format!("extra-{}", FIELD_FIXME_SKIP_IF_UKI)), + serde_yaml::Value::Bool(true), + ); + } + plans_mapping.insert( serde_yaml::Value::String(plan_key), serde_yaml::Value::Mapping(plan_value), diff --git a/tmt/tests/booted/test-soft-reboot-selinux-policy.nu b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu index 97db58bc1..4e2706804 100644 --- a/tmt/tests/booted/test-soft-reboot-selinux-policy.nu +++ b/tmt/tests/booted/test-soft-reboot-selinux-policy.nu @@ -2,8 +2,11 @@ # tmt: # summary: Test soft reboot with SELinux policy changes # duration: 30m +# extra: +# fixme_skip_if_uki: true # # Verify that soft reboot is blocked when SELinux policies differ + use std assert use tap.nu From 7fa736d31c5a6381e704797b067de3937135c715 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 17 Apr 2026 15:35:33 +0530 Subject: [PATCH 6/6] Run cargo xtask update-generated Signed-off-by: Pragyan Poudyal --- docs/src/host-v1.schema.json | 7 ++++++- docs/src/man/bootc-container-ukify.8.md | 4 ++++ tmt/plans/integration.fmf | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/src/host-v1.schema.json b/docs/src/host-v1.schema.json index 53d6580a8..eee565acd 100644 --- a/docs/src/host-v1.schema.json +++ b/docs/src/host-v1.schema.json @@ -143,6 +143,10 @@ "description": "Whether we boot using systemd or grub", "$ref": "#/$defs/Bootloader" }, + "missingVerityAllowed": { + "description": "Whether fs-verity validation is optional", + "type": "boolean" + }, "verity": { "description": "The erofs verity", "type": "string" @@ -151,7 +155,8 @@ "required": [ "verity", "bootType", - "bootloader" + "bootloader", + "missingVerityAllowed" ] }, "BootEntryOstree": { diff --git a/docs/src/man/bootc-container-ukify.8.md b/docs/src/man/bootc-container-ukify.8.md index 83b24a9c0..bad9e10cc 100644 --- a/docs/src/man/bootc-container-ukify.8.md +++ b/docs/src/man/bootc-container-ukify.8.md @@ -31,6 +31,10 @@ Any additional arguments after `--` are passed through to ukify unchanged. Make fs-verity validation optional in case the filesystem doesn't support it +**--write-dumpfile-to**=*WRITE_DUMPFILE_TO* + + Write a dumpfile to this path + # EXAMPLES diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index e8c36401d..0f0eb6abb 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -135,6 +135,7 @@ execute: how: fmf test: - /tmt/tests/tests/test-29-soft-reboot-selinux-policy + extra-fixme_skip_if_uki: true /plan-30-install-unified-flag: summary: Test bootc install with experimental unified storage flag