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