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 camino::Utf8PathBuf;
11use cap_std_ext::cap_std::fs::Dir;
12use cap_std_ext::dirext::CapStdExtDirExt;
13use serde::Serialize;
14
15use crate::bootc_composefs::boot::EFI_LINUX;
16
17/// Information about the kernel in a container image.
18#[derive(Debug, Serialize)]
19#[serde(rename_all = "kebab-case")]
20pub(crate) struct Kernel {
21    /// The kernel version identifier. For traditional kernels, this is derived from the
22    /// `/usr/lib/modules/<version>` directory name. For UKI images, this is the UKI filename
23    /// (without the .efi extension).
24    pub(crate) version: String,
25    /// Whether the kernel is packaged as a UKI (Unified Kernel Image).
26    pub(crate) unified: bool,
27}
28
29/// Path to kernel component(s)
30///
31/// UKI kernels only have the single PE binary, whereas
32/// traditional "vmlinuz" kernels have distinct kernel and
33/// initramfs.
34pub(crate) enum KernelPath {
35    Uki(Utf8PathBuf),
36    Vmlinuz {
37        path: Utf8PathBuf,
38        initramfs: Utf8PathBuf,
39    },
40}
41
42/// Internal-only kernel wrapper with extra path information that are
43/// useful but we don't want to leak out via serialization to
44/// inspection.
45///
46/// `Kernel` implements `From<KernelInternal>` so we can just `.into()`
47/// to get the "public" form where needed.
48pub(crate) struct KernelInternal {
49    pub(crate) kernel: Kernel,
50    pub(crate) path: KernelPath,
51}
52
53impl From<KernelInternal> for Kernel {
54    fn from(kernel_internal: KernelInternal) -> Self {
55        kernel_internal.kernel
56    }
57}
58
59/// Find the kernel in a container image root directory.
60///
61/// This function first attempts to find a UKI in `/boot/EFI/Linux/*.efi`.
62/// If that doesn't exist, it falls back to looking for a traditional kernel
63/// layout with `/usr/lib/modules/<version>/vmlinuz`.
64///
65/// Returns `None` if no kernel is found.
66pub(crate) fn find_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
67    // First, try to find a UKI
68    if let Some(uki_path) = find_uki_path(root)? {
69        let version = uki_path.file_stem().unwrap_or(uki_path.as_str()).to_owned();
70        return Ok(Some(KernelInternal {
71            kernel: Kernel {
72                version,
73                unified: true,
74            },
75            path: KernelPath::Uki(uki_path),
76        }));
77    }
78
79    // Fall back to checking for a traditional kernel via ostree_ext
80    if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
81        let version = modules_dir
82            .file_name()
83            .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
84            .to_owned();
85        let vmlinuz = modules_dir.join("vmlinuz");
86        let initramfs = modules_dir.join("initramfs.img");
87        return Ok(Some(KernelInternal {
88            kernel: Kernel {
89                version,
90                unified: false,
91            },
92            path: KernelPath::Vmlinuz {
93                path: vmlinuz,
94                initramfs,
95            },
96        }));
97    }
98
99    Ok(None)
100}
101
102/// Returns the path to the first UKI found in the container root, if any.
103///
104/// Looks in `/boot/EFI/Linux/*.efi`. If multiple UKIs are present, returns
105/// the first one in sorted order for determinism.
106fn find_uki_path(root: &Dir) -> Result<Option<Utf8PathBuf>> {
107    let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
108        return Ok(None);
109    };
110    let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
111        return Ok(None);
112    };
113
114    let mut uki_files = Vec::new();
115    for entry in efi_linux.entries()? {
116        let entry = entry?;
117        let name = entry.file_name();
118        let name_path = Path::new(&name);
119        let extension = name_path.extension().and_then(|v| v.to_str());
120        if extension == Some("efi") {
121            if let Some(name_str) = name.to_str() {
122                uki_files.push(name_str.to_owned());
123            }
124        }
125    }
126
127    // Sort for deterministic behavior when multiple UKIs are present
128    uki_files.sort();
129    Ok(uki_files
130        .into_iter()
131        .next()
132        .map(|filename| Utf8PathBuf::from(format!("boot/{EFI_LINUX}/{filename}"))))
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};
139
140    #[test]
141    fn test_find_kernel_none() -> Result<()> {
142        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
143        assert!(find_kernel(&tempdir)?.is_none());
144        Ok(())
145    }
146
147    #[test]
148    fn test_find_kernel_traditional() -> Result<()> {
149        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
150        tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
151        tempdir.atomic_write(
152            "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
153            b"fake kernel",
154        )?;
155
156        let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
157        assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
158        assert!(!kernel_internal.kernel.unified);
159        match &kernel_internal.path {
160            KernelPath::Vmlinuz { path, initramfs } => {
161                assert_eq!(
162                    path.as_str(),
163                    "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
164                );
165                assert_eq!(
166                    initramfs.as_str(),
167                    "usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
168                );
169            }
170            KernelPath::Uki(_) => panic!("Expected Vmlinuz, got Uki"),
171        }
172        Ok(())
173    }
174
175    #[test]
176    fn test_find_kernel_uki() -> Result<()> {
177        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
178        tempdir.create_dir_all("boot/EFI/Linux")?;
179        tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
180
181        let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
182        assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
183        assert!(kernel_internal.kernel.unified);
184        match &kernel_internal.path {
185            KernelPath::Uki(path) => {
186                assert_eq!(path.as_str(), "boot/EFI/Linux/fedora-6.12.0.efi");
187            }
188            KernelPath::Vmlinuz { .. } => panic!("Expected Uki, got Vmlinuz"),
189        }
190        Ok(())
191    }
192
193    #[test]
194    fn test_find_kernel_uki_takes_precedence() -> Result<()> {
195        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
196        // Both traditional and UKI exist
197        tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
198        tempdir.atomic_write(
199            "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
200            b"fake kernel",
201        )?;
202        tempdir.create_dir_all("boot/EFI/Linux")?;
203        tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
204
205        let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
206        // UKI should take precedence
207        assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
208        assert!(kernel_internal.kernel.unified);
209        Ok(())
210    }
211
212    #[test]
213    fn test_find_uki_path_sorted() -> Result<()> {
214        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
215        tempdir.create_dir_all("boot/EFI/Linux")?;
216        tempdir.atomic_write("boot/EFI/Linux/zzz.efi", b"fake uki")?;
217        tempdir.atomic_write("boot/EFI/Linux/aaa.efi", b"fake uki")?;
218        tempdir.atomic_write("boot/EFI/Linux/mmm.efi", b"fake uki")?;
219
220        // Should return first in sorted order
221        let path = find_uki_path(&tempdir)?.expect("should find uki");
222        assert_eq!(path.as_str(), "boot/EFI/Linux/aaa.efi");
223        Ok(())
224    }
225}