From ce8443a7bb60cdf0c43aa42d5c41e66070b91a66 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 14 May 2026 12:25:09 +0530 Subject: [PATCH 1/3] ukify: Allow passing custom kernel, initramfs While building a sealed UKI image we'd want to remove the original kernel + initramfs from the final image and have only the final UKI present. This was not possible before as `bootc container ukify` expected kernel + initramfs to be present in `usr/lib/modules` of container root Fixes: #2185 Signed-off-by: Pragyan Poudyal wip --- crates/lib/src/cli.rs | 33 +++++++++++++++ crates/lib/src/ukify.rs | 54 ++++++++++++++++--------- docs/src/man/bootc-container-ukify.8.md | 12 ++++++ 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 48559f34d..cb02c641c 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -442,6 +442,19 @@ pub(crate) enum ContainerOpts { #[clap(long)] write_dumpfile_to: Option, + /// The kernel version. + /// Required if kernel is passed + #[clap(long, requires = "kernel")] + kver: Option, + + /// Path to the kernel + #[clap(long, requires = "initramfs", requires = "kver")] + kernel: Option, + + /// Path to the initramfs + #[clap(long, requires = "kernel")] + initramfs: Option, + /// Additional arguments to pass to ukify (after `--`). #[clap(last = true)] args: Vec, @@ -1893,12 +1906,32 @@ async fn run_from_opt(opt: Opt) -> Result<()> { kargs, allow_missing_verity, write_dumpfile_to, + kernel, + kver, + initramfs, args, } => { + let kernel = match (kernel, initramfs) { + (Some(path), Some(initramfs)) => Some(crate::kernel::KernelInternal { + kernel: crate::kernel::Kernel { + unified: false, + version: kver + .ok_or_else(|| anyhow::anyhow!("Expected kver to be present"))?, + }, + k_type: crate::kernel::KernelType::Vmlinuz { path, initramfs }, + }), + + (None, None) => None, + + // Shouldn't happen due to clap constraints but for sanity + _ => anyhow::bail!("--kernel and --initramfs must be provided together"), + }; + crate::ukify::build_ukify( &rootfs, &kargs, &args, + kernel, allow_missing_verity, write_dumpfile_to.as_deref(), ) diff --git a/crates/lib/src/ukify.rs b/crates/lib/src/ukify.rs index c36cdb360..06008e7ef 100644 --- a/crates/lib/src/ukify.rs +++ b/crates/lib/src/ukify.rs @@ -15,6 +15,7 @@ use fn_error_context::context; use crate::bootc_composefs::digest::compute_composefs_digest; use crate::bootc_composefs::status::ComposefsCmdline; +use crate::kernel::KernelInternal; /// Build a UKI from the given rootfs. /// @@ -30,6 +31,7 @@ pub(crate) async fn build_ukify( rootfs: &Utf8Path, extra_kargs: &[String], args: &[OsString], + kernel: Option, allow_missing_fsverity: bool, write_dumpfile_to: Option<&Utf8Path>, ) -> Result<()> { @@ -52,12 +54,14 @@ pub(crate) async fn build_ukify( let root = Dir::open_ambient_dir(rootfs, cap_std_ext::cap_std::ambient_authority()) .with_context(|| format!("Opening rootfs {rootfs}"))?; - // Find the kernel - let kernel = crate::kernel::find_kernel(&root)? - .ok_or_else(|| anyhow::anyhow!("No kernel found in {rootfs}"))?; + let kernel_final = match kernel { + Some(ref kernel) => kernel, + None => &crate::kernel::find_kernel(&root)? + .ok_or_else(|| anyhow::anyhow!("No kernel found in {rootfs}"))?, + }; // Extract vmlinuz and initramfs paths, or bail if this is already a UKI - let (vmlinuz_path, initramfs_path) = match kernel.k_type { + let (vmlinuz_path, initramfs_path) = match &kernel_final.k_type { crate::kernel::KernelType::Vmlinuz { path, initramfs } => (path, initramfs), crate::kernel::KernelType::Uki { path, .. } => { anyhow::bail!("Cannot build UKI: rootfs already contains a UKI at {path}"); @@ -65,17 +69,31 @@ pub(crate) async fn build_ukify( }; // Verify kernel and initramfs exist - if !root - .try_exists(&vmlinuz_path) - .context("Checking for vmlinuz")? - { - anyhow::bail!("Kernel not found at {vmlinuz_path}"); - } - if !root - .try_exists(&initramfs_path) - .context("Checking for initramfs")? - { - anyhow::bail!("Initramfs not found at {initramfs_path}"); + // + // NOTE: Not using cap_std here as the vmlinuz/initramfs path from + // args can be outside of "rootfs" + if kernel.is_some() { + if !vmlinuz_path.exists() { + anyhow::bail!("Kernel not found at {vmlinuz_path}"); + } + + if !initramfs_path.exists() { + anyhow::bail!("Initramfs not found at {initramfs_path}"); + } + } else { + if !root + .try_exists(&vmlinuz_path) + .context("Checking for vmlinuz")? + { + anyhow::bail!("Kernel not found at {vmlinuz_path}"); + } + + if !root + .try_exists(&initramfs_path) + .context("Checking for initramfs")? + { + anyhow::bail!("Initramfs not found at {initramfs_path}"); + } } // Compute the composefs digest @@ -105,7 +123,7 @@ pub(crate) async fn build_ukify( .arg("--initrd") .arg(&initramfs_path) .arg("--uname") - .arg(&kernel.kernel.version) + .arg(&kernel_final.kernel.version) .arg("--cmdline") .arg(&cmdline_str) .arg("--os-release") @@ -132,7 +150,7 @@ mod tests { let tempdir = tempfile::tempdir().unwrap(); let path = Utf8Path::from_path(tempdir.path()).unwrap(); - let result = build_ukify(path, &[], &[], false, None).await; + let result = build_ukify(path, &[], &[], None, false, None).await; assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( @@ -150,7 +168,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, None).await; + let result = build_ukify(path, &[], &[], None, false, None).await; assert!(result.is_err()); let err = format!("{:#}", result.unwrap_err()); assert!( diff --git a/docs/src/man/bootc-container-ukify.8.md b/docs/src/man/bootc-container-ukify.8.md index bad9e10cc..b77f37b0f 100644 --- a/docs/src/man/bootc-container-ukify.8.md +++ b/docs/src/man/bootc-container-ukify.8.md @@ -35,6 +35,18 @@ Any additional arguments after `--` are passed through to ukify unchanged. Write a dumpfile to this path +**--kver**=*KVER* + + The kernel version. Required if kernel is passed + +**--kernel**=*KERNEL* + + Path to the kernel + +**--initramfs**=*INITRAMFS* + + Path to the initramfs + # EXAMPLES From 64945a24abddfcc7778ed80ca7b53491c121b21d Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 14 May 2026 21:13:52 +0530 Subject: [PATCH 2/3] dockerfile/uki: Rework to remove kernel + initrd Now that we can pass kernel and initrd paths to `bootc ukify`, rework our UKI Dockerfile to remove kernel + initrd from the final layer and only keep the UKI This still will not *remove* the kernel + initrd from the tarball but have whiteout instead See https://github.com/bootc-dev/bootc/issues/2027#issuecomment-4244181869 Signed-off-by: Pragyan Poudyal --- Dockerfile | 46 +++++++++-- contrib/packaging/finalize-uki | 15 +--- contrib/packaging/seal-uki | 98 ++++++++++++++++------- crates/tests-integration/src/container.rs | 2 + tmt/tests/Dockerfile.upgrade | 34 ++++++-- tmt/tests/booted/tap.nu | 28 ++++++- 6 files changed, 166 insertions(+), 57 deletions(-) diff --git a/Dockerfile b/Dockerfile index e90ad035a..7e81776a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ # 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) + pkgs_to_install+=(systemd-ukify binutils) fi if [[ ${#pkgs_to_install[@]} -gt 0 ]]; then @@ -135,7 +135,10 @@ ARG pkgversion ARG SOURCE_DATE_EPOCH ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} # Build RPM directly from source, using cached target directory -RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=cache,target=/src/target \ + --mount=type=cache,target=/var/roothome \ + RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm # Build a systemd-sysext containing just the bootc binary. # Skips RPM machinery entirely for fast incremental rebuilds. @@ -218,7 +221,7 @@ COPY --from=update-generated-from-code /src/docs/src/*.schema.json /docs/src/ # ---- # Perform all filesystem transformations except generating the sealed UKI (if configured) -FROM base as base-penultimate +FROM base as base-penultimate-source ARG variant ARG bootloader ARG boot_type @@ -246,6 +249,10 @@ rm -rf /var/cache rm -rf /run/rhsm EORUN + +FROM base-penultimate-source as base-penultimate +ARG boot_type + # Configure the rootfs ARG rootfs="" RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ @@ -260,9 +267,19 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp COPY --from=packaging /usr-extras/ /usr/ # Clean up package manager caches 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=base-penultimate-source,src=/,target=/run/base-penultimate-src \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging <&2 - exit 1 -fi +kver=$1 +shift # Create the EFI directory structure mkdir -p /boot/EFI/Linux @@ -36,12 +31,6 @@ mkdir -p /boot/EFI/Linux target=/boot/EFI/Linux/${kver}.efi cp "${uki_src}/${kver}.efi" "${target}" -# Remove the raw kernel and initramfs since we're using a UKI now. -# NOTE: We intentionally keep these for now until bcvk is updated to extract -# kernel/initramfs from UKIs in subdirectories. Once bcvk PR #144 is fixed -# to look for .efi files in /usr/lib/modules//, we can uncomment this. -# rm -v "/usr/lib/modules/${kver}/vmlinuz" "/usr/lib/modules/${kver}/initramfs.img" - # NOTE: We used to create a symlink from /usr/lib/modules/${kver}/${kver}.efi to the UKI # for tooling compatibility. However, composefs-boot's find_uki_components() doesn't # handle symlinks correctly and fails with "is not a regular file". The UKI is already diff --git a/contrib/packaging/seal-uki b/contrib/packaging/seal-uki index 66de92ffd..c493aeeb4 100755 --- a/contrib/packaging/seal-uki +++ b/contrib/packaging/seal-uki @@ -2,32 +2,82 @@ # Generate a sealed UKI with embedded composefs digest set -xeuo pipefail -# Path to the desired root filesystem -target=$1 -shift -# Write to this directory -output=$1 -shift -# Path to secrets directory -secrets=$1 -shift -allow_missing_verity=$1 -shift -seal_state=$1 -shift - -if [[ $seal_state == "sealed" && $allow_missing_verity == "true" ]]; then +missing_verity=() + +while [ ! -z "${1:-}" ]; do + case "$1" in + # Path to the desired root filesystem + "--target") + target="$2" + shift + shift + ;; + + # Write to this directory + "--output") + output="$2" + shift + shift + ;; + + # Path to secrets directory + "--secrets") + secrets="$2" + shift + shift + ;; + + "--allow-missing-verity") + missing_verity=(--allow-missing-verity) + shift + ;; + + "--seal-state") + seal_state="$2" + shift + shift + ;; + + # The kernel version + "--kver") + kver="$2" + shift + shift + ;; + + # Path to the kernel + "--kernel") + kernel="$2" + shift + shift + ;; + + # Path to the initrd + "--initramfs") + initramfs="$2" + shift + shift + ;; + + * ) + echo "Argument $1 not understood" + exit 1 + ;; + esac +done + +if [[ $seal_state == "sealed" && ${#missing_verity[@]} -gt 0 ]]; then echo "Cannot have missing verity with sealed UKI" >&2 exit 1 fi -# Find the kernel version (needed for output filename) -kver=$(bootc container inspect --rootfs "${target}" --json | jq -r '.kernel.version') -if [ -z "$kver" ] || [ "$kver" = "null" ]; then - echo "Error: No kernel found" >&2 - exit 1 +if [[ -z $kernel || -z $initramfs || -z $kver ]]; then + echo "kernel, initramfs and kver are required" >&2 + exit 1 fi +kernel_params=(--kernel "$kernel" --initramfs "$initramfs" --kver "$kver") + mkdir -p "${output}" # Baseline ukify options @@ -45,12 +95,6 @@ fi # Baseline container ukify options containerukifyargs=(--rootfs "${target}") -missing_verity=() - -if [[ $allow_missing_verity == "true" ]]; then - missing_verity+=(--allow-missing-verity) -fi - # Build the UKI using bootc container ukify # This computes the composefs digest, reads kargs from kargs.d, and invokes ukify -bootc container ukify "${containerukifyargs[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}" +bootc container ukify "${containerukifyargs[@]}" "${kernel_params[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}" diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index a7d8e89d6..b03a69f58 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -51,6 +51,8 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { .as_bool() .expect("kernel.unified should be a boolean"); + println!("kernel: {kernel:#?}"); + let is_uki = std::env::var("BOOTC_boot_type").is_ok_and(|var| var == "uki"); if let Some(variant) = std::env::var("BOOTC_variant").ok() { diff --git a/tmt/tests/Dockerfile.upgrade b/tmt/tests/Dockerfile.upgrade index 561e2e0a7..02e50e3ac 100644 --- a/tmt/tests/Dockerfile.upgrade +++ b/tmt/tests/Dockerfile.upgrade @@ -13,6 +13,16 @@ ARG filesystem=ext4 FROM scratch AS packaging COPY contrib/packaging / +# Get kernel + initrd from the UKI +FROM localhost/bootc as kernel +ARG boot_type +RUN <<-EOF + if test "${boot_type}" = "uki"; then + objcopy -O binary --only-section=.initrd /boot/EFI/Linux/*.efi /boot/initramfs.img + objcopy -O binary --only-section=.linux /boot/EFI/Linux/*.efi /boot/vmlinuz + fi +EOF + # Create the upgrade content (a simple marker file). # For UKI builds, we also remove the existing UKI so that seal-uki can # regenerate it with the correct composefs digest for this derived image. @@ -36,16 +46,28 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=secret,id=secureboot_key \ --mount=type=secret,id=secureboot_cert \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + --mount=type=bind,from=kernel,src=/,target=/run/kernel \ --mount=type=bind,from=upgrade-base,src=/,target=/run/target < Date: Fri, 15 May 2026 12:51:30 +0530 Subject: [PATCH 3/3] test/integration: Test vmlinuz non-existence with UKI vmlinuz and intrd should not be present in UKI images; add test for the same Signed-off-by: Pragyan Poudyal --- crates/tests-integration/src/container.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index b03a69f58..57dc3af1d 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -1,8 +1,8 @@ use indoc::indoc; use scopeguard::defer; use serde::Deserialize; -use std::fs; use std::process::Command; +use std::{fs, path::Path}; use anyhow::{Context, Result}; use camino::Utf8Path; @@ -51,8 +51,6 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { .as_bool() .expect("kernel.unified should be a boolean"); - println!("kernel: {kernel:#?}"); - let is_uki = std::env::var("BOOTC_boot_type").is_ok_and(|var| var == "uki"); if let Some(variant) = std::env::var("BOOTC_variant").ok() { @@ -74,6 +72,18 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { ); // Version should be non-empty after stripping extension assert!(!version.is_empty(), "version should not be empty for UKI"); + + // For UKI make sure vmlinuz + initrd don't exist + let usr_lib_mod = Path::new("/usr/lib/modules").join(version); + assert!(usr_lib_mod.exists(), "'{usr_lib_mod:?}' does not exist"); + assert!( + !usr_lib_mod.join("vmlinuz").exists(), + "vmlinuz should not exist for UKI" + ); + assert!( + !usr_lib_mod.join("initramfs.img").exists(), + "initramfs should not exist for UKI" + ); } o => eprintln!("notice: Unhandled variant for kernel check: {o:?}"), }