1use std::fs::create_dir_all;
65use std::io::Read;
66use std::io::Write;
67use std::path::Path;
68use std::sync::Arc;
69
70use anyhow::{Context, Result, anyhow, bail};
71use bootc_kernel_cmdline::utf8::{Cmdline, Parameter};
72use bootc_mount::tempmount::TempMount;
73use camino::{Utf8Path, Utf8PathBuf};
74use cap_std_ext::{
75 cap_std::{ambient_authority, fs::Dir},
76 dirext::CapStdExtDirExt,
77};
78use clap::ValueEnum;
79use composefs::fs::read_file;
80use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
81use composefs::tree::RegularFile;
82use composefs_boot::bootloader::{
83 BootEntry as ComposefsBootEntry, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT, PEType,
84 UsrLibModulesVmlinuz, get_boot_resources,
85};
86use composefs_boot::{cmdline::get_cmdline_composefs, os_release::OsReleaseInfo, uki};
87use composefs_ctl::composefs;
88use composefs_ctl::composefs_boot;
89use composefs_ctl::composefs_oci;
90use fn_error_context::context;
91use rustix::{mount::MountFlags, path::Arg};
92use schemars::JsonSchema;
93use serde::{Deserialize, Serialize};
94
95use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state};
96use crate::bootc_composefs::status::ComposefsCmdline;
97use crate::bootc_kargs::compute_new_kargs;
98use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED};
99use crate::parsers::bls_config::{BLSConfig, BLSConfigType};
100use crate::task::Task;
101use crate::{bootc_composefs::repo::open_composefs_repo, store::Storage};
102use crate::{bootc_composefs::status::get_sorted_grub_uki_boot_entries, install::PostFetchState};
103use crate::{
104 composefs_consts::{
105 BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, UKI_NAME_PREFIX, USER_CFG, USER_CFG_STAGED,
106 },
107 spec::{Bootloader, Host},
108};
109use crate::{parsers::grub_menuconfig::MenuEntry, store::BootedComposefs};
110
111use crate::install::{RootSetup, State};
112
113pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
115pub(crate) const EFI_LINUX: &str = "EFI/Linux";
117
118const SYSTEMD_TIMEOUT: &str = "timeout 5";
120const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf";
121
122pub(crate) const INITRD: &str = "initrd";
123pub(crate) const VMLINUZ: &str = "vmlinuz";
124
125const BOOTC_AUTOENROLL_PATH: &str = "usr/lib/bootc/install/secureboot-keys";
126
127const AUTH_EXT: &str = "auth";
128
129pub(crate) const BOOTC_UKI_DIR: &str = "EFI/Linux/bootc";
134
135pub(crate) enum BootSetupType<'a> {
136 Setup((&'a RootSetup, &'a State, &'a PostFetchState)),
138 Upgrade((&'a Storage, &'a BootedComposefs, &'a Host)),
140}
141
142#[derive(
143 ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema,
144)]
145pub enum BootType {
146 #[default]
147 Bls,
148 Uki,
149}
150
151impl ::std::fmt::Display for BootType {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 let s = match self {
154 BootType::Bls => "bls",
155 BootType::Uki => "uki",
156 };
157
158 write!(f, "{}", s)
159 }
160}
161
162impl TryFrom<&str> for BootType {
163 type Error = anyhow::Error;
164
165 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
166 match value {
167 "bls" => Ok(Self::Bls),
168 "uki" => Ok(Self::Uki),
169 unrecognized => Err(anyhow::anyhow!(
170 "Unrecognized boot option: '{unrecognized}'"
171 )),
172 }
173 }
174}
175
176impl From<&ComposefsBootEntry<Sha512HashValue>> for BootType {
177 fn from(entry: &ComposefsBootEntry<Sha512HashValue>) -> Self {
178 match entry {
179 ComposefsBootEntry::Type1(..) => Self::Bls,
180 ComposefsBootEntry::Type2(..) => Self::Uki,
181 ComposefsBootEntry::UsrLibModulesVmLinuz(..) => Self::Bls,
182 }
183 }
184}
185
186pub(crate) fn get_efi_uuid_source() -> String {
189 format!(
190 r#"
191if [ -f ${{config_directory}}/{EFI_UUID_FILE} ]; then
192 source ${{config_directory}}/{EFI_UUID_FILE}
193fi
194"#
195 )
196}
197
198const ESP_MOUNT_FLAGS: MountFlags =
200 MountFlags::from_bits_retain(MountFlags::NOEXEC.bits() | MountFlags::NOSUID.bits());
201
202const ESP_MOUNT_DATA: &std::ffi::CStr = c"fmask=0177,dmask=0077";
204
205pub fn mount_esp(device: &str) -> Result<TempMount> {
207 TempMount::mount_dev(device, "vfat", ESP_MOUNT_FLAGS, Some(ESP_MOUNT_DATA))
208}
209
210pub(crate) fn mount_esp_at(
213 device: &str,
214 path: std::path::PathBuf,
215) -> Result<bootc_mount::tempmount::MountGuard> {
216 bootc_mount::tempmount::MountGuard::mount(
217 device,
218 path,
219 "vfat",
220 ESP_MOUNT_FLAGS,
221 Some(ESP_MOUNT_DATA),
222 )
223}
224
225pub(crate) const FILENAME_PRIORITY_PRIMARY: &str = "1";
228
229pub(crate) const FILENAME_PRIORITY_SECONDARY: &str = "0";
231
232pub(crate) const SORTKEY_PRIORITY_PRIMARY: &str = "0";
235
236pub(crate) const SORTKEY_PRIORITY_SECONDARY: &str = "1";
238
239pub fn type1_entry_conf_file_name(
251 os_id: &str,
252 version: impl std::fmt::Display,
253 priority: &str,
254) -> String {
255 let os_id_safe = os_id.replace('-', "_");
256 format!("bootc_{os_id_safe}-{version}-{priority}.conf")
257}
258
259pub(crate) fn primary_sort_key(os_id: &str) -> String {
264 format!("bootc-{os_id}-{SORTKEY_PRIORITY_PRIMARY}")
265}
266
267pub(crate) fn secondary_sort_key(os_id: &str) -> String {
270 format!("bootc-{os_id}-{SORTKEY_PRIORITY_SECONDARY}")
271}
272
273pub(crate) fn get_type1_dir_name(depl_verity: &str) -> String {
275 format!("{TYPE1_BOOT_DIR_PREFIX}{depl_verity}")
276}
277
278pub(crate) fn get_uki_name(depl_verity: &str) -> String {
280 format!("{UKI_NAME_PREFIX}{depl_verity}{EFI_EXT}")
281}
282
283pub(crate) fn get_uki_addon_dir_name(depl_verity: &str) -> String {
285 format!("{UKI_NAME_PREFIX}{depl_verity}{EFI_ADDON_DIR_EXT}")
286}
287
288#[allow(dead_code)]
289pub(crate) fn get_uki_addon_file_name(depl_verity: &str) -> String {
291 format!("{UKI_NAME_PREFIX}{depl_verity}{EFI_ADDON_FILE_EXT}")
292}
293
294#[context("Computing boot digest")]
300fn compute_boot_digest(
301 entry: &UsrLibModulesVmlinuz<Sha512HashValue>,
302 repo: &crate::store::ComposefsRepository,
303) -> Result<String> {
304 let vmlinuz = read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?;
305
306 let Some(initramfs) = &entry.initramfs else {
307 anyhow::bail!("initramfs not found");
308 };
309
310 let initramfs = read_file(initramfs, &repo).context("Reading intird")?;
311
312 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
313 .context("Creating hasher")?;
314
315 hasher.update(&vmlinuz).context("hashing vmlinuz")?;
316 hasher.update(&initramfs).context("hashing initrd")?;
317
318 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
319
320 Ok(hex::encode(digest))
321}
322
323#[context("Computing boot digest for Type1 entries")]
324fn compute_boot_digest_type1(dir: &Dir) -> Result<String> {
325 let mut vmlinuz = dir
326 .open(VMLINUZ)
327 .with_context(|| format!("Opening {VMLINUZ}"))?;
328
329 let mut initrd = dir
330 .open(INITRD)
331 .with_context(|| format!("Opening {INITRD}"))?;
332
333 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
334 .context("Creating hasher")?;
335
336 std::io::copy(&mut vmlinuz, &mut hasher)?;
337 std::io::copy(&mut initrd, &mut hasher)?;
338
339 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
340
341 Ok(hex::encode(digest))
342}
343
344#[context("Computing boot digest")]
350pub(crate) fn compute_boot_digest_uki(uki: &[u8]) -> Result<String> {
351 let vmlinuz =
352 uki::get_section(uki, ".linux").ok_or_else(|| anyhow::anyhow!(".linux not present"))??;
353
354 let initramfs = uki::get_section(uki, ".initrd")
355 .ok_or_else(|| anyhow::anyhow!(".initrd not present"))??;
356
357 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
358 .context("Creating hasher")?;
359
360 hasher.update(&vmlinuz).context("hashing vmlinuz")?;
361 hasher.update(&initramfs).context("hashing initrd")?;
362
363 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
364
365 Ok(hex::encode(digest))
366}
367
368#[context("Checking boot entry duplicates")]
374pub(crate) fn find_vmlinuz_initrd_duplicate(
375 storage: &Storage,
376 digest: &str,
377) -> Result<Option<String>> {
378 let boot_dir = storage.bls_boot_binaries_dir()?;
379
380 for entry in boot_dir.entries_utf8()? {
381 let entry = entry?;
382 let dir_name = entry.file_name()?;
383
384 if !entry.file_type()?.is_dir() {
385 continue;
386 }
387
388 let Some(..) = dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) else {
389 continue;
390 };
391
392 let entry_digest = compute_boot_digest_type1(&boot_dir.open_dir(&dir_name)?)?;
393
394 if entry_digest == digest {
395 return Ok(Some(dir_name));
396 }
397 }
398
399 Ok(None)
400}
401
402#[context("Writing BLS entries to disk")]
403fn write_bls_boot_entries_to_disk(
404 boot_dir: &Utf8PathBuf,
405 deployment_id: &Sha512HashValue,
406 entry: &UsrLibModulesVmlinuz<Sha512HashValue>,
407 repo: &crate::store::ComposefsRepository,
408) -> Result<()> {
409 let dir_name = get_type1_dir_name(&deployment_id.to_hex());
410
411 let path = boot_dir.join(&dir_name);
413 create_dir_all(&path)?;
414
415 let entries_dir = Dir::open_ambient_dir(&path, ambient_authority())
416 .with_context(|| format!("Opening {path}"))?;
417
418 entries_dir
419 .atomic_write(
420 VMLINUZ,
421 read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?,
422 )
423 .context("Writing vmlinuz to path")?;
424
425 let Some(initramfs) = &entry.initramfs else {
426 anyhow::bail!("initramfs not found");
427 };
428
429 entries_dir
430 .atomic_write(
431 INITRD,
432 read_file(initramfs, &repo).context("Reading initrd")?,
433 )
434 .context("Writing initrd to path")?;
435
436 let owned_fd = entries_dir
438 .reopen_as_ownedfd()
439 .context("Reopen as owned fd")?;
440
441 rustix::fs::fsync(owned_fd).context("fsync")?;
442
443 Ok(())
444}
445
446pub fn parse_os_release(root: &Dir) -> Result<Option<(String, Option<String>, Option<String>)>> {
450 let file = root
452 .open_optional("usr/lib/os-release")
453 .context("Opening usr/lib/os-release")?;
454
455 let Some(mut os_rel_file) = file else {
456 return Ok(None);
457 };
458
459 let mut file_contents = String::new();
460 os_rel_file.read_to_string(&mut file_contents)?;
461
462 let parsed = OsReleaseInfo::parse(&file_contents);
463
464 let os_id = parsed
465 .get_value(&["ID"])
466 .unwrap_or_else(|| "bootc".to_string());
467
468 Ok(Some((
469 os_id,
470 parsed.get_pretty_name(),
471 parsed.get_version(),
472 )))
473}
474
475struct BLSEntryPath {
476 entries_path: Utf8PathBuf,
478 abs_entries_path: Utf8PathBuf,
480 config_path: Utf8PathBuf,
482}
483
484#[context("Setting up BLS boot")]
489pub(crate) fn setup_composefs_bls_boot(
490 setup_type: BootSetupType,
491 repo: crate::store::ComposefsRepository,
492 id: &Sha512HashValue,
493 entry: &ComposefsBootEntry<Sha512HashValue>,
494 mounted_erofs: &Dir,
495) -> Result<String> {
496 let id_hex = id.to_hex();
497
498 let (root_path, esp_device, mut cmdline_refs, bootloader) = match setup_type {
499 BootSetupType::Setup((root_setup, state, postfetch)) => {
500 let mut cmdline_options = Cmdline::new();
502
503 cmdline_options.extend(&root_setup.kargs);
504
505 let composefs_cmdline =
506 ComposefsCmdline::build(&id_hex, state.composefs_options.allow_missing_verity);
507 cmdline_options.extend(&Cmdline::from(&composefs_cmdline.to_string()));
508
509 if let Some(boot) = root_setup.boot_mount_spec() {
513 if !boot.source.is_empty() {
514 let mount_extra = format!(
515 "systemd.mount-extra={}:/boot:{}:{}",
516 boot.source,
517 boot.fstype,
518 boot.options.as_deref().unwrap_or("defaults"),
519 );
520 cmdline_options.extend(&Cmdline::from(mount_extra.as_str()));
521 tracing::debug!("Added /boot mount karg: {mount_extra}");
522 }
523 }
524
525 let esp_part = root_setup.device_info.find_first_colocated_esp()?;
527
528 (
529 root_setup.physical_root_path.clone(),
530 esp_part.path(),
531 cmdline_options,
532 postfetch.detected_bootloader.clone(),
533 )
534 }
535
536 BootSetupType::Upgrade((storage, booted_cfs, host)) => {
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, booted_cfs)?;
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 cfs_cmdline =
555 ComposefsCmdline::build(&id_hex, booted_cfs.cmdline.allow_missing_fsverity)
556 .to_string();
557
558 let param = Parameter::parse(&cfs_cmdline)
559 .context("Failed to create 'composefs=' parameter")?;
560 cmdline.add_or_modify(¶m);
561
562 let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
564 let esp_dev = root_dev.find_first_colocated_esp()?;
565
566 (
567 Utf8PathBuf::from("/sysroot"),
568 esp_dev.path(),
569 cmdline,
570 bootloader,
571 )
572 }
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")? as &Dir)
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 Bootloader::None => unreachable!("Checked at install time"),
627 };
628
629 let (bls_config, boot_digest, os_id) = match &entry {
630 ComposefsBootEntry::Type1(..) => anyhow::bail!("Found Type1 entries in /boot"),
631 ComposefsBootEntry::Type2(..) => anyhow::bail!("Found UKI"),
632
633 ComposefsBootEntry::UsrLibModulesVmLinuz(usr_lib_modules_vmlinuz) => {
634 let boot_digest = compute_boot_digest(usr_lib_modules_vmlinuz, &repo)
635 .context("Computing boot digest")?;
636
637 let osrel = parse_os_release(mounted_erofs)?;
638
639 let (os_id, title, version, sort_key) = match osrel {
640 Some((id_str, title_opt, version_opt)) => (
641 id_str.clone(),
642 title_opt.unwrap_or_else(|| id.to_hex()),
643 version_opt.unwrap_or_else(|| id.to_hex()),
644 primary_sort_key(&id_str),
645 ),
646 None => {
647 let default_id = "bootc".to_string();
648 (
649 default_id.clone(),
650 id.to_hex(),
651 id.to_hex(),
652 primary_sort_key(&default_id),
653 )
654 }
655 };
656
657 let mut bls_config = BLSConfig::default();
658
659 let entries_dir = get_type1_dir_name(&id_hex);
660
661 bls_config
662 .with_title(title)
663 .with_version(version)
664 .with_sort_key(sort_key)
665 .with_cfg(BLSConfigType::NonEFI {
666 linux: entry_paths
667 .abs_entries_path
668 .join(&entries_dir)
669 .join(VMLINUZ),
670 initrd: vec![entry_paths.abs_entries_path.join(&entries_dir).join(INITRD)],
671 options: Some(cmdline_refs),
672 });
673
674 let shared_entry = match setup_type {
675 BootSetupType::Setup(_) => None,
676 BootSetupType::Upgrade((storage, ..)) => {
677 find_vmlinuz_initrd_duplicate(storage, &boot_digest)?
678 }
679 };
680
681 match shared_entry {
682 Some(shared_entry) => {
683 match bls_config.cfg_type {
689 BLSConfigType::NonEFI {
690 ref mut linux,
691 ref mut initrd,
692 ..
693 } => {
694 *linux = entry_paths
695 .abs_entries_path
696 .join(&shared_entry)
697 .join(VMLINUZ);
698
699 *initrd = vec![
700 entry_paths
701 .abs_entries_path
702 .join(&shared_entry)
703 .join(INITRD),
704 ];
705 }
706
707 _ => unreachable!(),
708 };
709 }
710
711 None => {
712 write_bls_boot_entries_to_disk(
713 &entry_paths.entries_path,
714 id,
715 usr_lib_modules_vmlinuz,
716 &repo,
717 )?;
718 }
719 };
720
721 (bls_config, boot_digest, os_id)
722 }
723 };
724
725 let loader_path = entry_paths.config_path.join("loader");
726
727 let (config_path, booted_bls) = if is_upgrade {
728 let boot_dir = Dir::open_ambient_dir(&entry_paths.config_path, ambient_authority())?;
729
730 let BootSetupType::Upgrade((_, booted_cfs, ..)) = setup_type else {
731 unreachable!("enum mismatch");
733 };
734
735 let mut booted_bls = get_booted_bls(&boot_dir, booted_cfs)?;
736 booted_bls.sort_key = Some(secondary_sort_key(&os_id));
737
738 let staged_path = loader_path.join(STAGED_BOOT_LOADER_ENTRIES);
739
740 if boot_dir
743 .remove_all_optional(TYPE1_ENT_PATH_STAGED)
744 .context("Failed to remove staged directory")?
745 {
746 tracing::debug!("Removed existing staged entries directory");
747 }
748
749 (staged_path, Some(booted_bls))
751 } else {
752 (loader_path.join(BOOT_LOADER_ENTRIES), None)
753 };
754
755 create_dir_all(&config_path).with_context(|| format!("Creating {:?}", config_path))?;
756
757 let loader_entries_dir = Dir::open_ambient_dir(&config_path, ambient_authority())
758 .with_context(|| format!("Opening {config_path:?}"))?;
759
760 loader_entries_dir.atomic_write(
761 type1_entry_conf_file_name(&os_id, &bls_config.version(), FILENAME_PRIORITY_PRIMARY),
762 bls_config.to_string().as_bytes(),
763 )?;
764
765 if let Some(booted_bls) = booted_bls {
766 loader_entries_dir.atomic_write(
767 type1_entry_conf_file_name(&os_id, &booted_bls.version(), FILENAME_PRIORITY_SECONDARY),
768 booted_bls.to_string().as_bytes(),
769 )?;
770 }
771
772 let owned_loader_entries_fd = loader_entries_dir
773 .reopen_as_ownedfd()
774 .context("Reopening as owned fd")?;
775
776 rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?;
777
778 Ok(boot_digest)
779}
780
781struct UKIInfo {
782 boot_label: String,
783 version: Option<String>,
784 os_id: Option<String>,
785 boot_digest: String,
786}
787
788#[context("Writing {file_path} to ESP")]
790fn write_pe_to_esp(
791 repo: &crate::store::ComposefsRepository,
792 file: &RegularFile<Sha512HashValue>,
793 file_path: &Utf8Path,
794 pe_type: PEType,
795 uki_id: &Sha512HashValue,
796 missing_fsverity_allowed: bool,
797 mounted_efi: impl AsRef<Path>,
798) -> Result<Option<UKIInfo>> {
799 let efi_bin = read_file(file, &repo).context("Reading .efi binary")?;
800
801 let mut boot_label: Option<UKIInfo> = None;
802
803 if matches!(pe_type, PEType::Uki) {
806 let cmdline = uki::get_cmdline(&efi_bin).context("Getting UKI cmdline")?;
807
808 let (composefs_cmdline, missing_verity_allowed_cmdline) =
809 get_cmdline_composefs::<Sha512HashValue>(cmdline).context("Parsing composefs=")?;
810
811 match missing_fsverity_allowed {
814 true if !missing_verity_allowed_cmdline => {
815 tracing::warn!(
816 "--allow-missing-fsverity passed as option but UKI cmdline does not support it"
817 );
818 }
819
820 false if missing_verity_allowed_cmdline => {
821 tracing::warn!("UKI cmdline has composefs set as insecure");
822 }
823
824 _ => { }
825 }
826
827 if composefs_cmdline != *uki_id {
828 anyhow::bail!(
829 "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})"
830 );
831 }
832
833 let osrel = uki::get_text_section(&efi_bin, ".osrel")?;
834
835 let parsed_osrel = OsReleaseInfo::parse(osrel);
836
837 let boot_digest = compute_boot_digest_uki(&efi_bin)?;
838
839 boot_label = Some(UKIInfo {
840 boot_label: uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?,
841 version: parsed_osrel.get_version(),
842 os_id: parsed_osrel.get_value(&["ID"]),
843 boot_digest,
844 });
845 }
846
847 let efi_linux_path = mounted_efi.as_ref().join(BOOTC_UKI_DIR);
848 create_dir_all(&efi_linux_path).context("Creating bootc UKI directory")?;
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 = get_uki_addon_dir_name(&uki_id.to_hex());
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 => &get_uki_name(&uki_id.to_hex()),
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!("/{BOOTC_UKI_DIR}/{}", get_uki_name(&id.to_hex())).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((_, booted_cfs, ..)) => {
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, booted_cfs)?;
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, missing_fsverity_allowed, uki_addons) = match setup_type
1061 {
1062 BootSetupType::Setup((root_setup, state, postfetch)) => {
1063 state.require_no_kargs_for_uki()?;
1064
1065 let esp_part = root_setup.device_info.find_first_colocated_esp()?;
1067
1068 (
1069 root_setup.physical_root_path.clone(),
1070 esp_part.path(),
1071 postfetch.detected_bootloader.clone(),
1072 state.composefs_options.allow_missing_verity,
1073 state.composefs_options.uki_addon.as_ref(),
1074 )
1075 }
1076
1077 BootSetupType::Upgrade((storage, booted_cfs, host)) => {
1078 let sysroot = Utf8PathBuf::from("/sysroot"); let bootloader = host.require_composefs_booted()?.bootloader.clone();
1080
1081 let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
1083 let esp_dev = root_dev.find_first_colocated_esp()?;
1084
1085 (
1086 sysroot,
1087 esp_dev.path(),
1088 bootloader,
1089 booted_cfs.cmdline.allow_missing_fsverity,
1090 None,
1091 )
1092 }
1093 };
1094
1095 let esp_mount = mount_esp(&esp_device).context("Mounting ESP")?;
1096
1097 let mut uki_info: Option<UKIInfo> = None;
1098
1099 for entry in entries {
1100 match entry {
1101 ComposefsBootEntry::Type1(..) => tracing::debug!("Skipping Type1 Entry"),
1102 ComposefsBootEntry::UsrLibModulesVmLinuz(..) => {
1103 tracing::debug!("Skipping vmlinuz in /usr/lib/modules")
1104 }
1105
1106 ComposefsBootEntry::Type2(entry) => {
1107 if matches!(entry.pe_type, PEType::UkiAddon) {
1109 let Some(addons) = uki_addons else {
1110 continue;
1111 };
1112
1113 let addon_name = entry
1114 .file_path
1115 .components()
1116 .last()
1117 .ok_or_else(|| anyhow::anyhow!("Could not get UKI addon name"))?;
1118
1119 let addon_name = addon_name.as_str()?;
1120
1121 let addon_name =
1122 addon_name.strip_suffix(EFI_ADDON_FILE_EXT).ok_or_else(|| {
1123 anyhow::anyhow!("UKI addon doesn't end with {EFI_ADDON_DIR_EXT}")
1124 })?;
1125
1126 if !addons.iter().any(|passed_addon| passed_addon == addon_name) {
1127 continue;
1128 }
1129 }
1130
1131 let utf8_file_path = Utf8Path::from_path(&entry.file_path)
1132 .ok_or_else(|| anyhow::anyhow!("Path is not valid UTf8"))?;
1133
1134 let ret = write_pe_to_esp(
1135 &repo,
1136 &entry.file,
1137 utf8_file_path,
1138 entry.pe_type,
1139 &id,
1140 missing_fsverity_allowed,
1141 esp_mount.dir.path(),
1142 )?;
1143
1144 if let Some(label) = ret {
1145 uki_info = Some(label);
1146 }
1147 }
1148 };
1149 }
1150
1151 let uki_info =
1152 uki_info.ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?;
1153
1154 let boot_digest = uki_info.boot_digest.clone();
1155
1156 match bootloader {
1157 Bootloader::Grub => {
1158 write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
1159 }
1160
1161 Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?,
1162
1163 Bootloader::None => unreachable!("Checked at install time"),
1164 };
1165
1166 Ok(boot_digest)
1167}
1168
1169pub(crate) struct MountedImageRoot {
1183 _esp: bootc_mount::tempmount::MountGuard,
1185 _tmp: bootc_mount::tempmount::MountGuard,
1186 composefs: TempMount,
1187 pub(crate) esp_subdir: &'static str,
1188}
1189
1190impl MountedImageRoot {
1191 #[context("Preparing image root for bootloader installation")]
1195 pub(crate) fn new(
1196 composefs_mnt_fd: std::os::fd::OwnedFd,
1197 device: &bootc_blockdev::Device,
1198 ) -> Result<Self> {
1199 let roots = device.find_all_roots()?;
1200 let mut esp_part = None;
1201 for root in &roots {
1202 if let Some(esp) = root.find_partition_of_esp_optional()? {
1203 esp_part = Some(esp);
1204 break;
1205 }
1206 }
1207 let esp_part = esp_part.ok_or_else(|| anyhow!("ESP partition not found"))?;
1208
1209 let composefs = TempMount::mount_fd(composefs_mnt_fd)
1212 .context("Attaching composefs image to temporary directory")?;
1213
1214 let esp_subdir = "boot";
1219
1220 let esp_path = composefs.dir.path().join(esp_subdir);
1221 let esp =
1222 mount_esp_at(&esp_part.path(), esp_path).context("Mounting ESP into composefs root")?;
1223
1224 let tmp_path = composefs.dir.path().join("tmp");
1227 let tmp = bootc_mount::tempmount::MountGuard::mount(
1228 "tmpfs",
1229 tmp_path,
1230 "tmpfs",
1231 MountFlags::NOEXEC | MountFlags::NOSUID | MountFlags::NODEV,
1232 None::<&std::ffi::CStr>,
1233 )
1234 .context("Mounting tmpfs into composefs root")?;
1235
1236 Ok(Self {
1237 _esp: esp,
1238 _tmp: tmp,
1239 composefs,
1240 esp_subdir,
1241 })
1242 }
1243
1244 pub(crate) fn dir(&self) -> &Dir {
1246 &self.composefs.fd
1247 }
1248
1249 pub(crate) fn root_path(&self) -> &std::path::Path {
1251 self.composefs.dir.path()
1252 }
1253
1254 pub(crate) fn open_esp_dir(&self) -> Result<Dir> {
1256 self.composefs
1257 .fd
1258 .open_dir(self.esp_subdir)
1259 .with_context(|| format!("Opening ESP at /{}", self.esp_subdir))
1260 }
1261}
1262
1263pub struct SecurebootKeys {
1264 pub dir: Dir,
1265 pub keys: Vec<Utf8PathBuf>,
1266}
1267
1268fn get_secureboot_keys(fs: &Dir, p: &str) -> Result<Option<SecurebootKeys>> {
1269 let mut entries = vec![];
1270
1271 let keys_dir = match fs.open_dir_optional(p)? {
1273 Some(d) => d,
1274 _ => return Ok(None),
1275 };
1276
1277 for entry in keys_dir.entries()? {
1280 let dir_e = entry?;
1281 let dirname = dir_e.file_name();
1282 if !dir_e.file_type()?.is_dir() {
1283 bail!("/{p}/{dirname:?} is not a directory");
1284 }
1285
1286 let dir_path: Utf8PathBuf = dirname.try_into()?;
1287 let dir = dir_e.open_dir()?;
1288 for entry in dir.entries()? {
1289 let e = entry?;
1290 let local: Utf8PathBuf = e.file_name().try_into()?;
1291 let path = dir_path.join(local);
1292
1293 if path.extension() != Some(AUTH_EXT) {
1294 continue;
1295 }
1296
1297 if !e.file_type()?.is_file() {
1298 bail!("/{p}/{path:?} is not a file");
1299 }
1300 entries.push(path);
1301 }
1302 }
1303 return Ok(Some(SecurebootKeys {
1304 dir: keys_dir,
1305 keys: entries,
1306 }));
1307}
1308
1309#[context("Setting up composefs boot")]
1310pub(crate) async fn setup_composefs_boot(
1311 root_setup: &RootSetup,
1312 state: &State,
1313 pull_result: &composefs_oci::PullResult<Sha512HashValue>,
1314 allow_missing_fsverity: bool,
1315) -> Result<()> {
1316 const COMPOSEFS_BOOT_SETUP_JOURNAL_ID: &str = "1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5";
1317
1318 tracing::info!(
1319 message_id = COMPOSEFS_BOOT_SETUP_JOURNAL_ID,
1320 bootc.operation = "boot_setup",
1321 bootc.config_digest = %pull_result.config_digest,
1322 bootc.allow_missing_fsverity = allow_missing_fsverity,
1323 "Setting up composefs boot",
1324 );
1325
1326 let mut repo = open_composefs_repo(&root_setup.physical_root)?;
1327 if allow_missing_fsverity {
1328 repo.set_insecure();
1329 }
1330
1331 let repo = Arc::new(repo);
1332
1333 let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest)
1335 .context("Generating bootable EROFS image")?;
1336
1337 let fs = composefs_oci::image::create_filesystem(&*repo, &pull_result.config_digest, None)
1339 .context("Creating composefs filesystem for boot entry discovery")?;
1340 let entries =
1341 get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?;
1342
1343 let composefs_mnt_fd = repo
1344 .mount(&id.to_hex())
1345 .context("Failed to mount composefs image")?;
1346 let mounted_root = MountedImageRoot::new(composefs_mnt_fd, &root_setup.device_info)?;
1347
1348 let postfetch = PostFetchState::new(state, mounted_root.dir())?;
1349
1350 let boot_uuid = root_setup
1351 .get_boot_uuid()?
1352 .or(root_setup.rootfs_uuid.as_deref())
1353 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1354
1355 if cfg!(target_arch = "s390x") {
1356 crate::bootloader::install_via_zipl(
1358 &root_setup.device_info.require_single_root()?,
1359 boot_uuid,
1360 )?;
1361 } else if postfetch.detected_bootloader == Bootloader::Grub {
1362 crate::bootloader::install_via_bootupd(
1363 &root_setup.device_info,
1364 &root_setup.physical_root_path,
1365 &state.config_opts,
1366 None,
1367 )?;
1368 } else {
1369 crate::bootloader::install_systemd_boot(
1370 &mounted_root,
1371 &state.config_opts,
1372 get_secureboot_keys(mounted_root.dir(), BOOTC_AUTOENROLL_PATH)?,
1373 )?;
1374 }
1375
1376 let Some(entry) = entries.iter().next() else {
1377 anyhow::bail!("No boot entries!");
1378 };
1379
1380 let boot_type = BootType::from(entry);
1381
1382 let repo = Arc::try_unwrap(repo).map_err(|_| {
1384 anyhow::anyhow!(
1385 "BUG: Arc<Repository> still has other references after boot image generation"
1386 )
1387 })?;
1388
1389 let boot_digest = match boot_type {
1390 BootType::Bls => setup_composefs_bls_boot(
1391 BootSetupType::Setup((&root_setup, &state, &postfetch)),
1392 repo,
1393 &id,
1394 entry,
1395 mounted_root.dir(),
1396 )?,
1397 BootType::Uki => setup_composefs_uki_boot(
1398 BootSetupType::Setup((&root_setup, &state, &postfetch)),
1399 repo,
1400 &id,
1401 entries,
1402 )?,
1403 };
1404
1405 write_composefs_state(
1406 &root_setup.physical_root_path,
1407 &id,
1408 &crate::spec::ImageReference::from(state.target_imgref.clone()),
1409 None,
1410 boot_type,
1411 boot_digest,
1412 &pull_result.manifest_digest.to_string(),
1413 allow_missing_fsverity,
1414 )
1415 .await?;
1416
1417 Ok(())
1418}
1419
1420#[cfg(test)]
1421mod tests {
1422 use super::*;
1423
1424 #[test]
1425 fn test_type1_filename_generation() {
1426 let filename =
1428 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1429 assert_eq!(filename, "bootc_fedora-41.20251125.0-1.conf");
1430
1431 let primary =
1433 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1434 let secondary =
1435 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1436 assert_eq!(primary, "bootc_fedora-41.20251125.0-1.conf");
1437 assert_eq!(secondary, "bootc_fedora-41.20251125.0-0.conf");
1438
1439 let filename =
1441 type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1442 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1443
1444 let filename =
1446 type1_entry_conf_file_name("my-custom-os", "1.0.0", FILENAME_PRIORITY_PRIMARY);
1447 assert_eq!(filename, "bootc_my_custom_os-1.0.0-1.conf");
1448
1449 let filename = type1_entry_conf_file_name("rhel", "9.3.0", FILENAME_PRIORITY_SECONDARY);
1451 assert_eq!(filename, "bootc_rhel-9.3.0-0.conf");
1452 }
1453
1454 #[test]
1455 fn test_grub_filename_parsing() {
1456 let filename = type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", "1");
1465 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1466
1467 let without_ext = filename.strip_suffix(".conf").unwrap();
1473 let parts: Vec<&str> = without_ext.rsplitn(3, '-').collect();
1474 assert_eq!(parts.len(), 3);
1475 assert_eq!(parts[0], "1"); assert_eq!(parts[1], "41.20251125.0"); assert_eq!(parts[2], "bootc_fedora_coreos"); }
1479
1480 #[test]
1481 fn test_sort_keys() {
1482 let primary = primary_sort_key("fedora");
1484 let secondary = secondary_sort_key("fedora");
1485
1486 assert_eq!(primary, "bootc-fedora-0");
1487 assert_eq!(secondary, "bootc-fedora-1");
1488
1489 assert!(primary < secondary);
1491
1492 let primary_coreos = primary_sort_key("fedora-coreos");
1494 assert_eq!(primary_coreos, "bootc-fedora-coreos-0");
1495 }
1496
1497 #[test]
1498 fn test_filename_sorting_grub_style() {
1499 let primary =
1503 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1504 let secondary =
1505 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1506
1507 assert!(
1509 primary > secondary,
1510 "Primary should sort before secondary in descending order"
1511 );
1512
1513 let newer =
1515 type1_entry_conf_file_name("fedora", "42.20251125.0", FILENAME_PRIORITY_PRIMARY);
1516 let older =
1517 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1518
1519 assert!(
1521 newer > older,
1522 "Newer version should sort before older in descending order"
1523 );
1524
1525 let fedora = type1_entry_conf_file_name("fedora", "41.0", FILENAME_PRIORITY_PRIMARY);
1527 let rhel = type1_entry_conf_file_name("rhel", "9.0", FILENAME_PRIORITY_PRIMARY);
1528
1529 assert!(
1531 rhel > fedora,
1532 "RHEL should sort before Fedora in descending order"
1533 );
1534 }
1535}