Skip to content
Merged
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
1 change: 1 addition & 0 deletions contrib/packaging/bcvk.spec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Requires: virtiofsd
# Extraction and initramfs tools are needed for some ephemeral boot images.
Recommends: binutils
Recommends: kernel-tools
Recommends: android-tools
# libvirt-client is optional but recommended for 'bcvk libvirt' commands
Recommends: libvirt-client

Expand Down
168 changes: 144 additions & 24 deletions crates/kit/src/kernel.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! Kernel detection for container images.
//!
//! This module provides functionality to detect kernel and initramfs in container
//! images, supporting both traditional kernels (with separate vmlinuz/initrd) and
//! Unified Kernel Images (UKI).
//! images, supporting traditional kernels, Unified Kernel Images (UKI), and
//! aboot artifacts.

use std::io::Read;
use std::path::Path;

use camino::{Utf8Path, Utf8PathBuf};
Expand All @@ -26,31 +27,63 @@ const VMLINUZ: &str = "vmlinuz";
/// Traditional initramfs filename
const INITRAMFS: &str = "initramfs.img";

/// Aboot artifacts use this name regardless of whether they contain a UKI or
/// an Android boot image.
const ABOOT_PREFIX: &str = "aboot-";
const ABOOT_EXTENSION: &str = "img";
const ANDROID_BOOT_MAGIC: &[u8; 8] = b"ANDROID!";
const PE_MAGIC: &[u8; 2] = b"MZ";

/// Format of the boot artifact from which QEMU obtains its kernel and initramfs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KernelKind {
/// Separate vmlinuz and initramfs files.
Traditional,
/// Unified Kernel Image, including ukiboot aboot payloads.
Uki,
/// Android boot image containing a kernel and ramdisk.
AndroidBoot,
}

/// Information about a kernel found in a container image.
#[derive(Debug, Clone)]
pub struct KernelInfo {
/// Path to the kernel (vmlinuz or UKI .efi file)
/// Path to the kernel or combined boot artifact.
pub kernel_path: Utf8PathBuf,
/// Path to the initramfs (only for traditional kernels, None for UKI)
/// Path to the initramfs (only for traditional kernels).
pub initramfs_path: Option<Utf8PathBuf>,
/// Whether this is a Unified Kernel Image
pub is_uki: bool,
/// Format of the discovered boot artifact.
pub kind: KernelKind,
}

/// Find kernel/initramfs in a container image root directory.
///
/// UKIs take precedence over traditional kernels. This handles older images
/// that may have both a UKI and vmlinuz+initramfs.
/// Aboot artifacts take precedence over UKIs, which take precedence over
/// traditional kernels. An aboot image may retain other kernel files.
///
/// Search order:
/// 1. `/boot/EFI/Linux/*.efi` - UKI in ESP
/// 2. `/usr/lib/modules/<version>/*.efi` - UKI alongside modules
/// 3. `/usr/lib/modules/<version>/vmlinuz` + `initramfs.img` - traditional
/// 1. `/boot/aboot-*.img` - ukiboot UKI or Android boot image
/// 2. `/boot/EFI/Linux/*.efi` - UKI in ESP
/// 3. `/usr/lib/modules/<version>/*.efi` - UKI alongside modules
/// 4. `/usr/lib/modules/<version>/vmlinuz` + `initramfs.img` - traditional
///
/// Returns an error if multiple UKIs are found, or if no UKI exists and
/// multiple traditional kernels are found.
/// Returns an error if multiple artifacts of the preferred kind are found.
/// Returns `None` if no kernel is found.
pub fn find_kernel(root: &Dir) -> Result<Option<KernelInfo>> {
let aboot = find_aboot_artifacts(root)?;
match aboot.as_slice() {
[] => {}
[artifact] => return Ok(Some(artifact.clone())),
artifacts => {
let paths: Vec<_> = artifacts.iter().map(|k| k.kernel_path.as_str()).collect();
bail!(
"Found {} aboot artifacts, expected exactly one:\n {}",
artifacts.len(),
paths.join("\n ")
);
}
}

// First, collect all UKIs
let mut ukis: Vec<KernelInfo> = Vec::new();
ukis.extend(find_ukis_in_esp(root)?);
Expand Down Expand Up @@ -86,6 +119,50 @@ pub fn find_kernel(root: &Dir) -> Result<Option<KernelInfo>> {
}
}

