Skip to main content

bootc_lib/bootc_composefs/backwards_compat/
bcompat_boot.rs

1use std::io::{Read, Write};
2
3use crate::{
4    bootc_composefs::{
5        boot::{
6            BOOTC_UKI_DIR, BootType, FILENAME_PRIORITY_PRIMARY, FILENAME_PRIORITY_SECONDARY,
7            get_efi_uuid_source, get_uki_name, parse_os_release, type1_entry_conf_file_name,
8        },
9        rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg},
10        status::{
11            ComposefsCmdline, get_bootloader, get_sorted_grub_uki_boot_entries,
12            get_sorted_type1_boot_entries,
13        },
14    },
15    composefs_consts::{
16        ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE, TYPE1_BOOT_DIR_PREFIX,
17        TYPE1_ENT_PATH_STAGED, UKI_NAME_PREFIX, USER_CFG_STAGED,
18    },
19    parsers::bls_config::{BLSConfig, BLSConfigType},
20    spec::Bootloader,
21    store::Storage,
22};
23use anyhow::{Context, Result};
24use camino::Utf8PathBuf;
25use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
26use composefs_ctl::composefs_boot::bootloader::{EFI_ADDON_DIR_EXT, EFI_EXT};
27use fn_error_context::context;
28use ocidir::cap_std::ambient_authority;
29use rustix::fs::{RenameFlags, fsync, renameat_with};
30
31/// Represents a pending rename operation to be executed atomically
32#[derive(Debug)]
33struct PendingRename {
34    old_name: String,
35    new_name: String,
36}
37
38/// Transaction context for managing atomic renames (both files and directories)
39#[derive(Debug)]
40struct RenameTransaction {
41    operations: Vec<PendingRename>,
42}
43
44impl RenameTransaction {
45    fn new() -> Self {
46        Self {
47            operations: Vec::new(),
48        }
49    }
50
51    fn add_operation(&mut self, old_name: String, new_name: String) {
52        self.operations.push(PendingRename { old_name, new_name });
53    }
54
55    /// Execute all renames atomically in the provided directory
56    /// If any operation fails, attempt to rollback all completed operations
57    ///
58    /// We currently only have two entries at max, so this is quite unlikely to fail...
59    #[context("Executing rename transactions")]
60    fn execute_transaction(&self, target_dir: &Dir) -> Result<()> {
61        let mut completed_operations = Vec::new();
62
63        for op in &self.operations {
64            match renameat_with(
65                target_dir,
66                &op.old_name,
67                target_dir,
68                &op.new_name,
69                RenameFlags::empty(),
70            ) {
71                Ok(()) => {
72                    completed_operations.push(op);
73                    tracing::debug!("Renamed {} -> {}", op.old_name, op.new_name);
74                }
75                Err(e) => {
76                    // Attempt rollback of completed operations
77                    for completed_op in completed_operations.iter().rev() {
78                        if let Err(rollback_err) = renameat_with(
79                            target_dir,
80                            &completed_op.new_name,
81                            target_dir,
82                            &completed_op.old_name,
83                            RenameFlags::empty(),
84                        ) {
85                            tracing::error!(
86                                "Rollback failed for {} -> {}: {}",
87                                completed_op.new_name,
88                                completed_op.old_name,
89                                rollback_err
90                            );
91                        }
92                    }
93
94                    return Err(e).context(format!("Failed to rename {}", op.old_name));
95                }
96            }
97        }
98
99        Ok(())
100    }
101}
102
103/// Plan EFI binary renames and populate the transaction
104/// The actual renames are deferred to the transaction
105#[context("Planning EFI renames")]
106fn plan_efi_binary_renames(
107    esp: &Dir,
108    digest: &str,
109    rename_transaction: &mut RenameTransaction,
110) -> Result<()> {
111    let bootc_uki_dir = esp.open_dir(BOOTC_UKI_DIR)?;
112
113    for entry in bootc_uki_dir.entries_utf8()? {
114        let entry = entry?;
115        let filename = entry.file_name()?;
116
117        if filename.starts_with(UKI_NAME_PREFIX) {
118            continue;
119        }
120
121        if !filename.ends_with(EFI_EXT) && !filename.ends_with(EFI_ADDON_DIR_EXT) {
122            continue;
123        }
124
125        if !filename.contains(digest) {
126            continue;
127        }
128
129        let new_name = format!("{UKI_NAME_PREFIX}{filename}");
130        rename_transaction.add_operation(filename.to_string(), new_name);
131    }
132
133    Ok(())
134}
135
136/// Plan BLS directory renames and populate the transaction
137/// The actual renames are deferred to the transaction
138#[context("Planning BLS directory renames")]
139fn plan_bls_entry_rename(binaries_dir: &Dir, entry_to_fix: &str) -> Result<Option<String>> {
140    for entry in binaries_dir.entries_utf8()? {
141        let entry = entry?;
142        let filename = entry.file_name()?;
143
144        // We don't really put any files here, but just in case
145        if !entry.file_type()?.is_dir() {
146            continue;
147        }
148
149        if filename != entry_to_fix {
150            continue;
151        }
152
153        let new_name = format!("{TYPE1_BOOT_DIR_PREFIX}{filename}");
154        return Ok(Some(new_name));
155    }
156
157    Ok(None)
158}
159
160#[context("Staging BLS entry changes")]
161fn stage_bls_entry_changes(
162    storage: &Storage,
163    boot_dir: &Dir,
164    entries: &Vec<BLSConfig>,
165    cfs_cmdline: &ComposefsCmdline,
166) -> Result<(RenameTransaction, Vec<(String, BLSConfig)>)> {
167    let mut rename_transaction = RenameTransaction::new();
168
169    let root = Dir::open_ambient_dir("/", ambient_authority())?;
170    let osrel = parse_os_release(&root)?;
171
172    let os_id = osrel
173        .as_ref()
174        .map(|(s, _, _)| s.as_str())
175        .unwrap_or("bootc");
176
177    // to not add duplicate transactions since we share BLS entries
178    // across deployements
179    let mut fixed = vec![];
180    let mut new_bls_entries = vec![];
181
182    for entry in entries {
183        let (digest, has_prefix) = entry.boot_artifact_info()?;
184        let digest = digest.to_string();
185
186        if has_prefix {
187            continue;
188        }
189
190        let mut new_entry = entry.clone();
191
192        let conf_filename = if *cfs_cmdline.digest == digest {
193            type1_entry_conf_file_name(os_id, new_entry.version(), FILENAME_PRIORITY_PRIMARY)
194        } else {
195            type1_entry_conf_file_name(os_id, new_entry.version(), FILENAME_PRIORITY_SECONDARY)
196        };
197
198        match &mut new_entry.cfg_type {
199            BLSConfigType::NonEFI { linux, initrd, .. } => {
200                let new_name =
201                    plan_bls_entry_rename(&storage.bls_boot_binaries_dir()?, &digest)?
202                        .ok_or_else(|| anyhow::anyhow!("Directory for entry {digest} not found"))?;
203
204                // We don't want this multiple times in the rename_transaction if it was already
205                // "fixed"
206                if !fixed.contains(&digest) {
207                    rename_transaction.add_operation(digest.clone(), new_name.clone());
208                }
209
210                *linux = linux.as_str().replace(&digest, &new_name).into();
211                *initrd = initrd
212                    .iter_mut()
213                    .map(|path| path.as_str().replace(&digest, &new_name).into())
214                    .collect();
215            }
216
217            BLSConfigType::EFI { efi, .. } => {
218                // boot_dir in case of UKI is the ESP
219                plan_efi_binary_renames(&boot_dir, &digest, &mut rename_transaction)?;
220                *efi = Utf8PathBuf::from("/")
221                    .join(BOOTC_UKI_DIR)
222                    .join(get_uki_name(&digest));
223            }
224
225            _ => anyhow::bail!("Unknown BLS config type"),
226        }
227
228        new_bls_entries.push((conf_filename, new_entry));
229        fixed.push(digest.into());
230    }
231
232    Ok((rename_transaction, new_bls_entries))
233}
234
235fn create_staged_bls_entries(boot_dir: &Dir, entries: &Vec<(String, BLSConfig)>) -> Result<()> {
236    boot_dir.create_dir_all(TYPE1_ENT_PATH_STAGED)?;
237    let staged_entries = boot_dir.open_dir(TYPE1_ENT_PATH_STAGED)?;
238
239    for (filename, new_entry) in entries {
240        staged_entries.atomic_write(filename, new_entry.to_string().as_bytes())?;
241    }
242
243    fsync(staged_entries.reopen_as_ownedfd()?).context("fsync")
244}
245
246fn get_boot_type(storage: &Storage, cfs_cmdline: &ComposefsCmdline) -> Result<BootType> {
247    let mut config = String::new();
248
249    let origin_path = Utf8PathBuf::from(STATE_DIR_RELATIVE)
250        .join(&*cfs_cmdline.digest)
251        .join(format!("{}.origin", cfs_cmdline.digest));
252
253    storage
254        .physical_root
255        .open(origin_path)
256        .context("Opening origin file")?
257        .read_to_string(&mut config)
258        .context("Reading origin file")?;
259
260    let origin = tini::Ini::from_string(&config)
261        .with_context(|| format!("Failed to parse origin as ini"))?;
262
263    let boot_type = match origin.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE) {
264        Some(s) => BootType::try_from(s.as_str())?,
265        None => anyhow::bail!("{ORIGIN_KEY_BOOT} not found"),
266    };
267
268    Ok(boot_type)
269}
270
271fn handle_bls_conf(
272    storage: &Storage,
273    cfs_cmdline: &ComposefsCmdline,
274    boot_dir: &Dir,
275    is_uki: bool,
276) -> Result<()> {
277    let entries = get_sorted_type1_boot_entries(boot_dir, true)?;
278    let (rename_transaction, new_bls_entries) =
279        stage_bls_entry_changes(storage, boot_dir, &entries, cfs_cmdline)?;
280
281    if rename_transaction.operations.is_empty() {
282        tracing::debug!("Nothing to do");
283        return Ok(());
284    }
285
286    create_staged_bls_entries(boot_dir, &new_bls_entries)?;
287
288    let binaries_dir = if is_uki {
289        let esp = storage.require_esp()?;
290        let uki_dir = esp.fd.open_dir(BOOTC_UKI_DIR).context("Opening UKI dir")?;
291
292        uki_dir
293    } else {
294        storage.bls_boot_binaries_dir()?
295    };
296
297    // execute all EFI PE renames atomically before the final exchange
298    rename_transaction
299        .execute_transaction(&binaries_dir)
300        .context("Failed to execute EFI binary rename transaction")?;
301
302    fsync(binaries_dir.reopen_as_ownedfd()?)?;
303
304    let loader_dir = boot_dir.open_dir("loader").context("Opening loader dir")?;
305    rename_exchange_bls_entries(&loader_dir)?;
306
307    Ok(())
308}
309
310/// Goes through the ESP and prepends every UKI/Addon with our custom prefix
311/// Goes through the BLS entries and prepends our custom prefix
312#[context("Prepending custom prefix to EFI and BLS entries")]
313pub(crate) async fn prepend_custom_prefix(
314    storage: &Storage,
315    cfs_cmdline: &ComposefsCmdline,
316) -> Result<()> {
317    let boot_dir = storage.require_boot_dir()?;
318
319    let bootloader = get_bootloader()?;
320
321    match get_boot_type(storage, cfs_cmdline)? {
322        BootType::Bls => {
323            handle_bls_conf(storage, cfs_cmdline, boot_dir, false)?;
324        }
325
326        BootType::Uki => match bootloader {
327            Bootloader::Grub => {
328                let esp = storage.require_esp()?;
329
330                let mut buf = String::new();
331                let menuentries = get_sorted_grub_uki_boot_entries(boot_dir, &mut buf)?;
332
333                let mut new_menuentries = vec![];
334                let mut rename_transaction = RenameTransaction::new();
335
336                for entry in menuentries {
337                    let (digest, has_prefix) = entry.boot_artifact_info()?;
338                    let digest = digest.to_string();
339
340                    if has_prefix {
341                        continue;
342                    }
343
344                    plan_efi_binary_renames(&esp.fd, &digest, &mut rename_transaction)?;
345
346                    let new_path = Utf8PathBuf::from("/")
347                        .join(BOOTC_UKI_DIR)
348                        .join(get_uki_name(&digest));
349
350                    let mut new_entry = entry.clone();
351                    new_entry.body.chainloader = new_path.into();
352
353                    new_menuentries.push(new_entry);
354                }
355
356                if rename_transaction.operations.is_empty() {
357                    tracing::debug!("Nothing to do");
358                    return Ok(());
359                }
360
361                let grub_dir = boot_dir.open_dir("grub2").context("opening boot/grub2")?;
362
363                grub_dir
364                    .atomic_replace_with(USER_CFG_STAGED, |f| -> std::io::Result<_> {
365                        f.write_all(get_efi_uuid_source().as_bytes())?;
366
367                        for entry in new_menuentries {
368                            f.write_all(entry.to_string().as_bytes())?;
369                        }
370
371                        Ok(())
372                    })
373                    .with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
374
375                let esp = storage.require_esp()?;
376                let uki_dir = esp.fd.open_dir(BOOTC_UKI_DIR).context("Opening UKI dir")?;
377
378                // execute all EFI PE renames atomically before the final exchange
379                rename_transaction
380                    .execute_transaction(&uki_dir)
381                    .context("Failed to execute EFI binary rename transaction")?;
382
383                fsync(uki_dir.reopen_as_ownedfd()?)?;
384                rename_exchange_user_cfg(&grub_dir)?;
385            }
386
387            Bootloader::Systemd => {
388                handle_bls_conf(storage, cfs_cmdline, boot_dir, true)?;
389            }
390
391            Bootloader::None => unreachable!("Checked at install time"),
392        },
393    };
394
395    Ok(())
396}