1use std::ffi::OsStr;
65use std::fs::create_dir_all;
66use std::io::Write;
67use std::path::Path;
68
69use anyhow::{Context, Result, anyhow, bail};
70use bootc_blockdev::find_parent_devices;
71use bootc_kernel_cmdline::utf8::{Cmdline, Parameter, ParameterKey};
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::BootOps;
83use composefs_boot::bootloader::{EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT, PEType};
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 if bootloader == Bootloader::Systemd {
572 cmdline_refs.remove(&ParameterKey::from("root"));
573 }
574
575 let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
576
577 let current_root = if is_upgrade {
578 Some(&Dir::open_ambient_dir("/", ambient_authority()).context("Opening root")?)
579 } else {
580 None
581 };
582
583 compute_new_kargs(mounted_erofs, current_root, &mut cmdline_refs)?;
584
585 let (entry_paths, _tmpdir_guard) = match bootloader {
586 Bootloader::Grub => {
587 let root = Dir::open_ambient_dir(&root_path, ambient_authority())
588 .context("Opening root path")?;
589
590 let entries_path = match root.is_mountpoint("boot")? {
595 Some(true) => "/",
596 Some(false) | None => "/boot",
598 };
599
600 (
601 BLSEntryPath {
602 entries_path: root_path.join("boot"),
603 config_path: root_path.join("boot"),
604 abs_entries_path: entries_path.into(),
605 },
606 None,
607 )
608 }
609
610 Bootloader::Systemd => {
611 let efi_mount = mount_esp(&esp_device).context("Mounting ESP")?;
612
613 let mounted_efi = Utf8PathBuf::from(efi_mount.dir.path().as_str()?);
614 let efi_linux_dir = mounted_efi.join(EFI_LINUX);
615
616 (
617 BLSEntryPath {
618 entries_path: efi_linux_dir,
619 config_path: mounted_efi.clone(),
620 abs_entries_path: Utf8PathBuf::from("/").join(EFI_LINUX),
621 },
622 Some(efi_mount),
623 )
624 }
625 };
626
627 let (bls_config, boot_digest, os_id) = match &entry {
628 ComposefsBootEntry::Type1(..) => anyhow::bail!("Found Type1 entries in /boot"),
629 ComposefsBootEntry::Type2(..) => anyhow::bail!("Found UKI"),
630
631 ComposefsBootEntry::UsrLibModulesVmLinuz(usr_lib_modules_vmlinuz) => {
632 let boot_digest = compute_boot_digest(usr_lib_modules_vmlinuz, &repo)
633 .context("Computing boot digest")?;
634
635 let osrel = parse_os_release(fs, &repo)?;
636
637 let (os_id, title, version, sort_key) = match osrel {
638 Some((id_str, title_opt, version_opt)) => (
639 id_str.clone(),
640 title_opt.unwrap_or_else(|| id.to_hex()),
641 version_opt.unwrap_or_else(|| id.to_hex()),
642 primary_sort_key(&id_str),
643 ),
644 None => {
645 let default_id = "bootc".to_string();
646 (
647 default_id.clone(),
648 id.to_hex(),
649 id.to_hex(),
650 primary_sort_key(&default_id),
651 )
652 }
653 };
654
655 let mut bls_config = BLSConfig::default();
656
657 bls_config
658 .with_title(title)
659 .with_version(version)
660 .with_sort_key(sort_key)
661 .with_cfg(BLSConfigType::NonEFI {
662 linux: entry_paths.abs_entries_path.join(&id_hex).join(VMLINUZ),
663 initrd: vec![entry_paths.abs_entries_path.join(&id_hex).join(INITRD)],
664 options: Some(cmdline_refs),
665 });
666
667 match find_vmlinuz_initrd_duplicates(&boot_digest)? {
668 Some(shared_entries) => {
669 let mut shared_entry: Option<String> = None;
676
677 let entries =
678 Dir::open_ambient_dir(entry_paths.entries_path, ambient_authority())
679 .context("Opening entries path")?
680 .entries_utf8()
681 .context("Getting dir entries")?;
682
683 for ent in entries {
684 let ent = ent?;
685 let ent_name = ent.file_name()?;
687
688 if shared_entries.contains(&ent_name) {
689 shared_entry = Some(ent_name);
690 break;
691 }
692 }
693
694 let shared_entry = shared_entry
695 .ok_or_else(|| anyhow::anyhow!("Shared boot binaries not found"))?;
696
697 match bls_config.cfg_type {
698 BLSConfigType::NonEFI {
699 ref mut linux,
700 ref mut initrd,
701 ..
702 } => {
703 *linux = entry_paths
704 .abs_entries_path
705 .join(&shared_entry)
706 .join(VMLINUZ);
707
708 *initrd = vec![
709 entry_paths
710 .abs_entries_path
711 .join(&shared_entry)
712 .join(INITRD),
713 ];
714 }
715
716 _ => unreachable!(),
717 };
718 }
719
720 None => {
721 write_bls_boot_entries_to_disk(
722 &entry_paths.entries_path,
723 id,
724 usr_lib_modules_vmlinuz,
725 &repo,
726 )?;
727 }
728 };
729
730 (bls_config, boot_digest, os_id)
731 }
732 };
733
734 let loader_path = entry_paths.config_path.join("loader");
735
736 let (config_path, booted_bls) = if is_upgrade {
737 let boot_dir = Dir::open_ambient_dir(&entry_paths.config_path, ambient_authority())?;
738
739 let mut booted_bls = get_booted_bls(&boot_dir)?;
740 booted_bls.sort_key = Some(secondary_sort_key(&os_id));
741
742 let staged_path = loader_path.join(STAGED_BOOT_LOADER_ENTRIES);
743
744 if boot_dir
747 .remove_all_optional(TYPE1_ENT_PATH_STAGED)
748 .context("Failed to remove staged directory")?
749 {
750 tracing::debug!("Removed existing staged entries directory");
751 }
752
753 (staged_path, Some(booted_bls))
755 } else {
756 (loader_path.join(BOOT_LOADER_ENTRIES), None)
757 };
758
759 create_dir_all(&config_path).with_context(|| format!("Creating {:?}", config_path))?;
760
761 let loader_entries_dir = Dir::open_ambient_dir(&config_path, ambient_authority())
762 .with_context(|| format!("Opening {config_path:?}"))?;
763
764 loader_entries_dir.atomic_write(
765 type1_entry_conf_file_name(&os_id, &bls_config.version(), FILENAME_PRIORITY_PRIMARY),
766 bls_config.to_string().as_bytes(),
767 )?;
768
769 if let Some(booted_bls) = booted_bls {
770 loader_entries_dir.atomic_write(
771 type1_entry_conf_file_name(&os_id, &booted_bls.version(), FILENAME_PRIORITY_SECONDARY),
772 booted_bls.to_string().as_bytes(),
773 )?;
774 }
775
776 let owned_loader_entries_fd = loader_entries_dir
777 .reopen_as_ownedfd()
778 .context("Reopening as owned fd")?;
779
780 rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?;
781
782 Ok(boot_digest)
783}
784
785struct UKIInfo {
786 boot_label: String,
787 version: Option<String>,
788 os_id: Option<String>,
789 boot_digest: String,
790}
791
792#[context("Writing {file_path} to ESP")]
794fn write_pe_to_esp(
795 repo: &crate::store::ComposefsRepository,
796 file: &RegularFile<Sha512HashValue>,
797 file_path: &Utf8Path,
798 pe_type: PEType,
799 uki_id: &Sha512HashValue,
800 is_insecure_from_opts: bool,
801 mounted_efi: impl AsRef<Path>,
802 bootloader: &Bootloader,
803) -> Result<Option<UKIInfo>> {
804 let efi_bin = read_file(file, &repo).context("Reading .efi binary")?;
805
806 let mut boot_label: Option<UKIInfo> = None;
807
808 if matches!(pe_type, PEType::Uki) {
811 let cmdline = uki::get_cmdline(&efi_bin).context("Getting UKI cmdline")?;
812
813 let (composefs_cmdline, insecure) =
814 get_cmdline_composefs::<Sha512HashValue>(cmdline).context("Parsing composefs=")?;
815
816 match is_insecure_from_opts {
819 true if !insecure => {
820 tracing::warn!("--insecure passed as option but UKI cmdline does not support it");
821 }
822
823 false if insecure => {
824 tracing::warn!("UKI cmdline has composefs set as insecure");
825 }
826
827 _ => { }
828 }
829
830 if composefs_cmdline != *uki_id {
831 anyhow::bail!(
832 "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})"
833 );
834 }
835
836 let osrel = uki::get_text_section(&efi_bin, ".osrel")?;
837
838 let parsed_osrel = OsReleaseInfo::parse(osrel);
839
840 let boot_digest = compute_boot_digest_uki(&efi_bin)?;
841
842 boot_label = Some(UKIInfo {
843 boot_label: uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?,
844 version: parsed_osrel.get_version(),
845 os_id: parsed_osrel.get_value(&["ID"]),
846 boot_digest,
847 });
848 }
849
850 let efi_linux_path = mounted_efi.as_ref().join(match bootloader {
852 Bootloader::Grub => EFI_LINUX,
853 Bootloader::Systemd => SYSTEMD_UKI_DIR,
854 });
855
856 create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?;
857
858 let final_pe_path = match file_path.parent() {
859 Some(parent) => {
860 let renamed_path = match parent.as_str().ends_with(EFI_ADDON_DIR_EXT) {
861 true => {
862 let dir_name = format!("{}{}", uki_id.to_hex(), EFI_ADDON_DIR_EXT);
863
864 parent
865 .parent()
866 .map(|p| p.join(&dir_name))
867 .unwrap_or(dir_name.into())
868 }
869
870 false => parent.to_path_buf(),
871 };
872
873 let full_path = efi_linux_path.join(renamed_path);
874 create_dir_all(&full_path)?;
875
876 full_path
877 }
878
879 None => efi_linux_path,
880 };
881
882 let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority())
883 .with_context(|| format!("Opening {final_pe_path:?}"))?;
884
885 let pe_name = match pe_type {
886 PEType::Uki => &format!("{}{}", uki_id.to_hex(), EFI_EXT),
887 PEType::UkiAddon => file_path
888 .components()
889 .last()
890 .ok_or_else(|| anyhow::anyhow!("Failed to get UKI Addon file name"))?
891 .as_str(),
892 };
893
894 pe_dir
895 .atomic_write(pe_name, efi_bin)
896 .context("Writing UKI")?;
897
898 rustix::fs::fsync(
899 pe_dir
900 .reopen_as_ownedfd()
901 .context("Reopening as owned fd")?,
902 )
903 .context("fsync")?;
904
905 Ok(boot_label)
906}
907
908#[context("Writing Grub menuentry")]
909fn write_grub_uki_menuentry(
910 root_path: Utf8PathBuf,
911 setup_type: &BootSetupType,
912 boot_label: String,
913 id: &Sha512HashValue,
914 esp_device: &String,
915) -> Result<()> {
916 let boot_dir = root_path.join("boot");
917 create_dir_all(&boot_dir).context("Failed to create boot dir")?;
918
919 let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
920
921 let efi_uuid_source = get_efi_uuid_source();
922
923 let user_cfg_name = if is_upgrade {
924 USER_CFG_STAGED
925 } else {
926 USER_CFG
927 };
928
929 let grub_dir = Dir::open_ambient_dir(boot_dir.join("grub2"), ambient_authority())
930 .context("opening boot/grub2")?;
931
932 if is_upgrade {
934 let mut str_buf = String::new();
935 let boot_dir =
936 Dir::open_ambient_dir(boot_dir, ambient_authority()).context("Opening boot dir")?;
937 let entries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut str_buf)?;
938
939 grub_dir
940 .atomic_replace_with(user_cfg_name, |f| -> std::io::Result<_> {
941 f.write_all(efi_uuid_source.as_bytes())?;
942 f.write_all(
943 MenuEntry::new(&boot_label, &id.to_hex())
944 .to_string()
945 .as_bytes(),
946 )?;
947
948 f.write_all(entries[0].to_string().as_bytes())?;
952
953 Ok(())
954 })
955 .with_context(|| format!("Writing to {user_cfg_name}"))?;
956
957 rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
958
959 return Ok(());
960 }
961
962 let esp_uuid = Task::new("blkid for ESP UUID", "blkid")
965 .args(["-s", "UUID", "-o", "value", &esp_device])
966 .read()?;
967
968 grub_dir.atomic_write(
969 EFI_UUID_FILE,
970 format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes(),
971 )?;
972
973 grub_dir
975 .atomic_replace_with(user_cfg_name, |f| -> std::io::Result<_> {
976 f.write_all(efi_uuid_source.as_bytes())?;
977 f.write_all(
978 MenuEntry::new(&boot_label, &id.to_hex())
979 .to_string()
980 .as_bytes(),
981 )?;
982
983 Ok(())
984 })
985 .with_context(|| format!("Writing to {user_cfg_name}"))?;
986
987 rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
988
989 Ok(())
990}
991
992#[context("Writing systemd UKI config")]
993fn write_systemd_uki_config(
994 esp_dir: &Dir,
995 setup_type: &BootSetupType,
996 boot_label: UKIInfo,
997 id: &Sha512HashValue,
998) -> Result<()> {
999 let os_id = boot_label.os_id.as_deref().unwrap_or("bootc");
1000 let primary_sort_key = primary_sort_key(os_id);
1001
1002 let mut bls_conf = BLSConfig::default();
1003 bls_conf
1004 .with_title(boot_label.boot_label)
1005 .with_cfg(BLSConfigType::EFI {
1006 efi: format!("/{SYSTEMD_UKI_DIR}/{}{}", id.to_hex(), EFI_EXT).into(),
1007 })
1008 .with_sort_key(primary_sort_key.clone())
1009 .with_version(boot_label.version.unwrap_or_else(|| id.to_hex()));
1010
1011 let (entries_dir, booted_bls) = match setup_type {
1012 BootSetupType::Setup(..) => {
1013 esp_dir
1014 .create_dir_all(TYPE1_ENT_PATH)
1015 .with_context(|| format!("Creating {TYPE1_ENT_PATH}"))?;
1016
1017 (esp_dir.open_dir(TYPE1_ENT_PATH)?, None)
1018 }
1019
1020 BootSetupType::Upgrade(_) => {
1021 esp_dir
1022 .create_dir_all(TYPE1_ENT_PATH_STAGED)
1023 .with_context(|| format!("Creating {TYPE1_ENT_PATH_STAGED}"))?;
1024
1025 let mut booted_bls = get_booted_bls(&esp_dir)?;
1026 booted_bls.sort_key = Some(secondary_sort_key(os_id));
1027
1028 (esp_dir.open_dir(TYPE1_ENT_PATH_STAGED)?, Some(booted_bls))
1029 }
1030 };
1031
1032 entries_dir
1033 .atomic_write(
1034 type1_entry_conf_file_name(os_id, &bls_conf.version(), FILENAME_PRIORITY_PRIMARY),
1035 bls_conf.to_string().as_bytes(),
1036 )
1037 .context("Writing conf file")?;
1038
1039 if let Some(booted_bls) = booted_bls {
1040 entries_dir.atomic_write(
1041 type1_entry_conf_file_name(os_id, &booted_bls.version(), FILENAME_PRIORITY_SECONDARY),
1042 booted_bls.to_string().as_bytes(),
1043 )?;
1044 }
1045
1046 if !esp_dir.exists(SYSTEMD_LOADER_CONF_PATH) {
1048 esp_dir
1049 .atomic_write(SYSTEMD_LOADER_CONF_PATH, SYSTEMD_TIMEOUT)
1050 .with_context(|| format!("Writing to {SYSTEMD_LOADER_CONF_PATH}"))?;
1051 }
1052
1053 let esp_dir = esp_dir
1054 .reopen_as_ownedfd()
1055 .context("Reopening as owned fd")?;
1056 rustix::fs::fsync(esp_dir).context("fsync")?;
1057
1058 Ok(())
1059}
1060
1061#[context("Setting up UKI boot")]
1062pub(crate) fn setup_composefs_uki_boot(
1063 setup_type: BootSetupType,
1064 repo: crate::store::ComposefsRepository,
1065 id: &Sha512HashValue,
1066 entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
1067) -> Result<String> {
1068 let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type {
1069 BootSetupType::Setup((root_setup, state, postfetch, ..)) => {
1070 state.require_no_kargs_for_uki()?;
1071
1072 let esp_part = esp_in(&root_setup.device_info)?;
1073
1074 (
1075 root_setup.physical_root_path.clone(),
1076 esp_part.node.clone(),
1077 postfetch.detected_bootloader.clone(),
1078 state.composefs_options.insecure,
1079 state.composefs_options.uki_addon.as_ref(),
1080 )
1081 }
1082
1083 BootSetupType::Upgrade((storage, _, host)) => {
1084 let sysroot = Utf8PathBuf::from("/sysroot"); let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?;
1086 let bootloader = host.require_composefs_booted()?.bootloader.clone();
1087
1088 (
1089 sysroot,
1090 get_esp_partition(&sysroot_parent)?.0,
1091 bootloader,
1092 false,
1093 None,
1094 )
1095 }
1096 };
1097
1098 let esp_mount = mount_esp(&esp_device).context("Mounting ESP")?;
1099
1100 let mut uki_info: Option<UKIInfo> = None;
1101
1102 for entry in entries {
1103 match entry {
1104 ComposefsBootEntry::Type1(..) => tracing::debug!("Skipping Type1 Entry"),
1105 ComposefsBootEntry::UsrLibModulesVmLinuz(..) => {
1106 tracing::debug!("Skipping vmlinuz in /usr/lib/modules")
1107 }
1108
1109 ComposefsBootEntry::Type2(entry) => {
1110 if matches!(entry.pe_type, PEType::UkiAddon) {
1112 let Some(addons) = uki_addons else {
1113 continue;
1114 };
1115
1116 let addon_name = entry
1117 .file_path
1118 .components()
1119 .last()
1120 .ok_or_else(|| anyhow::anyhow!("Could not get UKI addon name"))?;
1121
1122 let addon_name = addon_name.as_str()?;
1123
1124 let addon_name =
1125 addon_name.strip_suffix(EFI_ADDON_FILE_EXT).ok_or_else(|| {
1126 anyhow::anyhow!("UKI addon doesn't end with {EFI_ADDON_DIR_EXT}")
1127 })?;
1128
1129 if !addons.iter().any(|passed_addon| passed_addon == addon_name) {
1130 continue;
1131 }
1132 }
1133
1134 let utf8_file_path = Utf8Path::from_path(&entry.file_path)
1135 .ok_or_else(|| anyhow::anyhow!("Path is not valid UTf8"))?;
1136
1137 let ret = write_pe_to_esp(
1138 &repo,
1139 &entry.file,
1140 utf8_file_path,
1141 entry.pe_type,
1142 &id,
1143 is_insecure_from_opts,
1144 esp_mount.dir.path(),
1145 &bootloader,
1146 )?;
1147
1148 if let Some(label) = ret {
1149 uki_info = Some(label);
1150 }
1151 }
1152 };
1153 }
1154
1155 let uki_info =
1156 uki_info.ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?;
1157
1158 let boot_digest = uki_info.boot_digest.clone();
1159
1160 match bootloader {
1161 Bootloader::Grub => {
1162 write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
1163 }
1164
1165 Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?,
1166 };
1167
1168 Ok(boot_digest)
1169}
1170
1171pub struct SecurebootKeys {
1172 pub dir: Dir,
1173 pub keys: Vec<Utf8PathBuf>,
1174}
1175
1176fn get_secureboot_keys(fs: &Dir, p: &str) -> Result<Option<SecurebootKeys>> {
1177 let mut entries = vec![];
1178
1179 let keys_dir = match fs.open_dir_optional(p)? {
1181 Some(d) => d,
1182 _ => return Ok(None),
1183 };
1184
1185 for entry in keys_dir.entries()? {
1188 let dir_e = entry?;
1189 let dirname = dir_e.file_name();
1190 if !dir_e.file_type()?.is_dir() {
1191 bail!("/{p}/{dirname:?} is not a directory");
1192 }
1193
1194 let dir_path: Utf8PathBuf = dirname.try_into()?;
1195 let dir = dir_e.open_dir()?;
1196 for entry in dir.entries()? {
1197 let e = entry?;
1198 let local: Utf8PathBuf = e.file_name().try_into()?;
1199 let path = dir_path.join(local);
1200
1201 if path.extension() != Some(AUTH_EXT) {
1202 continue;
1203 }
1204
1205 if !e.file_type()?.is_file() {
1206 bail!("/{p}/{path:?} is not a file");
1207 }
1208 entries.push(path);
1209 }
1210 }
1211 return Ok(Some(SecurebootKeys {
1212 dir: keys_dir,
1213 keys: entries,
1214 }));
1215}
1216
1217#[context("Setting up composefs boot")]
1218pub(crate) async fn setup_composefs_boot(
1219 root_setup: &RootSetup,
1220 state: &State,
1221 image_id: &str,
1222) -> Result<()> {
1223 let repo = open_composefs_repo(&root_setup.physical_root)?;
1224 let mut fs = create_composefs_filesystem(&repo, image_id, None)?;
1225 let entries = fs.transform_for_boot(&repo)?;
1226 let id = fs.commit_image(&repo, None)?;
1227 let mounted_fs = Dir::reopen_dir(
1228 &repo
1229 .mount(&id.to_hex())
1230 .context("Failed to mount composefs image")?,
1231 )?;
1232
1233 let postfetch = PostFetchState::new(state, &mounted_fs)?;
1234
1235 let boot_uuid = root_setup
1236 .get_boot_uuid()?
1237 .or(root_setup.rootfs_uuid.as_deref())
1238 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1239
1240 if cfg!(target_arch = "s390x") {
1241 crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?;
1243 } else if postfetch.detected_bootloader == Bootloader::Grub {
1244 crate::bootloader::install_via_bootupd(
1245 &root_setup.device_info,
1246 &root_setup.physical_root_path,
1247 &state.config_opts,
1248 None,
1249 )?;
1250 } else {
1251 crate::bootloader::install_systemd_boot(
1252 &root_setup.device_info,
1253 &root_setup.physical_root_path,
1254 &state.config_opts,
1255 None,
1256 get_secureboot_keys(&mounted_fs, BOOTC_AUTOENROLL_PATH)?,
1257 )?;
1258 }
1259
1260 let Some(entry) = entries.iter().next() else {
1261 anyhow::bail!("No boot entries!");
1262 };
1263
1264 let boot_type = BootType::from(entry);
1265
1266 let boot_digest = match boot_type {
1267 BootType::Bls => setup_composefs_bls_boot(
1268 BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)),
1269 repo,
1270 &id,
1271 entry,
1272 &mounted_fs,
1273 )?,
1274 BootType::Uki => setup_composefs_uki_boot(
1275 BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)),
1276 repo,
1277 &id,
1278 entries,
1279 )?,
1280 };
1281
1282 write_composefs_state(
1283 &root_setup.physical_root_path,
1284 &id,
1285 &crate::spec::ImageReference::from(state.target_imgref.clone()),
1286 None,
1287 boot_type,
1288 boot_digest,
1289 &get_container_manifest_and_config(&get_imgref(
1290 &state.source.imageref.transport.to_string(),
1291 &state.source.imageref.name,
1292 ))
1293 .await?,
1294 )
1295 .await?;
1296
1297 Ok(())
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302 use super::*;
1303
1304 #[test]
1305 fn test_type1_filename_generation() {
1306 let filename =
1308 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1309 assert_eq!(filename, "bootc_fedora-41.20251125.0-1.conf");
1310
1311 let primary =
1313 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1314 let secondary =
1315 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1316 assert_eq!(primary, "bootc_fedora-41.20251125.0-1.conf");
1317 assert_eq!(secondary, "bootc_fedora-41.20251125.0-0.conf");
1318
1319 let filename =
1321 type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1322 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1323
1324 let filename =
1326 type1_entry_conf_file_name("my-custom-os", "1.0.0", FILENAME_PRIORITY_PRIMARY);
1327 assert_eq!(filename, "bootc_my_custom_os-1.0.0-1.conf");
1328
1329 let filename = type1_entry_conf_file_name("rhel", "9.3.0", FILENAME_PRIORITY_SECONDARY);
1331 assert_eq!(filename, "bootc_rhel-9.3.0-0.conf");
1332 }
1333
1334 #[test]
1335 fn test_grub_filename_parsing() {
1336 let filename = type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", "1");
1345 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1346
1347 let without_ext = filename.strip_suffix(".conf").unwrap();
1353 let parts: Vec<&str> = without_ext.rsplitn(3, '-').collect();
1354 assert_eq!(parts.len(), 3);
1355 assert_eq!(parts[0], "1"); assert_eq!(parts[1], "41.20251125.0"); assert_eq!(parts[2], "bootc_fedora_coreos"); }
1359
1360 #[test]
1361 fn test_sort_keys() {
1362 let primary = primary_sort_key("fedora");
1364 let secondary = secondary_sort_key("fedora");
1365
1366 assert_eq!(primary, "bootc-fedora-0");
1367 assert_eq!(secondary, "bootc-fedora-1");
1368
1369 assert!(primary < secondary);
1371
1372 let primary_coreos = primary_sort_key("fedora-coreos");
1374 assert_eq!(primary_coreos, "bootc-fedora-coreos-0");
1375 }
1376
1377 #[test]
1378 fn test_filename_sorting_grub_style() {
1379 let primary =
1383 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1384 let secondary =
1385 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1386
1387 assert!(
1389 primary > secondary,
1390 "Primary should sort before secondary in descending order"
1391 );
1392
1393 let newer =
1395 type1_entry_conf_file_name("fedora", "42.20251125.0", FILENAME_PRIORITY_PRIMARY);
1396 let older =
1397 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1398
1399 assert!(
1401 newer > older,
1402 "Newer version should sort before older in descending order"
1403 );
1404
1405 let fedora = type1_entry_conf_file_name("fedora", "41.0", FILENAME_PRIORITY_PRIMARY);
1407 let rhel = type1_entry_conf_file_name("rhel", "9.0", FILENAME_PRIORITY_PRIMARY);
1408
1409 assert!(
1411 rhel > fedora,
1412 "RHEL should sort before Fedora in descending order"
1413 );
1414 }
1415}