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
46 changes: 0 additions & 46 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,26 +217,6 @@ fi
)
}

/// Returns `true` if detect the target rootfs carries a UKI.
pub(crate) fn container_root_has_uki(root: &Dir) -> Result<bool> {
let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
return Ok(false);
};
let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
return Ok(false);
};
for entry in efi_linux.entries()? {
let entry = entry?;
let name = entry.file_name();
let name = Path::new(&name);
let extension = name.extension().and_then(|v| v.to_str());
if extension == Some("efi") {
return Ok(true);
}
}
Ok(false)
}

pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?;
let esp = crate::bootloader::esp_in(&device_info)?;
Expand Down Expand Up @@ -1295,32 +1275,6 @@ pub(crate) async fn setup_composefs_boot(
#[cfg(test)]
mod tests {
use super::*;
use cap_std_ext::cap_std;

#[test]
fn test_root_has_uki() -> Result<()> {
// Test case 1: No boot directory
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
assert_eq!(container_root_has_uki(&tempdir)?, false);

// Test case 2: boot directory exists but no EFI/Linux
tempdir.create_dir(crate::install::BOOT)?;
assert_eq!(container_root_has_uki(&tempdir)?, false);

// Test case 3: boot/EFI/Linux exists but no .efi files
tempdir.create_dir_all("boot/EFI/Linux")?;
assert_eq!(container_root_has_uki(&tempdir)?, false);

// Test case 4: boot/EFI/Linux exists with non-.efi file
tempdir.atomic_write("boot/EFI/Linux/readme.txt", b"some file")?;
assert_eq!(container_root_has_uki(&tempdir)?, false);

// Test case 5: boot/EFI/Linux exists with .efi file
tempdir.atomic_write("boot/EFI/Linux/bootx64.efi", b"fake efi binary")?;
assert_eq!(container_root_has_uki(&tempdir)?, true);

Ok(())
}

#[test]
fn test_type1_filename_generation() {
Expand Down
26 changes: 17 additions & 9 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,22 @@ pub(crate) enum InstallOpts {
/// Subcommands which can be executed as part of a container build.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ContainerOpts {
/// Output JSON to stdout containing the container image metadata.
/// Output information about the container image.
///
/// By default, a human-readable summary is output. Use --json or --format
/// to change the output format.
Inspect {
/// Operate on the provided rootfs.
#[clap(long, default_value = "/")]
rootfs: Utf8PathBuf,

/// Output in JSON format.
#[clap(long)]
json: bool,

/// The output format.
#[clap(long, conflicts_with = "json")]
format: Option<OutputFormat>,
},
/// Perform relatively inexpensive static analysis checks as part of a container
/// build.
Expand Down Expand Up @@ -1457,14 +1468,11 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
}
}
Opt::Container(opts) => match opts {
ContainerOpts::Inspect { rootfs } => {
let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?;
let kargs = crate::bootc_kargs::get_kargs_in_root(root, std::env::consts::ARCH)?;
let kargs: Vec<String> = kargs.iter_str().map(|s| s.to_owned()).collect();
let inspect = crate::spec::ContainerInspect { kargs };
serde_json::to_writer_pretty(std::io::stdout().lock(), &inspect)?;
Ok(())
}
ContainerOpts::Inspect {
rootfs,
json,
format,
} => crate::status::container_inspect(&rootfs, json, format),
ContainerOpts::Lint {
rootfs,
fatal_warnings,
Expand Down
8 changes: 3 additions & 5 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1341,10 +1341,6 @@ async fn verify_target_fetch(
Ok(())
}

fn root_has_uki(root: &Dir) -> Result<bool> {
crate::bootc_composefs::boot::container_root_has_uki(root)
}

/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
async fn prepare_install(
config_opts: InstallConfigOpts,
Expand Down Expand Up @@ -1418,7 +1414,9 @@ async fn prepare_install(
tracing::debug!("Target image reference: {target_imgref}");

let composefs_required = if let Some(root) = target_rootfs.as_ref() {
root_has_uki(root)?
crate::kernel::find_kernel(root)?
.map(|k| k.unified)
.unwrap_or(false)
} else {
false
};
Expand Down
164 changes: 164 additions & 0 deletions crates/lib/src/kernel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//! Kernel detection for container images.
//!
//! This module provides functionality to detect kernel information in container
//! images, supporting both traditional kernels (with separate vmlinuz/initrd) and
//! Unified Kernel Images (UKI).

use std::path::Path;

use anyhow::Result;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt;
use serde::Serialize;

use crate::bootc_composefs::boot::EFI_LINUX;

/// Information about the kernel in a container image.
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct Kernel {
/// The kernel version identifier. For traditional kernels, this is derived from the
/// /usr/lib/modules/<version> directory name. For UKI images, this is the UKI filename
/// (without the .efi extension).
pub(crate) version: String,
/// Whether the kernel is packaged as a UKI (Unified Kernel Image).
pub(crate) unified: bool,
}

/// Find the kernel in a container image root directory.
///
/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
/// If that doesn't exist, it falls back to looking for a traditional kernel
/// layout with `/usr/lib/modules/<version>/vmlinuz`.
///
/// Returns `None` if no kernel is found.
pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
// 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,
}));
}

// 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
.file_name()
.ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
.to_owned();
return Ok(Some(Kernel {
version,
unified: false,
}));
}

Ok(None)
}

