bootc_lib/bootc_composefs/backwards_compat/
bcompat_boot.rs1use 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#[derive(Debug)]
33struct PendingRename {
34 old_name: String,
35 new_name: String,
36}
37
38#[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 #[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 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#[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#[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 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 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 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 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 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#[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 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}