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