1use std::ffi::OsStr;
65use std::fs::create_dir_all;
66use std::io::Write;
67use std::path::Path;
68
69use anyhow::{anyhow, bail, Context, Result};
70use bootc_blockdev::find_parent_devices;
71use bootc_kernel_cmdline::utf8::{Cmdline, Parameter};
72use bootc_mount::inspect_filesystem_of_dir;
73use bootc_mount::tempmount::TempMount;
74use camino::{Utf8Path, Utf8PathBuf};
75use cap_std_ext::{
76 cap_std::{ambient_authority, fs::Dir},
77 dirext::CapStdExtDirExt,
78};
79use clap::ValueEnum;
80use composefs::fs::read_file;
81use composefs::tree::RegularFile;
82use composefs_boot::bootloader::{PEType, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT};
83use composefs_boot::BootOps;
84use fn_error_context::context;
85use ostree_ext::composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
86use ostree_ext::composefs_boot::bootloader::UsrLibModulesVmlinuz;
87use ostree_ext::composefs_boot::{
88 bootloader::BootEntry as ComposefsBootEntry, cmdline::get_cmdline_composefs,
89 os_release::OsReleaseInfo, uki,
90};
91use ostree_ext::composefs_oci::image::create_filesystem as create_composefs_filesystem;
92use rustix::{mount::MountFlags, path::Arg};
93use schemars::JsonSchema;
94use serde::{Deserialize, Serialize};
95
96use crate::parsers::bls_config::{BLSConfig, BLSConfigType};
97use crate::parsers::grub_menuconfig::MenuEntry;
98use crate::task::Task;
99use crate::{
100 bootc_composefs::repo::get_imgref,
101 composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED},
102};
103use crate::{
104 bootc_composefs::repo::open_composefs_repo,
105 store::{ComposefsFilesystem, Storage},
106};
107use crate::{
108 bootc_composefs::state::{get_booted_bls, write_composefs_state},
109 bootloader::esp_in,
110};
111use crate::{
112 bootc_composefs::status::get_container_manifest_and_config, bootc_kargs::compute_new_kargs,
113};
114use crate::{bootc_composefs::status::get_sorted_grub_uki_boot_entries, install::PostFetchState};
115use crate::{
116 composefs_consts::{
117 BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST,
118 STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED,
119 },
120 spec::{Bootloader, Host},
121};
122
123use crate::install::{RootSetup, State};
124
125pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
127pub(crate) const EFI_LINUX: &str = "EFI/Linux";
129
130const SYSTEMD_TIMEOUT: &str = "timeout 5";
132const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf";
133
134const INITRD: &str = "initrd";
135const VMLINUZ: &str = "vmlinuz";
136
137const BOOTC_AUTOENROLL_PATH: &str = "usr/lib/bootc/install/secureboot-keys";
138
139const AUTH_EXT: &str = "auth";
140
141pub(crate) const SYSTEMD_UKI_DIR: &str = "EFI/Linux/bootc";
146
147pub(crate) enum BootSetupType<'a> {
148 Setup(
150 (
151 &'a RootSetup,
152 &'a State,
153 &'a PostFetchState,
154 &'a ComposefsFilesystem,
155 ),
156 ),
157 Upgrade((&'a Storage, &'a ComposefsFilesystem, &'a Host)),
159}
160
161#[derive(
162 ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema,
163)]
164pub enum BootType {
165 #[default]
166 Bls,
167 Uki,
168}
169
170impl ::std::fmt::Display for BootType {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 let s = match self {
173 BootType::Bls => "bls",
174 BootType::Uki => "uki",
175 };
176
177 write!(f, "{}", s)
178 }
179}
180
181impl TryFrom<&str> for BootType {
182 type Error = anyhow::Error;
183
184 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
185 match value {
186 "bls" => Ok(Self::Bls),
187 "uki" => Ok(Self::Uki),
188 unrecognized => Err(anyhow::anyhow!(
189 "Unrecognized boot option: '{unrecognized}'"
190 )),
191 }
192 }
193}
194
195impl From<&ComposefsBootEntry<Sha512HashValue>> for BootType {
196 fn from(entry: &ComposefsBootEntry<Sha512HashValue>) -> Self {
197 match entry {
198 ComposefsBootEntry::Type1(..) => Self::Bls,
199 ComposefsBootEntry::Type2(..) => Self::Uki,
200 ComposefsBootEntry::UsrLibModulesVmLinuz(..) => Self::Bls,
201 }
202 }
203}
204
205pub(crate) fn get_efi_uuid_source() -> String {
208 format!(
209 r#"
210if [ -f ${{config_directory}}/{EFI_UUID_FILE} ]; then
211 source ${{config_directory}}/{EFI_UUID_FILE}
212fi
213"#
214 )
215}
216
217pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
218 let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?;
219 let esp = crate::bootloader::esp_in(&device_info)?;
220
221 Ok((esp.node.clone(), esp.uuid.clone()))
222}
223
224pub fn mount_esp(device: &str) -> Result<TempMount> {
226 let flags = MountFlags::NOEXEC | MountFlags::NOSUID;
227 TempMount::mount_dev(device, "vfat", flags, Some(c"fmask=0177,dmask=0077"))
228}
229
230pub fn get_sysroot_parent_dev(physical_root: &Dir) -> Result<String> {
231 let fsinfo = inspect_filesystem_of_dir(physical_root)?;
232 let parent_devices = find_parent_devices(&fsinfo.source)?;
233
234 let Some(parent) = parent_devices.into_iter().next() else {
235 anyhow::bail!("Could not find parent device of system root");
236 };
237
238 Ok(parent)
239}
240
241pub(crate) const FILENAME_PRIORITY_PRIMARY: &str = "1";
244
245pub(crate) const FILENAME_PRIORITY_SECONDARY: &str = "0";
247
248pub(crate) const SORTKEY_PRIORITY_PRIMARY: &str = "0";
251
252pub(crate) const SORTKEY_PRIORITY_SECONDARY: &str = "1";
254
255pub fn type1_entry_conf_file_name(
267 os_id: &str,
268 version: impl std::fmt::Display,
269 priority: &str,
270) -> String {
271 let os_id_safe = os_id.replace('-', "_");
272 format!("bootc_{os_id_safe}-{version}-{priority}.conf")
273}
274
275pub(crate) fn primary_sort_key(os_id: &str) -> String {
280 format!("bootc-{os_id}-{SORTKEY_PRIORITY_PRIMARY}")
281}
282
283pub(crate) fn secondary_sort_key(os_id: &str) -> String {
286 format!("bootc-{os_id}-{SORTKEY_PRIORITY_SECONDARY}")
287}
288
289#[context("Computing boot digest")]
295fn compute_boot_digest(
296 entry: &UsrLibModulesVmlinuz<Sha512HashValue>,
297 repo: &crate::store::ComposefsRepository,
298) -> Result<String> {
299 let vmlinuz = read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?;
300
301 let Some(initramfs) = &entry.initramfs else {
302 anyhow::bail!("initramfs not found");
303 };
304
305 let initramfs = read_file(initramfs, &repo).context("Reading intird")?;
306
307 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
308 .context("Creating hasher")?;
309
310 hasher.update(&vmlinuz).context("hashing vmlinuz")?;
311 hasher.update(&initramfs).context("hashing initrd")?;
312
313 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
314
315 Ok(hex::encode(digest))
316}
317
318#[context("Computing boot digest")]
324pub(crate) fn compute_boot_digest_uki(uki: &[u8]) -> Result<String> {
325 let vmlinuz = composefs_boot::uki::get_section(uki, ".linux")
326 .ok_or_else(|| anyhow::anyhow!(".linux not present"))??;
327
328 let initramfs = composefs_boot::uki::get_section(uki, ".initrd")
329 .ok_or_else(|| anyhow::anyhow!(".initrd not present"))??;
330
331 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
332 .context("Creating hasher")?;
333
334 hasher.update(&vmlinuz).context("hashing vmlinuz")?;
335 hasher.update(&initramfs).context("hashing initrd")?;
336
337 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
338
339 Ok(hex::encode(digest))
340}
341
342#[context("Checking boot entry duplicates")]
347pub(crate) fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<Vec<String>>> {
348 let deployments = Dir::open_ambient_dir(STATE_DIR_ABS, ambient_authority());
349
350 let deployments = match deployments {
351 Ok(d) => d,
352 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
354 Err(e) => anyhow::bail!(e),
355 };
356
357 let mut symlink_to: Option<Vec<String>> = None;
358
359 for depl in deployments.entries()? {
360 let depl = depl?;
361
362 let depl_file_name = depl.file_name();
363 let depl_file_name = depl_file_name.as_str()?;
364
365 let config = depl
366 .open_dir()
367 .with_context(|| format!("Opening {depl_file_name}"))?
368 .read_to_string(format!("{depl_file_name}.origin"))
369 .context("Reading origin file")?;
370
371 let ini = tini::Ini::from_string(&config)
372 .with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?;
373
374 match ini.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST) {
375 Some(hash) => {
376 if hash == digest {
377 match symlink_to {
378 Some(ref mut prev) => prev.push(depl_file_name.to_string()),
379 None => symlink_to = Some(vec![depl_file_name.to_string()]),
380 }
381 }
382 }
383
384 None => symlink_to = None,
387 };
388 }
389
390 Ok(symlink_to)
391}
392
393#[context("Writing BLS entries to disk")]
394fn write_bls_boot_entries_to_disk(
395 boot_dir: &Utf8PathBuf,
396 deployment_id: &Sha512HashValue,
397 entry: &UsrLibModulesVmlinuz<Sha512HashValue>,
398 repo: &crate::store::ComposefsRepository,
399) -> Result<()> {
400 let id_hex = deployment_id.to_hex();
401
402 let path = boot_dir.join(&id_hex);
404 create_dir_all(&path)?;
405
406 let entries_dir = Dir::open_ambient_dir(&path, ambient_authority())
407 .with_context(|| format!("Opening {path}"))?;
408
409 entries_dir
410 .atomic_write(
411 VMLINUZ,
412 read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?,
413 )
414 .context("Writing vmlinuz to path")?;
415
416 let Some(initramfs) = &entry.initramfs else {
417 anyhow::bail!("initramfs not found");
418 };
419
420 entries_dir
421 .atomic_write(
422 INITRD,
423 read_file(initramfs, &repo).context("Reading initrd")?,
424 )
425 .context("Writing initrd to path")?;
426
427 let owned_fd = entries_dir
429 .reopen_as_ownedfd()
430 .context("Reopen as owned fd")?;
431
432 rustix::fs::fsync(owned_fd).context("fsync")?;
433
434 Ok(())
435}
436
437fn parse_os_release(
439 fs: &crate::store::ComposefsFilesystem,
440 repo: &crate::store::ComposefsRepository,
441) -> Result<Option<(String, Option<String>, Option<String>)>> {
442 let (dir, fname) = fs
444 .root
445 .split(OsStr::new("/usr/lib/os-release"))
446 .context("Getting /usr/lib/os-release")?;
447
448 let os_release = dir
449 .get_file_opt(fname)
450 .context("Getting /usr/lib/os-release")?;
451
452 let Some(os_rel_file) = os_release else {
453 return Ok(None);
454 };
455
456 let file_contents = match read_file(os_rel_file, repo) {
457 Ok(c) => c,
458 Err(e) => {
459 tracing::warn!("Could not read /usr/lib/os-release: {e:?}");
460 return Ok(None);
461 }
462 };
463
464 let file_contents = match std::str::from_utf8(&file_contents) {
465 Ok(c) => c,
466 Err(e) => {
467 tracing::warn!("/usr/lib/os-release did not have valid UTF-8: {e}");
468 return Ok(None);
469 }
470 };
471
472 let parsed = OsReleaseInfo::parse(file_contents);
473
474 let os_id = parsed
475 .get_value(&["ID"])
476 .unwrap_or_else(|| "bootc".to_string());
477
478 Ok(Some((
479 os_id,
480 parsed.get_pretty_name(),
481 parsed.get_version(),
482 )))
483}
484
485struct BLSEntryPath {
486 entries_path: Utf8PathBuf,
488 abs_entries_path: Utf8PathBuf,
490 config_path: Utf8PathBuf,
492}
493
494#[context("Setting up BLS boot")]
499pub(crate) fn setup_composefs_bls_boot(
500 setup_type: BootSetupType,
501 repo: crate::store::ComposefsRepository,
502 id: &Sha512HashValue,
503 entry: &ComposefsBootEntry<Sha512HashValue>,
504 mounted_erofs: &Dir,
505) -> Result<String> {
506 let id_hex = id.to_hex();
507
508 let (root_path, esp_device, mut cmdline_refs, fs, bootloader) = match setup_type {
509 BootSetupType::Setup((root_setup, state, postfetch, fs)) => {
510 let mut cmdline_options = Cmdline::new();
512
513 cmdline_options.extend(&root_setup.kargs);
514
515 let composefs_cmdline = if state.composefs_options.insecure {
516 format!("{COMPOSEFS_CMDLINE}=?{id_hex}")
517 } else {
518 format!("{COMPOSEFS_CMDLINE}={id_hex}")
519 };
520
521 cmdline_options.extend(&Cmdline::from(&composefs_cmdline));
522
523 let esp_part = esp_in(&root_setup.device_info)?;
525
526 (
527 root_setup.physical_root_path.clone(),
528 esp_part.node.clone(),
529 cmdline_options,
530 fs,
531 postfetch.detected_bootloader.clone(),
532 )
533 }
534
535 BootSetupType::Upgrade((storage, fs, host)) => {
536 let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?;
537 let bootloader = host.require_composefs_booted()?.bootloader.clone();
538
539 let boot_dir = storage.require_boot_dir()?;
540 let current_cfg = get_booted_bls(&boot_dir)?;
541
542 let mut cmdline = match current_cfg.cfg_type {
543 BLSConfigType::NonEFI { options, .. } => {
544 let options = options
545 .ok_or_else(|| anyhow::anyhow!("No 'options' found in BLS Config"))?;
546
547 Cmdline::from(options)
548 }
549
550 _ => anyhow::bail!("Found NonEFI config"),
551 };
552
553 let param = format!("{COMPOSEFS_CMDLINE}={id_hex}");
555 let param =
556 Parameter::parse(¶m).context("Failed to create 'composefs=' parameter")?;
557 cmdline.add_or_modify(¶m);
558
559 (
560 Utf8PathBuf::from("/sysroot"),
561 get_esp_partition(&sysroot_parent)?.0,
562 cmdline,
563 fs,
564 bootloader,
565 )
566 }
567 };
568
569 let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
570
571 let current_root = if is_upgrade {
572 Some(&Dir::open_ambient_dir("/", ambient_authority()).context("Opening root")?)
573 } else {
574 None
575 };
576
577 compute_new_kargs(mounted_erofs, current_root, &mut cmdline_refs)?;
578
579 let (entry_paths, _tmpdir_guard) = match bootloader {
580 Bootloader::Grub => {
581 let root = Dir::open_ambient_dir(&root_path, ambient_authority())
582 .context("Opening root path")?;
583
584 let entries_path = match root.is_mountpoint("boot")? {
589 Some(true) => "/",
590 Some(false) | None => "/boot",
592 };
593
594 (
595 BLSEntryPath {
596 entries_path: root_path.join("boot"),
597 config_path: root_path.join("boot"),
598 abs_entries_path: entries_path.into(),
599 },
600 None,
601 )
602 }
603
604 Bootloader::Systemd => {
605 let efi_mount = mount_esp(&esp_device).context("Mounting ESP")?;
606
607 let mounted_efi = Utf8PathBuf::from(efi_mount.dir.path().as_str()?);
608 let efi_linux_dir = mounted_efi.join(EFI_LINUX);
609
610 (
611 BLSEntryPath {
612 entries_path: efi_linux_dir,
613 config_path: mounted_efi.clone(),
614 abs_entries_path: Utf8PathBuf::from("/").join(EFI_LINUX),
615 },
616 Some(efi_mount),
617 )
618 }
619 };
620
621 let (bls_config, boot_digest, os_id) = match &entry {
622 ComposefsBootEntry::Type1(..) => anyhow::bail!("Found Type1 entries in /boot"),
623 ComposefsBootEntry::Type2(..) => anyhow::bail!("Found UKI"),
624
625 ComposefsBootEntry::UsrLibModulesVmLinuz(usr_lib_modules_vmlinuz) => {
626 let boot_digest = compute_boot_digest(usr_lib_modules_vmlinuz, &repo)
627 .context("Computing boot digest")?;
628
629 let osrel = parse_os_release(fs, &repo)?;
630
631 let (os_id, title, version, sort_key) = match osrel {
632 Some((id_str, title_opt, version_opt)) => (
633 id_str.clone(),
634 title_opt.unwrap_or_else(|| id.to_hex()),
635 version_opt.unwrap_or_else(|| id.to_hex()),
636 primary_sort_key(&id_str),
637 ),
638 None => {
639 let default_id = "bootc".to_string();
640 (
641 default_id.clone(),
642 id.to_hex(),
643 id.to_hex(),
644 primary_sort_key(&default_id),
645 )
646 }
647 };
648
649 let mut bls_config = BLSConfig::default();
650
651 bls_config
652 .with_title(title)
653 .with_version(version)
654 .with_sort_key(sort_key)
655 .with_cfg(BLSConfigType::NonEFI {
656 linux: entry_paths.abs_entries_path.join(&id_hex).join(VMLINUZ),
657 initrd: vec![entry_paths.abs_entries_path.join(&id_hex).join(INITRD)],
658 options: Some(cmdline_refs),
659 });
660
661 match find_vmlinuz_initrd_duplicates(&boot_digest)? {
662 Some(shared_entries) => {
663 let mut shared_entry: Option<String> = None;
670
671 let entries =
672 Dir::open_ambient_dir(entry_paths.entries_path, ambient_authority())
673 .context("Opening entries path")?
674 .entries_utf8()
675 .context("Getting dir entries")?;
676
677 for ent in entries {
678 let ent = ent?;
679 let ent_name = ent.file_name()?;
681
682 if shared_entries.contains(&ent_name) {
683 shared_entry = Some(ent_name);
684 break;
685 }
686 }
687
688 let shared_entry = shared_entry
689 .ok_or_else(|| anyhow::anyhow!("Shared boot binaries not found"))?;
690
691 match bls_config.cfg_type {
692 BLSConfigType::NonEFI {
693 ref mut linux,
694 ref mut initrd,
695 ..
696 } => {
697 *linux = entry_paths
698 .abs_entries_path
699 .join(&shared_entry)
700 .join(VMLINUZ);
701
702 *initrd = vec![entry_paths
703 .abs_entries_path
704 .join(&shared_entry)
705 .join(INITRD)];
706 }
707
708 _ => unreachable!(),
709 };
710 }
711
712 None => {
713 write_bls_boot_entries_to_disk(
714 &entry_paths.entries_path,
715 id,
716 usr_lib_modules_vmlinuz,
717 &repo,
718 )?;
719 }
720 };
721
722 (bls_config, boot_digest, os_id)
723 }
724 };
725
726 let loader_path = entry_paths.config_path.join("loader");
727
728 let (config_path, booted_bls) = if is_upgrade {
729 let boot_dir = Dir::open_ambient_dir(&entry_paths.config_path, ambient_authority())?;
730
731 let mut booted_bls = get_booted_bls(&boot_dir)?;
732 booted_bls.sort_key = Some(secondary_sort_key(&os_id));
733
734 let staged_path = loader_path.join(STAGED_BOOT_LOADER_ENTRIES);
735
736 if boot_dir
739 .remove_all_optional(TYPE1_ENT_PATH_STAGED)
740 .context("Failed to remove staged directory")?
741 {
742 tracing::debug!("Removed existing staged entries directory");
743 }
744
745 (staged_path, Some(booted_bls))
747 } else {
748 (loader_path.join(BOOT_LOADER_ENTRIES), None)
749 };
750
751 create_dir_all(&config_path).with_context(|| format!("Creating {:?}", config_path))?;
752
753 let loader_entries_dir = Dir::open_ambient_dir(&config_path, ambient_authority())
754 .with_context(|| format!("Opening {config_path:?}"))?;
755
756 loader_entries_dir.atomic_write(
757 type1_entry_conf_file_name(&os_id, &bls_config.version(), FILENAME_PRIORITY_PRIMARY),
758 bls_config.to_string().as_bytes(),
759 )?;
760
761 if let Some(booted_bls) = booted_bls {
762 loader_entries_dir.atomic_write(
763 type1_entry_conf_file_name(&os_id, &booted_bls.version(), FILENAME_PRIORITY_SECONDARY),
764 booted_bls.to_string().as_bytes(),
765 )?;
766 }
767
768 let owned_loader_entries_fd = loader_entries_dir
769 .reopen_as_ownedfd()
770 .context("Reopening as owned fd")?;
771
772 rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?;
773
774 Ok(boot_digest)
775}
776
777struct UKIInfo {
778 boot_label: String,
779 version: Option<String>,
780 os_id: Option<String>,
781 boot_digest: String,
782}
783
784#[context("Writing {file_path} to ESP")]
786fn write_pe_to_esp(
787 repo: &crate::store::ComposefsRepository,
788 file: &RegularFile<Sha512HashValue>,
789 file_path: &Utf8Path,
790 pe_type: PEType,
791 uki_id: &Sha512HashValue,
792 is_insecure_from_opts: bool,
793 mounted_efi: impl AsRef<Path>,
794 bootloader: &Bootloader,
795) -> Result<Option<UKIInfo>> {
796 let efi_bin = read_file(file, &repo).context("Reading .efi binary")?;
797
798 let mut boot_label: Option<UKIInfo> = None;
799
800 if matches!(pe_type, PEType::Uki) {
803 let cmdline = uki::get_cmdline(&efi_bin).context("Getting UKI cmdline")?;
804
805 let (composefs_cmdline, insecure) =
806 get_cmdline_composefs::<Sha512HashValue>(cmdline).context("Parsing composefs=")?;
807
808 match is_insecure_from_opts {
811 true if !insecure => {
812 tracing::warn!("--insecure passed as option but UKI cmdline does not support it");
813 }
814
815 false if insecure => {
816 tracing::warn!("UKI cmdline has composefs set as insecure");
817 }
818
819 _ => { }
820 }
821
822 if composefs_cmdline != *uki_id {
823 anyhow::bail!(
824 "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})"
825 );
826 }
827
828 let osrel = uki::get_text_section(&efi_bin, ".osrel")?;
829
830 let parsed_osrel = OsReleaseInfo::parse(osrel);
831
832 let boot_digest = compute_boot_digest_uki(&efi_bin)?;
833
834 boot_label = Some(UKIInfo {
835 boot_label: uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?,
836 version: parsed_osrel.get_version(),
837 os_id: parsed_osrel.get_value(&["ID"]),
838 boot_digest,
839 });
840 }
841
842 let efi_linux_path = mounted_efi.as_ref().join(match bootloader {
844 Bootloader::Grub => EFI_LINUX,
845 Bootloader::Systemd => SYSTEMD_UKI_DIR,
846 });
847
848 create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?;
849
850 let final_pe_path = match file_path.parent() {
851 Some(parent) => {
852 let renamed_path = match parent.as_str().ends_with(EFI_ADDON_DIR_EXT) {
853 true => {
854 let dir_name = format!("{}{}", uki_id.to_hex(), EFI_ADDON_DIR_EXT);
855
856 parent
857 .parent()
858 .map(|p| p.join(&dir_name))
859 .unwrap_or(dir_name.into())
860 }
861
862 false => parent.to_path_buf(),
863 };
864
865 let full_path = efi_linux_path.join(renamed_path);
866 create_dir_all(&full_path)?;
867
868 full_path
869 }
870
871 None => efi_linux_path,
872 };
873
874 let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority())
875 .with_context(|| format!("Opening {final_pe_path:?}"))?;
876
877 let pe_name = match pe_type {
878 PEType::Uki => &format!("{}{}", uki_id.to_hex(), EFI_EXT),
879 PEType::UkiAddon => file_path
880 .components()
881 .last()
882 .ok_or_else(|| anyhow::anyhow!("Failed to get UKI Addon file name"))?
883 .as_str(),
884 };
885
886 pe_dir
887 .atomic_write(pe_name, efi_bin)
888 .context("Writing UKI")?;
889
890 rustix::fs::fsync(
891 pe_dir
892 .reopen_as_ownedfd()
893 .context("Reopening as owned fd")?,
894 )
895 .context("fsync")?;
896
897 Ok(boot_label)
898}
899
900#[context("Writing Grub menuentry")]
901fn write_grub_uki_menuentry(
902 root_path: Utf8PathBuf,
903 setup_type: &BootSetupType,
904 boot_label: String,
905 id: &Sha512HashValue,
906 esp_device: &String,
907) -> Result<()> {
908 let boot_dir = root_path.join("boot");
909 create_dir_all(&boot_dir).context("Failed to create boot dir")?;
910
911 let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
912
913 let efi_uuid_source = get_efi_uuid_source();
914
915 let user_cfg_name = if is_upgrade {
916 USER_CFG_STAGED
917 } else {
918 USER_CFG
919 };
920
921 let grub_dir = Dir::open_ambient_dir(boot_dir.join("grub2"), ambient_authority())
922 .context("opening boot/grub2")?;
923
924 if is_upgrade {
926 let mut str_buf = String::new();
927 let boot_dir =
928 Dir::open_ambient_dir(boot_dir, ambient_authority()).context("Opening boot dir")?;
929 let entries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut str_buf)?;
930
931 grub_dir
932 .atomic_replace_with(user_cfg_name, |f| -> std::io::Result<_> {
933 f.write_all(efi_uuid_source.as_bytes())?;
934 f.write_all(
935 MenuEntry::new(&boot_label, &id.to_hex())
936 .to_string()
937 .as_bytes(),
938 )?;
939
940 f.write_all(entries[0].to_string().as_bytes())?;
944
945 Ok(())
946 })
947 .with_context(|| format!("Writing to {user_cfg_name}"))?;
948
949 rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
950
951 return Ok(());
952 }
953
954 let esp_uuid = Task::new("blkid for ESP UUID", "blkid")
957 .args(["-s", "UUID", "-o", "value", &esp_device])
958 .read()?;
959
960 grub_dir.atomic_write(
961 EFI_UUID_FILE,
962 format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes(),
963 )?;
964
965 grub_dir
967 .atomic_replace_with(user_cfg_name, |f| -> std::io::Result<_> {
968 f.write_all(efi_uuid_source.as_bytes())?;
969 f.write_all(
970 MenuEntry::new(&boot_label, &id.to_hex())
971 .to_string()
972 .as_bytes(),
973 )?;
974
975 Ok(())
976 })
977 .with_context(|| format!("Writing to {user_cfg_name}"))?;
978
979 rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
980
981 Ok(())
982}
983
984#[context("Writing systemd UKI config")]
985fn write_systemd_uki_config(
986 esp_dir: &Dir,
987 setup_type: &BootSetupType,
988 boot_label: UKIInfo,
989 id: &Sha512HashValue,
990) -> Result<()> {
991 let os_id = boot_label.os_id.as_deref().unwrap_or("bootc");
992 let primary_sort_key = primary_sort_key(os_id);
993
994 let mut bls_conf = BLSConfig::default();
995 bls_conf
996 .with_title(boot_label.boot_label)
997 .with_cfg(BLSConfigType::EFI {
998 efi: format!("/{SYSTEMD_UKI_DIR}/{}{}", id.to_hex(), EFI_EXT).into(),
999 })
1000 .with_sort_key(primary_sort_key.clone())
1001 .with_version(boot_label.version.unwrap_or_else(|| id.to_hex()));
1002
1003 let (entries_dir, booted_bls) = match setup_type {
1004 BootSetupType::Setup(..) => {
1005 esp_dir
1006 .create_dir_all(TYPE1_ENT_PATH)
1007 .with_context(|| format!("Creating {TYPE1_ENT_PATH}"))?;
1008
1009 (esp_dir.open_dir(TYPE1_ENT_PATH)?, None)
1010 }
1011
1012 BootSetupType::Upgrade(_) => {
1013 esp_dir
1014 .create_dir_all(TYPE1_ENT_PATH_STAGED)
1015 .with_context(|| format!("Creating {TYPE1_ENT_PATH_STAGED}"))?;
1016
1017 let mut booted_bls = get_booted_bls(&esp_dir)?;
1018 booted_bls.sort_key = Some(secondary_sort_key(os_id));
1019
1020 (esp_dir.open_dir(TYPE1_ENT_PATH_STAGED)?, Some(booted_bls))
1021 }
1022 };
1023
1024 entries_dir
1025 .atomic_write(
1026 type1_entry_conf_file_name(os_id, &bls_conf.version(), FILENAME_PRIORITY_PRIMARY),
1027 bls_conf.to_string().as_bytes(),
1028 )
1029 .context("Writing conf file")?;
1030
1031 if let Some(booted_bls) = booted_bls {
1032 entries_dir.atomic_write(
1033 type1_entry_conf_file_name(os_id, &booted_bls.version(), FILENAME_PRIORITY_SECONDARY),
1034 booted_bls.to_string().as_bytes(),
1035 )?;
1036 }
1037
1038 if !esp_dir.exists(SYSTEMD_LOADER_CONF_PATH) {
1040 esp_dir
1041 .atomic_write(SYSTEMD_LOADER_CONF_PATH, SYSTEMD_TIMEOUT)
1042 .with_context(|| format!("Writing to {SYSTEMD_LOADER_CONF_PATH}"))?;
1043 }
1044
1045 let esp_dir = esp_dir
1046 .reopen_as_ownedfd()
1047 .context("Reopening as owned fd")?;
1048 rustix::fs::fsync(esp_dir).context("fsync")?;
1049
1050 Ok(())
1051}
1052
1053#[context("Setting up UKI boot")]
1054pub(crate) fn setup_composefs_uki_boot(
1055 setup_type: BootSetupType,
1056 repo: crate::store::ComposefsRepository,
1057 id: &Sha512HashValue,
1058 entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
1059) -> Result<String> {
1060 let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type {
1061 BootSetupType::Setup((root_setup, state, postfetch, ..)) => {
1062 state.require_no_kargs_for_uki()?;
1063
1064 let esp_part = esp_in(&root_setup.device_info)?;
1065
1066 (
1067 root_setup.physical_root_path.clone(),
1068 esp_part.node.clone(),
1069 postfetch.detected_bootloader.clone(),
1070 state.composefs_options.insecure,
1071 state.composefs_options.uki_addon.as_ref(),
1072 )
1073 }
1074
1075 BootSetupType::Upgrade((storage, _, host)) => {
1076 let sysroot = Utf8PathBuf::from("/sysroot"); let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?;
1078 let bootloader = host.require_composefs_booted()?.bootloader.clone();
1079
1080 (
1081 sysroot,
1082 get_esp_partition(&sysroot_parent)?.0,
1083 bootloader,
1084 false,
1085 None,
1086 )
1087 }
1088 };
1089
1090 let esp_mount = mount_esp(&esp_device).context("Mounting ESP")?;
1091
1092 let mut uki_info: Option<UKIInfo> = None;
1093
1094 for entry in entries {
1095 match entry {
1096 ComposefsBootEntry::Type1(..) => tracing::debug!("Skipping Type1 Entry"),
1097 ComposefsBootEntry::UsrLibModulesVmLinuz(..) => {
1098 tracing::debug!("Skipping vmlinuz in /usr/lib/modules")
1099 }
1100
1101 ComposefsBootEntry::Type2(entry) => {
1102 if matches!(entry.pe_type, PEType::UkiAddon) {
1104 let Some(addons) = uki_addons else {
1105 continue;
1106 };
1107
1108 let addon_name = entry
1109 .file_path
1110 .components()
1111 .last()
1112 .ok_or_else(|| anyhow::anyhow!("Could not get UKI addon name"))?;
1113
1114 let addon_name = addon_name.as_str()?;
1115
1116 let addon_name =
1117 addon_name.strip_suffix(EFI_ADDON_FILE_EXT).ok_or_else(|| {
1118 anyhow::anyhow!("UKI addon doesn't end with {EFI_ADDON_DIR_EXT}")
1119 })?;
1120
1121 if !addons.iter().any(|passed_addon| passed_addon == addon_name) {
1122 continue;
1123 }
1124 }
1125
1126 let utf8_file_path = Utf8Path::from_path(&entry.file_path)
1127 .ok_or_else(|| anyhow::anyhow!("Path is not valid UTf8"))?;
1128
1129 let ret = write_pe_to_esp(
1130 &repo,
1131 &entry.file,
1132 utf8_file_path,
1133 entry.pe_type,
1134 &id,
1135 is_insecure_from_opts,
1136 esp_mount.dir.path(),
1137 &bootloader,
1138 )?;
1139
1140 if let Some(label) = ret {
1141 uki_info = Some(label);
1142 }
1143 }
1144 };
1145 }
1146
1147 let uki_info =
1148 uki_info.ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?;
1149
1150 let boot_digest = uki_info.boot_digest.clone();
1151
1152 match bootloader {
1153 Bootloader::Grub => {
1154 write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
1155 }
1156
1157 Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?,
1158 };
1159
1160 Ok(boot_digest)
1161}
1162
1163pub struct SecurebootKeys {
1164 pub dir: Dir,
1165 pub keys: Vec<Utf8PathBuf>,
1166}
1167
1168fn get_secureboot_keys(fs: &Dir, p: &str) -> Result<Option<SecurebootKeys>> {
1169 let mut entries = vec![];
1170
1171 let keys_dir = match fs.open_dir_optional(p)? {
1173 Some(d) => d,
1174 _ => return Ok(None),
1175 };
1176
1177 for entry in keys_dir.entries()? {
1180 let dir_e = entry?;
1181 let dirname = dir_e.file_name();
1182 if !dir_e.file_type()?.is_dir() {
1183 bail!("/{p}/{dirname:?} is not a directory");
1184 }
1185
1186 let dir_path: Utf8PathBuf = dirname.try_into()?;
1187 let dir = dir_e.open_dir()?;
1188 for entry in dir.entries()? {
1189 let e = entry?;
1190 let local: Utf8PathBuf = e.file_name().try_into()?;
1191 let path = dir_path.join(local);
1192
1193 if path.extension() != Some(AUTH_EXT) {
1194 continue;
1195 }
1196
1197 if !e.file_type()?.is_file() {
1198 bail!("/{p}/{path:?} is not a file");
1199 }
1200 entries.push(path);
1201 }
1202 }
1203 return Ok(Some(SecurebootKeys {
1204 dir: keys_dir,
1205 keys: entries,
1206 }));
1207}
1208
1209#[context("Setting up composefs boot")]
1210pub(crate) async fn setup_composefs_boot(
1211 root_setup: &RootSetup,
1212 state: &State,
1213 image_id: &str,
1214) -> Result<()> {
1215 let repo = open_composefs_repo(&root_setup.physical_root)?;
1216 let mut fs = create_composefs_filesystem(&repo, image_id, None)?;
1217 let entries = fs.transform_for_boot(&repo)?;
1218 let id = fs.commit_image(&repo, None)?;
1219 let mounted_fs = Dir::reopen_dir(
1220 &repo
1221 .mount(&id.to_hex())
1222 .context("Failed to mount composefs image")?,
1223 )?;
1224
1225 let postfetch = PostFetchState::new(state, &mounted_fs)?;
1226
1227 let boot_uuid = root_setup
1228 .get_boot_uuid()?
1229 .or(root_setup.rootfs_uuid.as_deref())
1230 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1231
1232 if cfg!(target_arch = "s390x") {
1233 crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?;
1235 } else if postfetch.detected_bootloader == Bootloader::Grub {
1236 crate::bootloader::install_via_bootupd(
1237 &root_setup.device_info,
1238 &root_setup.physical_root_path,
1239 &state.config_opts,
1240 None,
1241 )?;
1242 } else {
1243 crate::bootloader::install_systemd_boot(
1244 &root_setup.device_info,
1245 &root_setup.physical_root_path,
1246 &state.config_opts,
1247 None,
1248 get_secureboot_keys(&mounted_fs, BOOTC_AUTOENROLL_PATH)?,
1249 )?;
1250 }
1251
1252 let Some(entry) = entries.iter().next() else {
1253 anyhow::bail!("No boot entries!");
1254 };
1255
1256 let boot_type = BootType::from(entry);
1257
1258 let boot_digest = match boot_type {
1259 BootType::Bls => setup_composefs_bls_boot(
1260 BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)),
1261 repo,
1262 &id,
1263 entry,
1264 &mounted_fs,
1265 )?,
1266 BootType::Uki => setup_composefs_uki_boot(
1267 BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)),
1268 repo,
1269 &id,
1270 entries,
1271 )?,
1272 };
1273
1274 write_composefs_state(
1275 &root_setup.physical_root_path,
1276 &id,
1277 &crate::spec::ImageReference::from(state.target_imgref.clone()),
1278 false,
1279 boot_type,
1280 boot_digest,
1281 &get_container_manifest_and_config(&get_imgref(
1282 &state.source.imageref.transport.to_string(),
1283 &state.source.imageref.name,
1284 ))
1285 .await?,
1286 )
1287 .await?;
1288
1289 Ok(())
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 use super::*;
1295
1296 #[test]
1297 fn test_type1_filename_generation() {
1298 let filename =
1300 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1301 assert_eq!(filename, "bootc_fedora-41.20251125.0-1.conf");
1302
1303 let primary =
1305 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1306 let secondary =
1307 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1308 assert_eq!(primary, "bootc_fedora-41.20251125.0-1.conf");
1309 assert_eq!(secondary, "bootc_fedora-41.20251125.0-0.conf");
1310
1311 let filename =
1313 type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1314 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1315
1316 let filename =
1318 type1_entry_conf_file_name("my-custom-os", "1.0.0", FILENAME_PRIORITY_PRIMARY);
1319 assert_eq!(filename, "bootc_my_custom_os-1.0.0-1.conf");
1320
1321 let filename = type1_entry_conf_file_name("rhel", "9.3.0", FILENAME_PRIORITY_SECONDARY);
1323 assert_eq!(filename, "bootc_rhel-9.3.0-0.conf");
1324 }
1325
1326 #[test]
1327 fn test_grub_filename_parsing() {
1328 let filename = type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", "1");
1337 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1338
1339 let without_ext = filename.strip_suffix(".conf").unwrap();
1345 let parts: Vec<&str> = without_ext.rsplitn(3, '-').collect();
1346 assert_eq!(parts.len(), 3);
1347 assert_eq!(parts[0], "1"); assert_eq!(parts[1], "41.20251125.0"); assert_eq!(parts[2], "bootc_fedora_coreos"); }
1351
1352 #[test]
1353 fn test_sort_keys() {
1354 let primary = primary_sort_key("fedora");
1356 let secondary = secondary_sort_key("fedora");
1357
1358 assert_eq!(primary, "bootc-fedora-0");
1359 assert_eq!(secondary, "bootc-fedora-1");
1360
1361 assert!(primary < secondary);
1363
1364 let primary_coreos = primary_sort_key("fedora-coreos");
1366 assert_eq!(primary_coreos, "bootc-fedora-coreos-0");
1367 }
1368
1369 #[test]
1370 fn test_filename_sorting_grub_style() {
1371 let primary =
1375 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1376 let secondary =
1377 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1378
1379 assert!(
1381 primary > secondary,
1382 "Primary should sort before secondary in descending order"
1383 );
1384
1385 let newer =
1387 type1_entry_conf_file_name("fedora", "42.20251125.0", FILENAME_PRIORITY_PRIMARY);
1388 let older =
1389 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1390
1391 assert!(
1393 newer > older,
1394 "Newer version should sort before older in descending order"
1395 );
1396
1397 let fedora = type1_entry_conf_file_name("fedora", "41.0", FILENAME_PRIORITY_PRIMARY);
1399 let rhel = type1_entry_conf_file_name("rhel", "9.0", FILENAME_PRIORITY_PRIMARY);
1400
1401 assert!(
1403 rhel > fedora,
1404 "RHEL should sort before Fedora in descending order"
1405 );
1406 }
1407}