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