Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 9 additions & 33 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2142,38 +2144,12 @@ async fn run_from_opt(opt: Opt) -> Result<CliExitStatus> {
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 {
Expand Down
105 changes: 105 additions & 0 deletions crates/lib/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,54 @@ pub(crate) fn find_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
Ok(None)
}

/// Files that distributions ship next to `vmlinuz` in `/usr/lib/modules/<kver>/`
/// 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-<kver>.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/<kver>/`.
///
/// 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<String> {
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
Expand Down Expand Up @@ -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::<std::io::Result<_>>()?;
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())?;
Expand Down
4 changes: 4 additions & 0 deletions docs/src/man/bootc-container-split-kernel-and-rootfs.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<kernel-version>/` 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/<kernel-version>/`, such as `config` or `System.map`, stay in the rootfs.

**Extract kernel files from a mounted container rootfs:**

Expand All @@ -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
```
Expand Down
Loading