/// Returns the filename of the first UKI found in the container root, if any.
///
/// Looks in `/boot/EFI/Linux/*.efi`. If multiple UKIs are present, returns
/// the first one in sorted order for determinism.
fn find_uki_filename(root: &Dir) -> Result<Option<String>> {
let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
return Ok(None);
};
let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
return Ok(None);
};

let mut uki_files = Vec::new();
for entry in efi_linux.entries()? {
let entry = entry?;
let name = entry.file_name();
let name_path = Path::new(&name);
let extension = name_path.extension().and_then(|v| v.to_str());
if extension == Some("efi") {
if let Some(name_str) = name.to_str() {
uki_files.push(name_str.to_owned());
}
}
}

// Sort for deterministic behavior when multiple UKIs are present
uki_files.sort();
Ok(uki_files.into_iter().next())
}

#[cfg(test)]
mod tests {
use super::*;
use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};

#[test]
fn test_find_kernel_none() -> Result<()> {
let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
assert!(find_kernel(&tempdir)?.is_none());
Ok(())
}

#[test]
fn test_find_kernel_traditional() -> Result<()> {
let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
tempdir.atomic_write(
"usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
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);
Ok(())
}

#[test]
fn test_find_kernel_uki() -> Result<()> {
let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
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);
Ok(())
}

#[test]
fn test_find_kernel_uki_takes_precedence() -> Result<()> {
let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
// Both traditional and UKI exist
tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
tempdir.atomic_write(
"usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
b"fake kernel",
)?;
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");
// UKI should take precedence
assert_eq!(kernel.version, "fedora-6.12.0");
assert!(kernel.unified);
Ok(())
}

#[test]
fn test_find_uki_filename_sorted() -> Result<()> {
let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
tempdir.create_dir_all("boot/EFI/Linux")?;
tempdir.atomic_write("boot/EFI/Linux/zzz.efi", b"fake uki")?;
tempdir.atomic_write("boot/EFI/Linux/aaa.efi", b"fake uki")?;
tempdir.atomic_write("boot/EFI/Linux/mmm.efi", b"fake uki")?;

// Should return first in sorted order
let filename = find_uki_filename(&tempdir)?.expect("should find uki");
assert_eq!(filename, "aaa.efi");
Ok(())
}
}
1 change: 1 addition & 0 deletions crates/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod image;
mod install;
pub(crate) mod journal;
mod k8sapitypes;
mod kernel;
mod lints;
mod lsm;
pub(crate) mod metadata;
Expand Down
3 changes: 3 additions & 0 deletions crates/lib/src/spec.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! The definition for host system state.

use std::fmt::Display;

use std::str::FromStr;

use anyhow::Result;
Expand Down Expand Up @@ -303,6 +304,8 @@ pub(crate) struct DeploymentEntry<'a> {
pub(crate) struct ContainerInspect {
/// Kernel arguments embedded in the container image.
pub(crate) kargs: Vec<String>,
/// Information about the kernel in the container image.
pub(crate) kernel: Option<crate::kernel::Kernel>,
}

impl Host {
Expand Down
Loading
Loading