diff --git a/contrib/packaging/seal-uki b/contrib/packaging/seal-uki index 67442f536..9684dc8be 100755 --- a/contrib/packaging/seal-uki +++ b/contrib/packaging/seal-uki @@ -12,15 +12,7 @@ shift secrets=$1 shift -# Compute the composefs digest from the target rootfs -composefs_digest=$(bootc container compute-composefs-digest "${target}") - -# Build the kernel command line -# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826 -# TODO: pick up kargs from /usr/lib/bootc/kargs.d -cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw" - -# Find the kernel version +# 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 @@ -29,12 +21,14 @@ fi mkdir -p "${output}" -ukify build \ - --linux "${target}/usr/lib/modules/${kver}/vmlinuz" \ - --initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \ - --uname="${kver}" \ - --cmdline "${cmdline}" \ - --os-release "@${target}/usr/lib/os-release" \ +# Build the UKI using bootc container ukify +# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify +# +# WORKAROUND: SELinux must be permissive for sealed UKI boot +# See https://github.com/bootc-dev/bootc/issues/1826 +bootc container ukify --rootfs "${target}" \ + --karg enforcing=0 \ + -- \ --signtool sbsign \ --secureboot-private-key "${secrets}/secureboot_key" \ --secureboot-certificate "${secrets}/secureboot_cert" \ diff --git a/contrib/packaging/usr-extras/lib/bootc/kargs.d/10-rootfs-rw.toml b/contrib/packaging/usr-extras/lib/bootc/kargs.d/10-rootfs-rw.toml new file mode 100644 index 000000000..76162303e --- /dev/null +++ b/contrib/packaging/usr-extras/lib/bootc/kargs.d/10-rootfs-rw.toml @@ -0,0 +1,2 @@ +# Mount the root filesystem read-write +kargs = ["rw"] diff --git a/contrib/packaging/usr-extras/lib/bootc/kargs.d/21-console-hvc0.toml b/contrib/packaging/usr-extras/lib/bootc/kargs.d/21-console-hvc0.toml index 2ebed1836..00aee77b2 100644 --- a/contrib/packaging/usr-extras/lib/bootc/kargs.d/21-console-hvc0.toml +++ b/contrib/packaging/usr-extras/lib/bootc/kargs.d/21-console-hvc0.toml @@ -1,2 +1,3 @@ # https://bugzilla.redhat.com/show_bug.cgi?id=2353887 -kargs = ["console=hvc0"] +# console=ttyS0 for QEMU serial, console=hvc0 for virtio/Xen console +kargs = ["console=ttyS0,115200n8", "console=hvc0"] diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index f5bffd012..5ff4233b6 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -392,6 +392,29 @@ pub(crate) enum ContainerOpts { /// Identifier for image; if not provided, the running image will be used. image: Option, }, + /// Build a Unified Kernel Image (UKI) using ukify. + /// + /// This command computes the necessary arguments from the container image + /// (kernel, initrd, cmdline, os-release) and invokes ukify with them. + /// Any additional arguments after `--` are passed through to ukify unchanged. + /// + /// Example: + /// bootc container ukify --rootfs /target -- --output /output/uki.efi + Ukify { + /// Operate on the provided rootfs. + #[clap(long, default_value = "/")] + rootfs: Utf8PathBuf, + + /// Additional kernel arguments to append to the cmdline. + /// Can be specified multiple times. + /// This is a temporary workaround and will be removed. + #[clap(long = "karg", hide = true)] + kargs: Vec, + + /// Additional arguments to pass to ukify (after `--`). + #[clap(last = true)] + args: Vec, + }, } /// Subcommands which operate on images. @@ -1598,6 +1621,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> { Ok(()) } + ContainerOpts::Ukify { + rootfs, + kargs, + args, + } => crate::ukify::build_ukify(&rootfs, &kargs, &args), }, Opt::Completion { shell } => { use clap_complete::aot::generate; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 75ac7fda0..3536c14cb 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1587,7 +1587,7 @@ async fn prepare_install( let composefs_required = if let Some(root) = target_rootfs.as_ref() { crate::kernel::find_kernel(root)? - .map(|k| k.unified) + .map(|k| k.kernel.unified) .unwrap_or(false) } else { false diff --git a/crates/lib/src/kernel.rs b/crates/lib/src/kernel.rs index b7d04d183..c66841fce 100644 --- a/crates/lib/src/kernel.rs +++ b/crates/lib/src/kernel.rs @@ -7,6 +7,7 @@ use std::path::Path; use anyhow::Result; +use camino::Utf8PathBuf; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::dirext::CapStdExtDirExt; use serde::Serialize; @@ -25,6 +26,28 @@ pub(crate) struct Kernel { pub(crate) unified: bool, } +/// Internal-only kernel wrapper with extra information (paths to +/// vmlinuz, initramfs) that are useful but we don't want to leak out +/// via serialization to inspection. +/// +/// `Kernel` implements `From` so we can just `.into()` +/// to get the "public" form where needed. +pub(crate) struct KernelInternal { + pub(crate) kernel: Kernel, + /// Path to vmlinuz for traditional kernels. + /// This is `None` for UKI images. + pub(crate) vmlinuz: Option, + /// Path to initramfs.img for traditional kernels. + /// This is `None` for UKI images. + pub(crate) initramfs: Option, +} + +impl From for Kernel { + fn from(kernel_internal: KernelInternal) -> Self { + kernel_internal.kernel + } +} + /// Find the kernel in a container image root directory. /// /// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`. @@ -32,28 +55,38 @@ pub(crate) struct Kernel { /// layout with `/usr/lib/modules//vmlinuz`. /// /// Returns `None` if no kernel is found. -pub(crate) fn find_kernel(root: &Dir) -> Result> { +pub(crate) fn find_kernel(root: &Dir) -> Result> { // First, try to find a UKI if let Some(uki_filename) = find_uki_filename(root)? { let version = uki_filename .strip_suffix(".efi") .unwrap_or(&uki_filename) .to_owned(); - return Ok(Some(Kernel { - version, - unified: true, + return Ok(Some(KernelInternal { + kernel: Kernel { + version, + unified: true, + }, + vmlinuz: None, + initramfs: None, })); } // Fall back to checking for a traditional kernel via ostree_ext - if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? { - let version = kernel_dir + if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? { + let version = modules_dir .file_name() - .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))? + .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))? .to_owned(); - return Ok(Some(Kernel { - version, - unified: false, + let vmlinuz = modules_dir.join("vmlinuz"); + let initramfs = modules_dir.join("initramfs.img"); + return Ok(Some(KernelInternal { + kernel: Kernel { + version, + unified: false, + }, + vmlinuz: Some(vmlinuz), + initramfs: Some(initramfs), })); } @@ -93,6 +126,7 @@ fn find_uki_filename(root: &Dir) -> Result> { #[cfg(test)] mod tests { use super::*; + use camino::Utf8Path; use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt}; #[test] @@ -111,9 +145,21 @@ mod tests { b"fake kernel", )?; - let kernel = find_kernel(&tempdir)?.expect("should find kernel"); - assert_eq!(kernel.version, "6.12.0-100.fc41.x86_64"); - assert!(!kernel.unified); + let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel"); + assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64"); + assert!(!kernel_internal.kernel.unified); + assert_eq!( + kernel_internal.vmlinuz.as_deref(), + Some(Utf8Path::new( + "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz" + )) + ); + assert_eq!( + kernel_internal.initramfs.as_deref(), + Some(Utf8Path::new( + "usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img" + )) + ); Ok(()) } @@ -123,9 +169,11 @@ mod tests { tempdir.create_dir_all("boot/EFI/Linux")?; tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?; - let kernel = find_kernel(&tempdir)?.expect("should find kernel"); - assert_eq!(kernel.version, "fedora-6.12.0"); - assert!(kernel.unified); + let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel"); + assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0"); + assert!(kernel_internal.kernel.unified); + assert!(kernel_internal.vmlinuz.is_none()); + assert!(kernel_internal.initramfs.is_none()); Ok(()) } @@ -141,10 +189,10 @@ mod tests { tempdir.create_dir_all("boot/EFI/Linux")?; tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?; - let kernel = find_kernel(&tempdir)?.expect("should find kernel"); + let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel"); // UKI should take precedence - assert_eq!(kernel.version, "fedora-6.12.0"); - assert!(kernel.unified); + assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0"); + assert!(kernel_internal.kernel.unified); Ok(()) } diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index bf49b6bc9..d7e88c3cc 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -93,6 +93,7 @@ pub mod spec; mod status; mod store; mod task; +mod ukify; mod utils; #[cfg(feature = "docgen")] diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index 26220c5c3..83ec92372 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -863,7 +863,7 @@ pub(crate) fn container_inspect( )?; let kargs = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?; let kargs: Vec = kargs.iter_str().map(|s| s.to_owned()).collect(); - let kernel = crate::kernel::find_kernel(&root)?; + let kernel = crate::kernel::find_kernel(&root)?.map(Into::into); let inspect = crate::spec::ContainerInspect { kargs, kernel }; // Determine output format: explicit --format wins, then --json, then default to human-readable diff --git a/crates/lib/src/ukify.rs b/crates/lib/src/ukify.rs new file mode 100644 index 000000000..f4b03e0de --- /dev/null +++ b/crates/lib/src/ukify.rs @@ -0,0 +1,166 @@ +//! Build Unified Kernel Images (UKI) using ukify. +//! +//! This module provides functionality to build UKIs by computing the necessary +//! arguments from a container image and invoking the ukify tool. + +use std::ffi::OsString; +use std::process::Command; + +use anyhow::{Context, Result}; +use bootc_kernel_cmdline::utf8::Cmdline; +use bootc_utils::CommandRunExt; +use camino::Utf8Path; +use cap_std_ext::cap_std::fs::Dir; +use fn_error_context::context; + +use crate::bootc_composefs::digest::compute_composefs_digest; +use crate::composefs_consts::COMPOSEFS_CMDLINE; + +/// Build a UKI from the given rootfs. +/// +/// This function: +/// 1. Verifies that ukify is available +/// 2. Finds the kernel in the rootfs +/// 3. Computes the composefs digest +/// 4. Reads kernel arguments from kargs.d +/// 5. Appends any additional kargs provided via --karg +/// 6. Invokes ukify with computed arguments plus any pass-through args +#[context("Building UKI")] +pub(crate) fn build_ukify( + rootfs: &Utf8Path, + extra_kargs: &[String], + args: &[OsString], +) -> Result<()> { + // Warn if --karg is used (temporary workaround) + if !extra_kargs.is_empty() { + tracing::warn!( + "The --karg flag is temporary and will be removed as soon as possible \ + (https://github.com/bootc-dev/bootc/issues/1826)" + ); + } + + // Verify ukify is available + if !crate::utils::have_executable("ukify")? { + anyhow::bail!( + "ukify executable not found in PATH. Please install systemd-ukify or equivalent." + ); + } + + // Open the rootfs directory + 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}"))?; + + // We can only build a UKI from a traditional kernel, not from an existing UKI + if kernel.kernel.unified { + anyhow::bail!( + "Cannot build UKI: rootfs already contains a UKI at boot/EFI/Linux/{}.efi", + kernel.kernel.version + ); + } + + // Get paths from the kernel info + let vmlinuz_path = kernel + .vmlinuz + .ok_or_else(|| anyhow::anyhow!("Traditional kernel should have vmlinuz path"))?; + let initramfs_path = kernel + .initramfs + .ok_or_else(|| anyhow::anyhow!("Traditional kernel should have initramfs path"))?; + + // 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}"); + } + + // Compute the composefs digest + let composefs_digest = compute_composefs_digest(rootfs, None)?; + + // Get kernel arguments from kargs.d + let mut cmdline = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?; + + // Add the composefs digest + let composefs_param = format!("{COMPOSEFS_CMDLINE}={composefs_digest}"); + cmdline.extend(&Cmdline::from(composefs_param)); + + // Add any extra kargs provided via --karg + for karg in extra_kargs { + cmdline.extend(&Cmdline::from(karg)); + } + + let cmdline_str = cmdline.to_string(); + + // Build the ukify command with cwd set to rootfs so paths can be relative + let mut cmd = Command::new("ukify"); + cmd.current_dir(rootfs); + cmd.arg("build") + .arg("--linux") + .arg(&vmlinuz_path) + .arg("--initrd") + .arg(&initramfs_path) + .arg("--uname") + .arg(&kernel.kernel.version) + .arg("--cmdline") + .arg(&cmdline_str) + .arg("--os-release") + .arg("@usr/lib/os-release"); + + // Add pass-through arguments + cmd.args(args); + + tracing::debug!("Executing ukify: {:?}", cmd); + + // Run ukify + cmd.run_inherited().context("Running ukify")?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_build_ukify_no_kernel() { + let tempdir = tempfile::tempdir().unwrap(); + let path = Utf8Path::from_path(tempdir.path()).unwrap(); + + let result = build_ukify(path, &[], &[]); + assert!(result.is_err()); + let err = format!("{:#}", result.unwrap_err()); + assert!( + err.contains("No kernel found") || err.contains("ukify executable not found"), + "Unexpected error message: {err}" + ); + } + + #[test] + fn test_build_ukify_already_uki() { + let tempdir = tempfile::tempdir().unwrap(); + let path = Utf8Path::from_path(tempdir.path()).unwrap(); + + // Create a UKI structure + 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, &[], &[]); + assert!(result.is_err()); + let err = format!("{:#}", result.unwrap_err()); + assert!( + err.contains("already contains a UKI") || err.contains("ukify executable not found"), + "Unexpected error message: {err}" + ); + } +} diff --git a/crates/lib/src/utils.rs b/crates/lib/src/utils.rs index 391c578df..876b2e9f0 100644 --- a/crates/lib/src/utils.rs +++ b/crates/lib/src/utils.rs @@ -67,7 +67,6 @@ pub(crate) fn find_mount_option<'a>( .next() } -#[allow(dead_code)] pub fn have_executable(name: &str) -> Result { let Some(path) = std::env::var_os("PATH") else { return Ok(false); diff --git a/docs/src/man/bootc-container-ukify.8.md b/docs/src/man/bootc-container-ukify.8.md new file mode 100644 index 000000000..1e2f27488 --- /dev/null +++ b/docs/src/man/bootc-container-ukify.8.md @@ -0,0 +1,42 @@ +# NAME + +bootc-container-ukify - Build a Unified Kernel Image (UKI) using ukify + +# SYNOPSIS + +bootc container ukify [OPTIONS] [-- UKIFY_ARGS...] + +# DESCRIPTION + +Build a Unified Kernel Image (UKI) using ukify + +This command computes the necessary arguments from the container image +(kernel, initrd, cmdline, os-release) and invokes ukify with them. +Any additional arguments after `--` are passed through to ukify unchanged. + +# OPTIONS + + +**ARGS** + + Additional arguments to pass to ukify (after `--`) + +**--rootfs**=*ROOTFS* + + Operate on the provided rootfs + + Default: / + + + +# EXAMPLES + + bootc container ukify --rootfs /target -- --output /output/uki.efi + +# SEE ALSO + +**bootc**(8), **ukify**(1) + +# VERSION + + diff --git a/docs/src/man/bootc-container.8.md b/docs/src/man/bootc-container.8.md index c19567a11..b8b540972 100644 --- a/docs/src/man/bootc-container.8.md +++ b/docs/src/man/bootc-container.8.md @@ -21,6 +21,7 @@ Operations which can be executed as part of a container build |---------|-------------| | **bootc container inspect** | Output information about the container image | | **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build | +| **bootc container ukify** | Build a Unified Kernel Image (UKI) using ukify |