Skip to main content

composefs_boot/
lib.rs

1//! Boot integration for composefs filesystem images.
2//!
3//! This crate provides functionality to transform composefs filesystem images for boot
4//! scenarios by extracting boot resources, applying SELinux labels, and preparing
5//! bootloader entries. It supports both Boot Loader Specification (Type 1) entries
6//! and Unified Kernel Images (Type 2) for UEFI boot.
7
8#![forbid(unsafe_code)]
9#![deny(missing_debug_implementations)]
10
11pub mod bootloader;
12pub mod cmdline;
13pub mod os_release;
14pub mod selabel;
15pub mod uki;
16pub mod write_boot;
17
18#[cfg(doc)]
19pub mod design;
20
21use std::ffi::OsStr;
22
23use anyhow::Result;
24use rustix::fd::AsFd;
25
26use composefs::{fsverity::FsVerityHashValue, repository::Repository, tree::FileSystem};
27
28use crate::bootloader::{BootEntry, get_boot_resources};
29
30/// These directories are required to exist in images.
31/// They may have content in the container, but we don't
32/// want to expose them in the final merged root.
33///
34/// # /boot
35///
36/// This is how sealed UKIs are handled; the UKI in /boot has the composefs
37/// digest, so we can't include it in the rendered image.
38///
39/// # /sysroot
40///
41/// See https://github.com/containers/composefs-rs/issues/164
42/// Basically there is only content here in ostree-container cases,
43/// and us traversing there for SELinux labeling will cause problems.
44/// The ostree-container code special cases it in a different way, but
45/// here we can just ignore it.
46const REQUIRED_TOPLEVEL_TO_EMPTY_DIRS: &[&str] = &["boot", "sysroot"];
47
48/// Empty the required top-level directories and set their mtime to match /usr.
49fn empty_toplevel_dirs<ObjectID: FsVerityHashValue>(fs: &mut FileSystem<ObjectID>) -> Result<()> {
50    let usr_mtime = fs.root.get_directory(OsStr::new("usr"))?.stat.st_mtim_sec;
51
52    for d in REQUIRED_TOPLEVEL_TO_EMPTY_DIRS {
53        let d = fs.root.get_directory_mut(d.as_ref())?;
54        d.stat.st_mtim_sec = usr_mtime;
55        d.clear();
56    }
57
58    Ok(())
59}
60
61/// Trait for transforming filesystem images for boot scenarios.
62///
63/// This trait provides functionality to prepare composefs filesystem images for booting by
64/// extracting boot resources and applying necessary transformations like SELinux labeling.
65pub trait BootOps<ObjectID: FsVerityHashValue> {
66    /// Transforms a filesystem image for boot by extracting boot entries and applying SELinux labels.
67    ///
68    /// This method extracts boot resources from the filesystem, empties required top-level
69    /// directories (/boot, /sysroot), and applies SELinux security contexts.
70    ///
71    /// # Arguments
72    ///
73    /// * `repo` - The composefs repository containing filesystem objects
74    ///
75    /// # Returns
76    ///
77    /// A vector of boot entries extracted from the filesystem (Type 1 BLS entries, Type 2 UKIs, etc.)
78    fn transform_for_boot(
79        &mut self,
80        repo: &Repository<ObjectID>,
81    ) -> Result<Vec<BootEntry<ObjectID>>>;
82
83    /// Apply boot filesystem transformations using an on-disk directory for file content.
84    ///
85    /// This applies the same filesystem transformations as [`BootOps::transform_for_boot`]
86    /// (emptying /boot and /sysroot, SELinux relabeling) but reads SELinux policy files
87    /// directly from the on-disk filesystem via a directory fd rather than from the
88    /// composefs repository.
89    ///
90    /// This does not extract boot entries (Type 1 BLS entries, UKIs, etc.) since those
91    /// are only needed for writing to the boot partition, not for computing the composefs
92    /// digest.
93    ///
94    /// # Arguments
95    ///
96    /// * `rootfs` - A directory fd pointing to the root of the on-disk filesystem
97    fn transform_for_boot_from_dir(&mut self, rootfs: impl AsFd) -> Result<()>;
98}
99
100impl<ObjectID: FsVerityHashValue> BootOps<ObjectID> for FileSystem<ObjectID> {
101    fn transform_for_boot(
102        &mut self,
103        repo: &Repository<ObjectID>,
104    ) -> Result<Vec<BootEntry<ObjectID>>> {
105        let boot_entries = get_boot_resources(self, repo)?;
106        empty_toplevel_dirs(self)?;
107        // Compact the leaves table after clearing directories, so that leaves
108        // which were only referenced by /boot or /sysroot are removed and
109        // don't appear as orphans when the filesystem is validated.
110        self.compact();
111        selabel::selabel(self, repo)?;
112
113        Ok(boot_entries)
114    }
115
116    fn transform_for_boot_from_dir(&mut self, rootfs: impl AsFd) -> Result<()> {
117        empty_toplevel_dirs(self)?;
118        // Same as above: compact to remove leaves orphaned by clearing dirs.
119        self.compact();
120        selabel::selabel_from_dir(self, rootfs)?;
121        Ok(())
122    }
123}