bootc_lib/
kernel.rs

1//! Kernel detection for container images.
2//!
3//! This module provides functionality to detect kernel information in container
4//! images, supporting both traditional kernels (with separate vmlinuz/initrd) and
5//! Unified Kernel Images (UKI).
6
7use std::path::Path;
8
9use anyhow::Result;
10use cap_std_ext::cap_std::fs::Dir;
11use cap_std_ext::dirext::CapStdExtDirExt;
12use serde::Serialize;
13
14use crate::bootc_composefs::boot::EFI_LINUX;
15
16/// Information about the kernel in a container image.
17#[derive(Debug, Serialize)]
18#[serde(rename_all = "kebab-case")]
19pub(crate) struct Kernel {
20    /// The kernel version identifier. For traditional kernels, this is derived from the
21    /// /usr/lib/modules/<version> directory name. For UKI images, this is the UKI filename
22    /// (without the .efi extension).
23    pub(crate) version: String,
24    /// Whether the kernel is packaged as a UKI (Unified Kernel Image).
25    pub(crate) unified: bool,
26}
27
28/// Find the kernel in a container image root directory.
29///
30/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
31/// If that doesn't exist, it falls back to looking for a traditional kernel
32/// layout with `/usr/lib/modules/<version>/vmlinuz`.
33///
34/// Returns `None` if no kernel is found.
35pub(crate) fn find_kernel(root: &Dir) -> Result<Option<Kernel>> {
36    // First, try to find a UKI
37    if let Some(uki_filename) = find_uki_filename(root)? {
38        let version = uki_filename
39            .strip_suffix(".efi")
40            .unwrap_or(&uki_filename)
41            .to_owned();
42        return Ok(Some(Kernel {
43            version,
44            unified: true,
45        }));
46    }
47
48    // Fall back to checking for a traditional kernel via ostree_ext
49    if let Some(kernel_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
50        let version = kernel_dir
51            .file_name()
52            .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {kernel_dir}"))?
53            .to_owned();
54        return Ok(Some(Kernel {
55            version,
56            unified: false,
57        }));
58    }
59
60    Ok(None)
61}
62
63/// Returns the filename of the first UKI found in the container root, if any.
64///
65/// Looks in `/boot/EFI/Linux/*.efi`. If multiple UKIs are present, returns
66/// the first one in sorted order for determinism.
67fn find_uki_filename(root: &Dir) -> Result<Option<String>> {
68    let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
69        return Ok(None);
70    };
71    let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
72        return Ok(None);
73    };
74
75    let mut uki_files = Vec::new();
76    for entry in efi_linux.entries()? {
77        let entry = entry?;
78        let name = entry.file_name();
79        let name_path = Path::new(&name);
80        let extension = name_path.extension().and_then(|v| v.to_str());
81        if extension == Some("efi") {
82            if let Some(name_str) = name.to_str() {
83                uki_files.push(name_str.to_owned());
84            }
85        }
86    }
87
88    // Sort for deterministic behavior when multiple UKIs are present
89    uki_files.sort();
90    Ok(uki_files.into_iter().next())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};
97
98    #[test]
99    fn test_find_kernel_none() -> Result<()> {
100        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
101        assert!(find_kernel(&tempdir)?.is_none());
102        Ok(())
103    }
104
105    #[test]
106    fn test_find_kernel_traditional() -> Result<()> {
107        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
108        tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
109        tempdir.atomic_write(
110            "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
111            b"fake kernel",
112        )?;
113
114        let kernel = find_kernel(&tempdir)?.expect("should find kernel");
115        assert_eq!(kernel.version, "6.12.0-100.fc41.x86_64");
116        assert!(!kernel.unified);
117        Ok(())
118    }
119
120    #[test]
121    fn test_find_kernel_uki() -> Result<()> {
122        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
123        tempdir.create_dir_all("boot/EFI/Linux")?;
124        tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
125
126        let kernel = find_kernel(&tempdir)?.expect("should find kernel");
127        assert_eq!(kernel.version, "fedora-6.12.0");
128        assert!(kernel.unified);
129        Ok(())
130    }
131
132    #[test]
133    fn test_find_kernel_uki_takes_precedence() -> Result<()> {
134        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
135        // Both traditional and UKI exist
136        tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
137        tempdir.atomic_write(
138            "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
139            b"fake kernel",
140        )?;
141        tempdir.create_dir_all("boot/EFI/Linux")?;
142        tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
143
144        let kernel = find_kernel(&tempdir)?.expect("should find kernel");
145        // UKI should take precedence
146        assert_eq!(kernel.version, "fedora-6.12.0");
147        assert!(kernel.unified);
148        Ok(())
149    }
150
151    #[test]
152    fn test_find_uki_filename_sorted() -> Result<()> {
153        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
154        tempdir.create_dir_all("boot/EFI/Linux")?;
155        tempdir.atomic_write("boot/EFI/Linux/zzz.efi", b"fake uki")?;
156        tempdir.atomic_write("boot/EFI/Linux/aaa.efi", b"fake uki")?;
157        tempdir.atomic_write("boot/EFI/Linux/mmm.efi", b"fake uki")?;
158
159        // Should return first in sorted order
160        let filename = find_uki_filename(&tempdir)?.expect("should find uki");
161        assert_eq!(filename, "aaa.efi");
162        Ok(())
163    }
164}