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