From fb790a04d16d13ab5ff1ada68103d531dc8776e9 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 25 Sep 2026 09:51:11 -0400 Subject: [PATCH] container: Move .vmlinuz.hmac along with the kernel in split-kernel-and-rootfs Fedora and RHEL derivatives ship the kernel's FIPS HMAC as /usr/lib/modules//.vmlinuz.hmac, which ostree and kernel-install copy next to vmlinuz when installing it. split-kernel-and-rootfs moved vmlinuz and the initramfs out but left the HMAC behind, orphaned in the rootfs of a sealed image. Keep a small documented list of files that belong to the kernel binary and move them into the output directory next to vmlinuz, under the same names, so they go wherever the kernel goes. The HMAC is the only entry today: Debian, Ubuntu and Arch ship no such companion there, and the other files ostree takes from this directory (devicetree, dtb/, aboot.img) are separate boot inputs, while config and System.map are userspace metadata that should stay in the rootfs. While here, move the logic into kernel.rs as a cap-std based function so it can be unit tested. Generated-by: AI Signed-off-by: Colin Walters --- crates/lib/src/cli.rs | 42 ++----- crates/lib/src/kernel.rs | 105 ++++++++++++++++++ ...otc-container-split-kernel-and-rootfs.8.md | 4 + 3 files changed, 118 insertions(+), 33 deletions(-) diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 9c34dd5fed..aa82b6c4f5 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -463,7 +463,9 @@ pub(crate) enum ContainerOpts { /// /// This command extracts the kernel (vmlinuz and initramfs.img) from the /// container rootfs and moves them to a separate output directory, organized - /// by kernel version + /// by kernel version. Files that belong to the kernel binary are moved + /// along with it, currently the FIPS HMAC file (.vmlinuz.hmac) that Fedora + /// and RHEL derivatives ship. /// /// Example: /// bootc container split-kernel-rootfs --rootfs /target-rootfs --output /out @@ -2142,38 +2144,12 @@ async fn run_from_opt(opt: Opt) -> Result { Ok(()) } ContainerOpts::SplitKernelAndRootfs { rootfs, output } => { - use crate::kernel::{KernelType, find_kernel}; - - let root = Dir::open_ambient_dir(&rootfs, ambient_authority())?; - - let kernel_internal = find_kernel(&root)? - .ok_or_else(|| anyhow::anyhow!("No kernel found in rootfs"))?; - - if kernel_internal.kernel.unified { - anyhow::bail!("UKIs are not supported"); - } - - match &kernel_internal.k_type { - KernelType::Vmlinuz { path, initramfs } => { - let kver = &kernel_internal.kernel.version; - let kernel_output_dir = output.join(kver); - std::fs::create_dir_all(&kernel_output_dir)?; - - let vmlinuz_src = rootfs.join(path); - let initramfs_src = rootfs.join(initramfs); - let vmlinuz_dst = kernel_output_dir.join("vmlinuz"); - let initramfs_dst = kernel_output_dir.join("initramfs.img"); - - std::fs::rename(&vmlinuz_src, &vmlinuz_dst).context("Moving vmlinuz")?; - std::fs::rename(&initramfs_src, &initramfs_dst) - .context("Moving initramfs")?; - } - - KernelType::Uki { .. } => { - anyhow::bail!("UKIs are not supported"); - } - } - + let root = Dir::open_ambient_dir(&rootfs, ambient_authority()) + .with_context(|| format!("Opening {rootfs}"))?; + std::fs::create_dir_all(&output).with_context(|| format!("Creating {output}"))?; + let output = Dir::open_ambient_dir(&output, ambient_authority()) + .with_context(|| format!("Opening {output}"))?; + crate::kernel::split_kernel(&root, &output)?; Ok(()) } ContainerOpts::ComputeComposefsDigest { diff --git a/crates/lib/src/kernel.rs b/crates/lib/src/kernel.rs index c7b4c34bad..83e7cc24cf 100644 --- a/crates/lib/src/kernel.rs +++ b/crates/lib/src/kernel.rs @@ -132,6 +132,54 @@ pub(crate) fn find_kernel(root: &Dir) -> Result> { Ok(None) } +/// Files that distributions ship next to `vmlinuz` in `/usr/lib/modules//` +/// which belong to that kernel binary, and so must travel with it. +/// +/// - `.vmlinuz.hmac`: the kernel's HMAC, which dracut's `fips` module checks +/// at boot in FIPS mode. Shipped by Fedora and RHEL derivatives' kernel +/// packages; ostree and Fedora's grub2 kernel-install plugin install it next +/// to the kernel as `.vmlinuz-.hmac`. +/// +/// This is deliberately limited to companions of the kernel binary. Userspace +/// metadata such as `config`, `System.map` or `symvers.xz` stays in the rootfs, +/// where tools look for it. ostree also installs a `devicetree` file or `dtb/` +/// directory and an `aboot.img` from this directory, but those are separate +/// boot inputs rather than companions of `vmlinuz`, so they are left alone. +pub(crate) const KERNEL_COMPANION_FILES: &[&str] = &[".vmlinuz.hmac"]; + +/// Move the kernel out of `root` into `output//`. +/// +/// The kernel is written as `vmlinuz` and the initramfs as `initramfs.img`, +/// along with any [`KERNEL_COMPANION_FILES`] present, under the same names. +/// UKIs are not supported. Returns the kernel version. +pub(crate) fn split_kernel(root: &Dir, output: &Dir) -> Result { + let kernel = find_kernel(root)?.ok_or_else(|| anyhow::anyhow!("No kernel found in rootfs"))?; + let KernelType::Vmlinuz { path, initramfs } = &kernel.k_type else { + anyhow::bail!("UKIs are not supported"); + }; + let kver = kernel.kernel.version; + + output + .create_dir_all(&kver) + .with_context(|| format!("Creating {kver} in output directory"))?; + let dest = output.open_dir(&kver)?; + + root.rename(path, &dest, "vmlinuz") + .with_context(|| format!("Moving {path}"))?; + root.rename(initramfs, &dest, "initramfs.img") + .with_context(|| format!("Moving {initramfs}"))?; + + for &name in KERNEL_COMPANION_FILES { + let src = path.with_file_name(name); + match root.rename(&src, &dest, name) { + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + r => r.with_context(|| format!("Moving {src}"))?, + } + } + + Ok(kver) +} + /// Returns the path to the first UKI found in the container root, if any. /// /// Looks in `/boot/EFI/Linux/*.efi`. If multiple UKIs are present, returns @@ -243,6 +291,63 @@ mod tests { Ok(()) } + #[test] + fn test_split_kernel() -> Result<()> { + const KVER: &str = "6.12.0-100.fc41.x86_64"; + let moddir = format!("usr/lib/modules/{KVER}"); + // Userspace metadata that distributions also put here; it must stay. + const STAYS: &[&str] = &["modules.dep", "config", "System.map"]; + // Companion files are optional: try none, each one alone, and all of them. + let cases = std::iter::once(&[][..]) + .chain(KERNEL_COMPANION_FILES.chunks(1)) + .chain(std::iter::once(KERNEL_COMPANION_FILES)); + for present in cases { + let root = cap_tempfile::tempdir(cap_std::ambient_authority())?; + let output = cap_tempfile::tempdir(cap_std::ambient_authority())?; + root.create_dir_all(&moddir)?; + root.atomic_write(format!("{moddir}/vmlinuz"), b"kernel")?; + root.atomic_write(format!("{moddir}/initramfs.img"), b"initramfs")?; + for name in STAYS.iter().chain(present) { + root.atomic_write(format!("{moddir}/{name}"), name)?; + } + + assert_eq!(split_kernel(&root, &output)?, KVER); + + let mut remaining: Vec<_> = root + .read_dir(&moddir)? + .map(|e| e.map(|e| e.file_name())) + .collect::>()?; + remaining.sort(); + let mut expected = STAYS.to_vec(); + expected.sort(); + assert_eq!(remaining, expected, "{present:?}"); + + let dest = output.open_dir(KVER)?; + assert_eq!(dest.read("vmlinuz")?, b"kernel"); + assert_eq!(dest.read("initramfs.img")?, b"initramfs"); + for &name in KERNEL_COMPANION_FILES { + if present.contains(&name) { + assert_eq!(dest.read(name)?, name.as_bytes(), "{name}"); + } else { + assert!(!dest.try_exists(name)?, "{name}"); + } + } + } + Ok(()) + } + + #[test] + fn test_split_kernel_uki() -> Result<()> { + let root = cap_tempfile::tempdir(cap_std::ambient_authority())?; + let output = cap_tempfile::tempdir(cap_std::ambient_authority())?; + root.create_dir_all("boot/EFI/Linux")?; + root.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", &create_minimal_pe())?; + assert!(split_kernel(&root, &output).is_err()); + // An empty rootfs has no kernel at all + assert!(split_kernel(&output, &root).is_err()); + Ok(()) + } + #[test] fn test_find_uki_path_sorted() -> Result<()> { let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?; diff --git a/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md b/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md index 8e5bb669f5..273b649d9a 100644 --- a/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md +++ b/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md @@ -34,6 +34,9 @@ bootc container split-kernel-and-rootfs --output /kernel ``` This extracts the kernel and initramfs from the current root filesystem (/) and places them in `/kernel//` with filenames `vmlinuz` and `initramfs.img`. +Files that belong to the kernel binary are moved along with it under the same names, when present. +Currently that is only the FIPS HMAC file (`.vmlinuz.hmac`) shipped by Fedora and RHEL derivatives. +Other files in `/usr/lib/modules//`, such as `config` or `System.map`, stay in the rootfs. **Extract kernel files from a mounted container rootfs:** @@ -50,6 +53,7 @@ After running the command, the output directory will contain: ``` /output/kernels/ └── 6.5.0-15-generic/ + ├── .vmlinuz.hmac (Fedora/RHEL, if present) ├── vmlinuz └── initramfs.img ```