Skip to main content

bootc_lib/bootc_composefs/
boot_partition.rs

1//! Selecting the partition that holds the composefs boot entries.
2//!
3//! Per the Boot Loader Specification, boot entries go on the Extended Boot
4//! Loader Partition (XBOOTLDR) on the ESP's disk if there is one, and on the
5//! ESP otherwise. bootc follows that for bootloaders that read entries that
6//! way (see [`Bootloader::is_bls_conformant`]), with one exception: systems
7//! installed by a bootc that didn't use XBOOTLDR partitions keep their entries
8//! on the ESP, so a booted system keeps using the partition holding the booted
9//! deployment's entries.
10
11use anyhow::Result;
12use bootc_mount::tempmount::TempMount;
13use camino::{Utf8Path, Utf8PathBuf};
14use cap_std_ext::cap_std::fs::Dir;
15use cap_std_ext::dirext::CapStdExtDirExt;
16
17use crate::composefs_consts::{BLS_ENTRY_FILE_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED};
18use crate::parsers::bls_config::parse_bls_config;
19use crate::spec::Bootloader;
20
21/// The partition holding UKIs, Type #1 entries and their kernels and
22/// initramfs images.
23#[derive(Debug, Clone)]
24pub(crate) enum BootPartition {
25    /// The EFI System Partition
26    Esp(Utf8PathBuf),
27    /// An Extended Boot Loader Partition on the ESP's disk. It holds UKIs
28    /// that don't fit on a small ESP, such as one shared with Windows.
29    Xbootldr(Utf8PathBuf),
30}
31
32impl BootPartition {
33    /// The partition to install entries to, given the ESP at `esp`: the VFAT
34    /// XBOOTLDR partition next to it if `bootloader` reads one and there is
35    /// one, and the ESP otherwise.
36    pub(crate) fn for_install(esp: Utf8PathBuf, bootloader: Bootloader) -> Result<Self> {
37        Ok(match sibling_xbootldr(&esp, bootloader)? {
38            Some(xbootldr) => Self::Xbootldr(xbootldr),
39            None => Self::Esp(esp),
40        })
41    }
42
43    /// Find and mount, with `mount`, the partition holding the entries of
44    /// the booted deployment `booted_verity`, given the ESP at `esp` it was
45    /// booted from.
46    pub(crate) fn mount_booted(
47        esp: Utf8PathBuf,
48        bootloader: Bootloader,
49        booted_verity: &str,
50        mount: impl Fn(&Utf8Path) -> Result<TempMount>,
51    ) -> Result<(Self, TempMount)> {
52        if let Some(xbootldr) = sibling_xbootldr(&esp, bootloader)? {
53            let mounted = mount(&xbootldr)?;
54            if has_type1_entry_for(&mounted.fd, booted_verity)? {
55                return Ok((Self::Xbootldr(xbootldr), mounted));
56            }
57            tracing::debug!(
58                "The booted deployment has no entry on XBOOTLDR partition {xbootldr}, using {esp}"
59            );
60        }
61        let mounted = mount(&esp)?;
62        Ok((Self::Esp(esp), mounted))
63    }
64
65    /// The partition's device path
66    pub(crate) fn device(&self) -> &Utf8Path {
67        match self {
68            Self::Esp(device) | Self::Xbootldr(device) => device,
69        }
70    }
71}
72
73fn sibling_xbootldr(esp: &Utf8Path, bootloader: Bootloader) -> Result<Option<Utf8PathBuf>> {
74    if !bootloader.is_bls_conformant() {
75        return Ok(None);
76    }
77    let xbootldr = bootc_blockdev::list_dev(esp)?.find_sibling_xbootldr()?;
78    Ok(xbootldr.map(|xbootldr| {
79        tracing::debug!("Found XBOOTLDR partition {} next to {esp}", xbootldr.path());
80        Utf8PathBuf::from(xbootldr.path())
81    }))
82}
83
84/// Whether `boot_dir` has one of our Type #1 entries, staged or not, for the
85/// deployment `verity`.
86fn has_type1_entry_for(boot_dir: &Dir, verity: &str) -> Result<bool> {
87    for path in [TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED] {
88        let Some(entries) = boot_dir.open_dir_optional(path)? else {
89            continue;
90        };
91        for entry in entries.entries_utf8()? {
92            let name = entry?.file_name()?;
93            if !(name.starts_with(BLS_ENTRY_FILE_PREFIX) && name.ends_with(".conf")) {
94                continue;
95            }
96            let entry = parse_bls_config(&entries.read_to_string(&name)?)?;
97            if entry.get_verity().is_ok_and(|v| v == verity) {
98                return Ok(true);
99            }
100        }
101    }
102    Ok(false)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::testutils::{ChangeType, TestRoot, fake_digest_version};
109
110    #[test]
111    fn test_has_type1_entry_for() -> Result<()> {
112        let mut root = TestRoot::new()?;
113        let installed = root.current().verity.clone();
114        root.upgrade(1, ChangeType::Kernel)?;
115        let upgraded = root.current().verity.clone();
116        let boot_dir = root.boot_dir()?;
117
118        assert!(has_type1_entry_for(&boot_dir, &installed)?);
119        assert!(has_type1_entry_for(&boot_dir, &upgraded)?);
120        assert!(!has_type1_entry_for(&boot_dir, &fake_digest_version(2))?);
121        Ok(())
122    }
123
124    #[test]
125    fn test_has_type1_entry_for_staged() -> Result<()> {
126        let root = TestRoot::new()?;
127        let verity = root.current().verity.clone();
128        let boot_dir = root.boot_dir()?;
129        boot_dir.rename(TYPE1_ENT_PATH, &boot_dir, TYPE1_ENT_PATH_STAGED)?;
130
131        assert!(has_type1_entry_for(&boot_dir, &verity)?);
132        Ok(())
133    }
134
135    #[test]
136    fn test_has_type1_entry_for_other_os() -> Result<()> {
137        let dir = cap_std_ext::cap_tempfile::tempdir(cap_std_ext::cap_std::ambient_authority())?;
138        let verity = fake_digest_version(0);
139        assert!(!has_type1_entry_for(&dir, &verity)?);
140
141        dir.create_dir_all(TYPE1_ENT_PATH)?;
142        dir.atomic_write(
143            format!("{TYPE1_ENT_PATH}/other-os.conf"),
144            "title Other\nlinux /vmlinuz\noptions root=UUID=other rw\n",
145        )?;
146        assert!(!has_type1_entry_for(&dir, &verity)?);
147        Ok(())
148    }
149}