fn find_aboot_artifacts(root: &Dir) -> Result<Vec<KernelInfo>> {
let Some(boot) = root.open_dir_optional("boot")? else {
return Ok(Vec::new());
};
let mut artifacts = Vec::new();
for entry in boot.entries()? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let Some((stem, extension)) = name.rsplit_once('.') else {
continue;
};
let Some(version) = stem.strip_prefix(ABOOT_PREFIX) else {
continue;
};
if version.is_empty() || extension != ABOOT_EXTENSION {
continue;
}

let mut file = boot.open(name)?;
let mut magic = [0u8; 8];
file.read_exact(&mut magic)
.with_context(|| format!("reading header of boot/{name}"))?;
let kind = if &magic == ANDROID_BOOT_MAGIC {
KernelKind::AndroidBoot
} else if magic.starts_with(PE_MAGIC) {
KernelKind::Uki
} else {
bail!("Unsupported aboot artifact format: boot/{name}");
};
artifacts.push(KernelInfo {
kernel_path: Utf8PathBuf::from(format!("boot/{name}")),
initramfs_path: None,
kind,
});
}
Ok(artifacts)
}

/// Check if a filename has the UKI extension (.efi)
fn is_uki_file(name: &std::ffi::OsStr) -> bool {
Path::new(name)
Expand All @@ -111,7 +188,7 @@ fn find_ukis_in_esp(root: &Dir) -> Result<Vec<KernelInfo>> {
ukis.push(KernelInfo {
kernel_path: Utf8PathBuf::from(format!("boot/{EFI_LINUX}/{name_str}")),
initramfs_path: None,
is_uki: true,
kind: KernelKind::Uki,
});
}
}
Expand Down Expand Up @@ -155,7 +232,7 @@ fn find_ukis_in_modules(root: &Dir) -> Result<Vec<KernelInfo>> {
"usr/lib/{MODULES_DIR}/{version}/{uki_name}"
)),
initramfs_path: None,
is_uki: true,
kind: KernelKind::Uki,
});
}
}
Expand Down Expand Up @@ -192,7 +269,7 @@ fn find_traditional_kernels_in_modules(root: &Dir) -> Result<Vec<KernelInfo>> {
initramfs_path: Some(Utf8PathBuf::from(format!(
"usr/lib/{MODULES_DIR}/{version}/{INITRAMFS}"
))),
is_uki: false,
kind: KernelKind::Traditional,
});
}
}
Expand Down Expand Up @@ -225,7 +302,7 @@ pub fn with_root_prefix(info: KernelInfo, root: &Utf8Path) -> KernelInfo {
KernelInfo {
kernel_path: root.join(&info.kernel_path),
initramfs_path: info.initramfs_path.map(|p| root.join(&p)),
is_uki: info.is_uki,
kind: info.kind,
}
}

Expand Down Expand Up @@ -256,7 +333,7 @@ mod tests {
)?;

