diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 01d6b3f99..124b83687 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -8,10 +8,14 @@ use bootc_blockdev::find_parent_devices; use bootc_mount::inspect_filesystem; use bootc_utils::CommandRunExt; use camino::{Utf8Path, Utf8PathBuf}; -use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; +use cap_std_ext::{ + cap_std::{ambient_authority, fs::Dir}, + dirext::CapStdExtDirExt, +}; use clap::ValueEnum; use composefs::fs::read_file; -use composefs::tree::FileSystem; +use composefs::tree::{FileSystem, RegularFile}; +use composefs_boot::bootloader::{PEType, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT}; use composefs_boot::BootOps; use fn_error_context::context; use ostree_ext::composefs::{ @@ -160,8 +164,7 @@ fn compute_boot_digest( /// Returns the verity of the deployment that has a boot digest same as the one passed in #[context("Checking boot entry duplicates")] fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result> { - let deployments = - cap_std::fs::Dir::open_ambient_dir(STATE_DIR_ABS, cap_std::ambient_authority()); + let deployments = Dir::open_ambient_dir(STATE_DIR_ABS, ambient_authority()); let deployments = match deployments { Ok(d) => d, @@ -217,7 +220,7 @@ fn write_bls_boot_entries_to_disk( let path = boot_dir.join(&id_hex); create_dir_all(&path)?; - let entries_dir = cap_std::fs::Dir::open_ambient_dir(&path, cap_std::ambient_authority()) + let entries_dir = Dir::open_ambient_dir(&path, ambient_authority()) .with_context(|| format!("Opening {path}"))?; entries_dir @@ -270,7 +273,7 @@ pub(crate) fn setup_composefs_bls_boot( // TODO: Make this generic repo: ComposefsRepository, id: &Sha256HashValue, - entry: ComposefsBootEntry, + entry: &ComposefsBootEntry, ) -> Result { let id_hex = id.to_hex(); @@ -480,9 +483,8 @@ pub(crate) fn setup_composefs_bls_boot( // Scope to allow for proper unmounting { - let loader_entries_dir = - cap_std::fs::Dir::open_ambient_dir(&config_path, cap_std::ambient_authority()) - .with_context(|| format!("Opening {config_path:?}"))?; + let loader_entries_dir = Dir::open_ambient_dir(&config_path, ambient_authority()) + .with_context(|| format!("Opening {config_path:?}"))?; loader_entries_dir.atomic_write( // SAFETY: We set sort_key above @@ -522,124 +524,108 @@ pub(crate) fn setup_composefs_bls_boot( Ok(boot_digest) } -#[context("Setting up UKI boot")] -pub(crate) fn setup_composefs_uki_boot( - setup_type: BootSetupType, - // TODO: Make this generic - repo: ComposefsRepository, - id: &Sha256HashValue, - entry: ComposefsBootEntry, -) -> Result<()> { - let (root_path, esp_device, is_insecure_from_opts) = match setup_type { - BootSetupType::Setup((root_setup, state, ..)) => { - if let Some(v) = &state.config_opts.karg { - if v.len() > 0 { - tracing::warn!("kargs passed for UKI will be ignored"); - } +/// Writes a PortableExecutable to ESP along with any PE specific or Global addons +fn write_pe_to_esp( + repo: &ComposefsRepository, + file: &RegularFile, + file_path: &PathBuf, + pe_type: PEType, + uki_id: &String, + is_insecure_from_opts: bool, + mounted_efi: &PathBuf, +) -> Result> { + let efi_bin = read_file(file, &repo).context("Reading .efi binary")?; + + let mut boot_label = None; + + // UKI Extension might not even have a cmdline + // TODO: UKI Addon might also have a composefs= cmdline? + if matches!(pe_type, PEType::Uki) { + let cmdline = uki::get_cmdline(&efi_bin).context("Getting UKI cmdline")?; + + let (composefs_cmdline, insecure) = get_cmdline_composefs::(cmdline)?; + + // If the UKI cmdline does not match what the user has passed as cmdline option + // NOTE: This will only be checked for new installs and now upgrades/switches + match is_insecure_from_opts { + true if !insecure => { + tracing::warn!("--insecure passed as option but UKI cmdline does not support it"); } - let esp_part = root_setup - .device_info - .partitions - .iter() - .find(|p| p.parttype.as_str() == ESP_GUID) - .ok_or_else(|| anyhow!("ESP partition not found"))?; + false if insecure => { + tracing::warn!("UKI cmdline has composefs set as insecure"); + } - ( - root_setup.physical_root_path.clone(), - esp_part.node.clone(), - state.composefs_options.as_ref().map(|x| x.insecure), - ) + _ => { /* no-op */ } } - BootSetupType::Upgrade(..) => { - let sysroot = Utf8PathBuf::from("/sysroot"); - - let fsinfo = inspect_filesystem(&sysroot)?; - let parent_devices = find_parent_devices(&fsinfo.source)?; - - let Some(parent) = parent_devices.into_iter().next() else { - anyhow::bail!("Could not find parent device for mountpoint /sysroot"); - }; - - (sysroot, get_esp_partition(&parent)?.0, None) + if composefs_cmdline.to_hex() != *uki_id { + anyhow::bail!( + "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})" + ); } - }; - let temp_efi_dir = tempfile::tempdir() - .map_err(|e| anyhow::anyhow!("Failed to create temporary directory for EFI mount: {e}"))?; - let mounted_efi = temp_efi_dir.path().to_path_buf(); + boot_label = Some(uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?); + } - Task::new("Mounting ESP", "mount") - .args([&PathBuf::from(&esp_device), &mounted_efi.clone()]) - .run()?; + // Write the UKI to ESP + let efi_linux_path = mounted_efi.join(EFI_LINUX); + create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?; - let boot_label = match entry { - ComposefsBootEntry::Type1(..) => unimplemented!(), - ComposefsBootEntry::UsrLibModulesVmLinuz(..) => unimplemented!(), - - ComposefsBootEntry::Type2(type2_entry) => { - let uki = read_file(&type2_entry.file, &repo).context("Reading UKI")?; - let cmdline = uki::get_cmdline(&uki).context("Getting UKI cmdline")?; - let (composefs_cmdline, insecure) = get_cmdline_composefs::(cmdline)?; - - // If the UKI cmdline does not match what the user has passed as cmdline option - // NOTE: This will only be checked for new installs and now upgrades/switches - if let Some(is_insecure_from_opts) = is_insecure_from_opts { - match is_insecure_from_opts { - true => { - if !insecure { - tracing::warn!( - "--insecure passed as option but UKI cmdline does not support it" - ) - } - } + let final_pe_path = match file_path.parent() { + Some(parent) => { + let renamed_path = match parent.as_str()?.ends_with(EFI_ADDON_DIR_EXT) { + true => { + let dir_name = format!("{}{}", uki_id, EFI_ADDON_DIR_EXT); - false => { - if insecure { - tracing::warn!("UKI cmdline has composefs set as insecure") - } - } + parent + .parent() + .map(|p| p.join(&dir_name)) + .unwrap_or(dir_name.into()) } - } - - let boot_label = uki::get_boot_label(&uki).context("Getting UKI boot label")?; - if composefs_cmdline != *id { - anyhow::bail!( - "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {id:?})" - ); - } + false => parent.to_path_buf(), + }; - // Write the UKI to ESP - let efi_linux_path = mounted_efi.join(EFI_LINUX); - create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?; + let full_path = efi_linux_path.join(renamed_path); + create_dir_all(&full_path)?; - let efi_linux = - cap_std::fs::Dir::open_ambient_dir(&efi_linux_path, cap_std::ambient_authority()) - .with_context(|| format!("Opening {efi_linux_path:?}"))?; + full_path + } - efi_linux - .atomic_write(format!("{}.efi", id.to_hex()), uki) - .context("Writing UKI")?; + None => efi_linux_path, + }; - rustix::fs::fsync( - efi_linux - .reopen_as_ownedfd() - .context("Reopening as owned fd")?, - ) - .context("fsync")?; + let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority()) + .with_context(|| format!("Opening {final_pe_path:?}"))?; - boot_label - } + let pe_name = match pe_type { + PEType::Uki => format!("{}{}", uki_id, EFI_EXT), + PEType::UkiAddon => format!("{}{}", uki_id, EFI_ADDON_FILE_EXT), }; - Command::new("umount") - .arg(&mounted_efi) - .log_debug() - .run_inherited_with_cmd_context() - .context("Unmounting ESP")?; + pe_dir + .atomic_write(pe_name, efi_bin) + .context("Writing UKI")?; + + rustix::fs::fsync( + pe_dir + .reopen_as_ownedfd() + .context("Reopening as owned fd")?, + ) + .context("fsync")?; + + Ok(boot_label) +} +#[context("Writing Grub menuentry")] +fn write_grub_uki_menuentry( + root_path: Utf8PathBuf, + setup_type: &BootSetupType, + boot_label: &String, + id: &Sha256HashValue, + esp_device: &String, +) -> Result<()> { let boot_dir = root_path.join("boot"); create_dir_all(&boot_dir).context("Failed to create boot dir")?; @@ -653,9 +639,8 @@ pub(crate) fn setup_composefs_uki_boot( USER_CFG }; - let grub_dir = - cap_std::fs::Dir::open_ambient_dir(boot_dir.join("grub2"), cap_std::ambient_authority()) - .context("opening boot/grub2")?; + let grub_dir = Dir::open_ambient_dir(boot_dir.join("grub2"), ambient_authority()) + .context("opening boot/grub2")?; // Iterate over all available deployments, and generate a menuentry for each // @@ -672,8 +657,8 @@ pub(crate) fn setup_composefs_uki_boot( )?; let mut str_buf = String::new(); - let boot_dir = cap_std::fs::Dir::open_ambient_dir(boot_dir, cap_std::ambient_authority()) - .context("Opening boot dir")?; + let boot_dir = + Dir::open_ambient_dir(boot_dir, ambient_authority()).context("Opening boot dir")?; let entries = get_sorted_uki_boot_entries(&boot_dir, &mut str_buf)?; // Write out only the currently booted entry, which should be the very first one @@ -721,6 +706,120 @@ pub(crate) fn setup_composefs_uki_boot( Ok(()) } +#[context("Setting up UKI boot")] +pub(crate) fn setup_composefs_uki_boot( + setup_type: BootSetupType, + // TODO: Make this generic + repo: ComposefsRepository, + id: &Sha256HashValue, + entries: Vec>, +) -> Result<()> { + let (root_path, esp_device, bootloader, is_insecure_from_opts) = match setup_type { + BootSetupType::Setup((root_setup, state, ..)) => { + if let Some(v) = &state.config_opts.karg { + if v.len() > 0 { + tracing::warn!("kargs passed for UKI will be ignored"); + } + } + + let esp_part = root_setup + .device_info + .partitions + .iter() + .find(|p| p.parttype.as_str() == ESP_GUID) + .ok_or_else(|| anyhow!("ESP partition not found"))?; + + let bootloader = state + .composefs_options + .as_ref() + .map(|opts| opts.bootloader.clone()) + .unwrap_or(Bootloader::default()); + + let is_insecure = state + .composefs_options + .as_ref() + .map(|x| x.insecure) + .unwrap_or(false); + + ( + root_setup.physical_root_path.clone(), + esp_part.node.clone(), + bootloader, + is_insecure, + ) + } + + BootSetupType::Upgrade((_, host)) => { + let sysroot = Utf8PathBuf::from("/sysroot"); + + let fsinfo = inspect_filesystem(&sysroot)?; + let parent_devices = find_parent_devices(&fsinfo.source)?; + + let Some(parent) = parent_devices.into_iter().next() else { + anyhow::bail!("Could not find parent device for mountpoint /sysroot"); + }; + + let bootloader = host.require_composefs_booted()?.bootloader.clone(); + + (sysroot, get_esp_partition(&parent)?.0, bootloader, false) + } + }; + + let temp_efi_dir = tempfile::tempdir() + .map_err(|e| anyhow::anyhow!("Failed to create temporary directory for EFI mount: {e}"))?; + let mounted_efi = temp_efi_dir.path().to_path_buf(); + + Task::new("Mounting ESP", "mount") + .args([&PathBuf::from(&esp_device), &mounted_efi.clone()]) + .run()?; + + let mut boot_label = String::new(); + + for entry in entries { + match entry { + ComposefsBootEntry::Type1(..) => tracing::debug!("Skipping Type1 Entry"), + ComposefsBootEntry::UsrLibModulesVmLinuz(..) => { + tracing::debug!("Skipping vmlinuz in /usr/lib/modules") + } + + ComposefsBootEntry::Type2(entry) => { + let ret = write_pe_to_esp( + &repo, + &entry.file, + &entry.file_path, + entry.pe_type, + &id.to_hex(), + is_insecure_from_opts, + &mounted_efi, + )?; + + if let Some(label) = ret { + boot_label = label; + } + } + }; + } + + Command::new("umount") + .arg(&mounted_efi) + .log_debug() + .run_inherited_with_cmd_context() + .context("Unmounting ESP")?; + + match bootloader { + Bootloader::Grub => { + write_grub_uki_menuentry(root_path, &setup_type, &boot_label, id, &esp_device)? + } + + Bootloader::Systemd => { + // No-op for now, but later we want to have .conf files so we can control the order of + // entries. + } + }; + + Ok(()) +} + #[context("Setting up composefs boot")] pub(crate) fn setup_composefs_boot( root_setup: &RootSetup, @@ -751,11 +850,11 @@ pub(crate) fn setup_composefs_boot( let entries = fs.transform_for_boot(&repo)?; let id = fs.commit_image(&repo, None)?; - let Some(entry) = entries.into_iter().next() else { + let Some(entry) = entries.iter().next() else { anyhow::bail!("No boot entries!"); }; - let boot_type = BootType::from(&entry); + let boot_type = BootType::from(entry); let mut boot_digest: Option = None; match boot_type { @@ -773,7 +872,7 @@ pub(crate) fn setup_composefs_boot( BootSetupType::Setup((&root_setup, &state, &fs)), repo, &id, - entry, + entries, )?, }; diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index f9f0f3e63..bebb95399 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -39,11 +39,11 @@ pub(crate) async fn switch_composefs(opts: SwitchOpts) -> Result<()> { let (repo, entries, id, fs) = pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?; - let Some(entry) = entries.into_iter().next() else { + let Some(entry) = entries.iter().next() else { anyhow::bail!("No boot entries!"); }; - let boot_type = BootType::from(&entry); + let boot_type = BootType::from(entry); let mut boot_digest = None; match boot_type { @@ -56,7 +56,7 @@ pub(crate) async fn switch_composefs(opts: SwitchOpts) -> Result<()> { )?) } BootType::Uki => { - setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entry)? + setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entries)? } }; diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 9b9bfa120..823a50bed 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -29,11 +29,11 @@ pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; - let Some(entry) = entries.into_iter().next() else { + let Some(entry) = entries.iter().next() else { anyhow::bail!("No boot entries!"); }; - let boot_type = BootType::from(&entry); + let boot_type = BootType::from(entry); let mut boot_digest = None; match boot_type { @@ -47,7 +47,7 @@ pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { } BootType::Uki => { - setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entry)? + setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entries)? } };