Skip to main content

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