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}