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 cfsctl::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 uki = root.read(&uki_path).context("Reading UKI")?;
79
80        // Best effort to check for composefs=?verity in the UKI cmdline
81        let cmdline = composefs_boot::uki::get_section(&uki, ".cmdline");
82
83        let cmdline = match cmdline {
84            Some(Ok(cmdline)) => {
85                let cmdline_str = std::str::from_utf8(cmdline)?;
86                Some(Cmdline::from(cmdline_str.to_owned()))
87            }
88
89            Some(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            None => None,
100        };
101
102        return Ok(Some(KernelInternal {
103            kernel: Kernel {
104                version,
105                unified: true,
106            },
107            k_type: KernelType::Uki {
108                path: uki_path,
109                cmdline,
110            },
111        }));
112    }
113
114    // Fall back to checking for a traditional kernel via ostree_ext
115    if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
116        let version = modules_dir
117            .file_name()
118            .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
119            .to_owned();
120        let vmlinuz = modules_dir.join("vmlinuz");
121        let initramfs = modules_dir.join("initramfs.img");
122        return Ok(Some(KernelInternal {
123            kernel: Kernel {
124                version,
125                unified: false,
126            },
127            k_type: KernelType::Vmlinuz {
128                path: vmlinuz,
129                initramfs,
130            },
131        }));
132    }
133
134    Ok(None)
135}
136
137/// Returns the path to the first UKI found in the container root, if any.
138///
139/// Looks in `/boot/EFI/Linux/*.efi`. If multiple UKIs are present, returns
140/// the first one in sorted order for determinism.
141fn find_uki_path(root: &Dir) -> Result<Option<Utf8PathBuf>> {
142    let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
143        return Ok(None);
144    };
145    let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
146        return Ok(None);
147    };
148
149    let mut uki_files = Vec::new();
150    for entry in efi_linux.entries()? {
151        let entry = entry?;
152        let name = entry.file_name();
153        let name_path = Path::new(&name);
154        let extension = name_path.extension().and_then(|v| v.to_str());
155        if extension == Some("efi") {
156            if let Some(name_str) = name.to_str() {
157                uki_files.push(name_str.to_owned());
158            }
159        }
160    }
161
162    // Sort for deterministic behavior when multiple UKIs are present
163    uki_files.sort();
164    Ok(uki_files
165        .into_iter()
166        .next()
167        .map(|filename| Utf8PathBuf::from(format!("boot/{EFI_LINUX}/{filename}"))))
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};
174
175    #[test]
176    fn test_find_kernel_none() -> Result<()> {
177        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
178        assert!(find_kernel(&tempdir)?.is_none());
179        Ok(())
180    }
181
182    #[test]
183    fn test_find_kernel_traditional() -> Result<()> {
184        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
185        tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
186        tempdir.atomic_write(
187            "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
188            b"fake kernel",
189        )?;
190
191        let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
192        assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
193        assert!(!kernel_internal.kernel.unified);
194        match &kernel_internal.k_type {
195            KernelType::Vmlinuz { path, initramfs } => {
196                assert_eq!(
197                    path.as_str(),
198                    "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
199                );
200                assert_eq!(
201                    initramfs.as_str(),
202                    "usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
203                );
204            }
205            KernelType::Uki { .. } => panic!("Expected Vmlinuz, got Uki"),
206        }
207        Ok(())
208    }
209
210    #[test]
211    fn test_find_kernel_uki() -> Result<()> {
212        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
213        tempdir.create_dir_all("boot/EFI/Linux")?;
214        tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
215
216        let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
217        assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
218        assert!(kernel_internal.kernel.unified);
219        match &kernel_internal.k_type {
220            KernelType::Uki { path, .. } => {
221                assert_eq!(path.as_str(), "boot/EFI/Linux/fedora-6.12.0.efi");
222            }
223            KernelType::Vmlinuz { .. } => panic!("Expected Uki, got Vmlinuz"),
224        }
225        Ok(())
226    }
227
228    #[test]
229    fn test_find_kernel_uki_takes_precedence() -> Result<()> {
230        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
231        // Both traditional and UKI exist
232        tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
233        tempdir.atomic_write(
234            "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
235            b"fake kernel",
236        )?;
237        tempdir.create_dir_all("boot/EFI/Linux")?;
238        tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
239
240        let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
241        // UKI should take precedence
242        assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
243        assert!(kernel_internal.kernel.unified);
244        Ok(())
245    }
246
247    #[test]
248    fn test_find_uki_path_sorted() -> Result<()> {
249        let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
250        tempdir.create_dir_all("boot/EFI/Linux")?;
251        tempdir.atomic_write("boot/EFI/Linux/zzz.efi", b"fake uki")?;
252        tempdir.atomic_write("boot/EFI/Linux/aaa.efi", b"fake uki")?;
253        tempdir.atomic_write("boot/EFI/Linux/mmm.efi", b"fake uki")?;
254
255        // Should return first in sorted order
256        let path = find_uki_path(&tempdir)?.expect("should find uki");
257        assert_eq!(path.as_str(), "boot/EFI/Linux/aaa.efi");
258        Ok(())
259    }
260}