let info = find_kernel(&tempdir)?.expect("should find kernel");
assert!(!info.is_uki);
assert_eq!(info.kind, KernelKind::Traditional);
assert!(info.kernel_path.as_str().contains("vmlinuz"));
assert!(info.initramfs_path.is_some());
assert!(info
Expand All @@ -275,7 +352,7 @@ mod tests {
tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;

let info = find_kernel(&tempdir)?.expect("should find kernel");
assert!(info.is_uki);
assert_eq!(info.kind, KernelKind::Uki);
assert!(info.kernel_path.as_str().contains("fedora-6.12.0.efi"));
assert!(info.initramfs_path.is_none());
Ok(())
Expand All @@ -291,7 +368,7 @@ mod tests {
)?;

let info = find_kernel(&tempdir)?.expect("should find kernel");
assert!(info.is_uki);
assert_eq!(info.kind, KernelKind::Uki);
assert!(info
.kernel_path
.as_str()
Expand Down Expand Up @@ -322,7 +399,7 @@ mod tests {

// Should find the UKI, ignoring traditional kernel
let info = find_kernel(&tempdir)?.expect("should find kernel");
assert!(info.is_uki);
assert_eq!(info.kind, KernelKind::Uki);
assert!(info.kernel_path.as_str().contains("fedora-6.12.0.efi"));
Ok(())
}
Expand All @@ -349,7 +426,7 @@ mod tests {

// Should find the UKI, ignoring traditional kernel
let info = find_kernel(&tempdir)?.expect("should find kernel");
assert!(info.is_uki);
assert_eq!(info.kind, KernelKind::Uki);
assert!(info
.kernel_path
.as_str()
Expand Down Expand Up @@ -451,12 +528,55 @@ mod tests {
assert!(err.contains("Found 2 UKIs"));
}

#[test]
fn test_find_aboot_artifact() -> Result<()> {
for (header, kind) in [
(b"ANDROID!".as_slice(), KernelKind::AndroidBoot),
(b"MZ______".as_slice(), KernelKind::Uki),
] {
let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
root.create_dir_all("boot")?;
root.atomic_write("boot/aboot-6.12.img", header)?;
root.atomic_write("boot/aboot-.img", b"")?;
root.atomic_write("boot/aboot-6.12.img.bak", b"")?;
root.atomic_write("boot/vbmeta-6.12.img", b"vbmeta")?;
root.create_dir_all("usr/lib/modules/6.12")?;
root.atomic_write("usr/lib/modules/6.12/vmlinuz", b"kernel")?;
root.atomic_write("usr/lib/modules/6.12/initramfs.img", b"initramfs")?;

let info = find_kernel(&root)?.expect("aboot artifact should take precedence");
assert_eq!(info.kind, kind);
assert_eq!(info.kernel_path, Utf8Path::new("boot/aboot-6.12.img"));
assert!(info.initramfs_path.is_none());
}
Ok(())
}

#[test]
fn test_find_aboot_artifact_errors() -> Result<()> {
let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
root.create_dir_all("boot")?;
root.atomic_write("boot/aboot-6.12.img", b"unknown!")?;
assert!(find_kernel(&root)
.unwrap_err()
.to_string()
.contains("Unsupported aboot artifact format"));

root.atomic_write("boot/aboot-6.12.img", b"ANDROID!")?;
root.atomic_write("boot/aboot-6.13.img", b"ANDROID!")?;
assert!(find_kernel(&root)
.unwrap_err()
.to_string()
.contains("Found 2 aboot artifacts"));
Ok(())
}

#[test]
fn test_with_root_prefix() {
let info = KernelInfo {
kernel_path: Utf8PathBuf::from("boot/EFI/Linux/test.efi"),
initramfs_path: None,
is_uki: true,
kind: KernelKind::Uki,
};

let prefixed = with_root_prefix(info, Utf8Path::new("/run/source-image"));
Expand All @@ -471,7 +591,7 @@ mod tests {
let info = KernelInfo {
kernel_path: Utf8PathBuf::from("usr/lib/modules/6.12.0/vmlinuz"),
initramfs_path: Some(Utf8PathBuf::from("usr/lib/modules/6.12.0/initramfs.img")),
is_uki: false,
kind: KernelKind::Traditional,
};

let prefixed = with_root_prefix(info, Utf8Path::new("/run/source-image"));
Expand Down
Loading
Loading