From 8b56e8feba454f8539e0bbebb86b134e04162455 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Wed, 23 Jul 2025 10:42:32 +0530 Subject: [PATCH 01/18] install/composefs: Move UKI write logic to bootc Instead of using `write_boot_simple` from composefs-rs, have custom logic in bootc to write UKIs and Grub menuentries Signed-off-by: Johan-Liebert1 --- crates/lib/src/deploy.rs | 4 +-- crates/lib/src/install.rs | 59 ++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index aa5a7c659..7daa4f8c5 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -767,7 +767,7 @@ pub(crate) fn rollback_composefs_uki(current: &BootEntry, rollback: &BootEntry) // Shouldn't really happen anyhow::bail!("Verity not found for rollback deployment") }; - usr_cfg.write(get_user_config(&verity).as_bytes())?; + usr_cfg.write(get_user_config(todo!(), &verity).as_bytes())?; let verity = if let Some(composefs) = ¤t.composefs { composefs.verity.clone() @@ -775,7 +775,7 @@ pub(crate) fn rollback_composefs_uki(current: &BootEntry, rollback: &BootEntry) // Shouldn't really happen anyhow::bail!("Verity not found for booted deployment") }; - usr_cfg.write(get_user_config(&verity).as_bytes())?; + usr_cfg.write(get_user_config(todo!(), &verity).as_bytes())?; Ok(()) } diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index eb747c2ac..dbeec343f 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -14,6 +14,7 @@ mod osbuild; pub(crate) mod osconfig; use std::collections::HashMap; +use std::fmt::write; use std::fs::create_dir_all; use std::io::{Read, Write}; use std::os::fd::{AsFd, AsRawFd}; @@ -50,8 +51,7 @@ use ostree_ext::composefs::{ util::Sha256Digest, }; use ostree_ext::composefs_boot::{ - bootloader::BootEntry as ComposefsBootEntry, - write_boot::write_boot_simple as composefs_write_boot_simple, BootOps, + bootloader::BootEntry as ComposefsBootEntry, cmdline::get_cmdline_composefs, uki, BootOps, }; use ostree_ext::composefs_oci::{ image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull, @@ -69,8 +69,8 @@ use ostree_ext::{ use rustix::fs::FileTypeExt; use rustix::fs::MetadataExt as _; use rustix::path::Arg; -use serde::{Deserialize, Serialize}; use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; #[cfg(feature = "install-to-disk")] use self::baseline::InstallBlockDeviceOpts; @@ -269,7 +269,9 @@ impl TryFrom<&str> for BootType { match value { "bls" => Ok(Self::Bls), "uki" => Ok(Self::Uki), - unrecognized => Err(anyhow::anyhow!("Unrecognized boot option: '{unrecognized}'")), + unrecognized => Err(anyhow::anyhow!( + "Unrecognized boot option: '{unrecognized}'" + )), } } } @@ -1706,10 +1708,11 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { Ok((esp.node, esp.uuid)) } -pub(crate) fn get_user_config(uki_id: &str) -> String { +pub(crate) fn get_user_config(boot_label: &String, uki_id: &str) -> String { + // TODO: Full EFI path here let s = format!( r#" -menuentry "Fedora Bootc UKI: ({uki_id})" {{ +menuentry "{boot_label}: ({uki_id})" {{ insmod fat insmod chain search --no-floppy --set=root --fs-uuid "${{EFI_PART_UUID}}" @@ -1779,16 +1782,34 @@ pub(crate) fn setup_composefs_uki_boot( .args([&PathBuf::from(&esp_device), &mounted_esp.clone()]) .run()?; - composefs_write_boot_simple( - &repo, - entry, - &id, - false, - &mounted_esp, - None, - Some(&id.to_hex()), - &[], - )?; + let boot_label = match entry { + ComposefsBootEntry::Type1(..) => todo!(), + ComposefsBootEntry::UsrLibModulesUki(..) => todo!(), + ComposefsBootEntry::UsrLibModulesVmLinuz(..) => todo!(), + + ComposefsBootEntry::Type2(type2_entry) => { + let uki = read_file(&type2_entry.file, &repo).context("Reading UKI")?; + let cmdline = uki::get_cmdline(&uki).context("Getting UKI cmdline")?; + let (composefs_cmdline, _) = get_cmdline_composefs::(cmdline)?; + + let boot_label = uki::get_boot_label(&uki).context("Getting UKI boot label")?; + + if composefs_cmdline != *id { + anyhow::bail!( + "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {id:?})" + ); + } + + // Write the UKI to ESP + let efi_linux = mounted_esp.join("EFI/Linux"); + create_dir_all(&efi_linux).context("Creating EFI/Linux")?; + + let final_uki_path = efi_linux.join(format!("{}.efi", id.to_hex())); + std::fs::write(final_uki_path, uki).context("Writing UKI to final path")?; + + boot_label + } + }; Task::new("Unmounting ESP", "umount") .arg(&mounted_esp) @@ -1826,7 +1847,7 @@ pub(crate) fn setup_composefs_uki_boot( .with_context(|| format!("Opening {user_cfg_name}"))?; usr_cfg.write_all(efi_uuid_source.as_bytes())?; - usr_cfg.write_all(get_user_config(&id.to_hex()).as_bytes())?; + usr_cfg.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; // root_path here will be /sysroot for entry in std::fs::read_dir(root_path.join(STATE_DIR_RELATIVE))? { @@ -1836,7 +1857,7 @@ pub(crate) fn setup_composefs_uki_boot( // SAFETY: Deployment file name shouldn't containg non UTF-8 chars let depl_file_name = depl_file_name.to_string_lossy(); - usr_cfg.write_all(get_user_config(&depl_file_name).as_bytes())?; + usr_cfg.write_all(get_user_config(&boot_label, &depl_file_name).as_bytes())?; } return Ok(()); @@ -1868,7 +1889,7 @@ pub(crate) fn setup_composefs_uki_boot( .with_context(|| format!("Opening {user_cfg_name}"))?; usr_cfg.write_all(efi_uuid_source.as_bytes())?; - usr_cfg.write_all(get_user_config(&id.to_hex()).as_bytes())?; + usr_cfg.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; Ok(()) } From 734f43dc050ed6a13d91d0fa92f7e53fd786fa96 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Wed, 23 Jul 2025 19:40:45 +0530 Subject: [PATCH 02/18] install: Add cmdline kargs to RootSetup Signed-off-by: Johan-Liebert1 --- crates/lib/src/install/baseline.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index cc5cd0cae..6bff7ee58 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -427,6 +427,7 @@ pub(crate) fn install_create_rootfs( .flatten() .chain([rootarg, RW_KARG.to_string()].into_iter()) .chain(bootarg) + .chain(state.config_opts.karg.clone().into_iter().flatten()) .collect::>(); bootc_mount::mount(&rootdev, &physical_root_path)?; From b0e43a89a8b153bcdce5bfaa23f4226eaf570780 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Wed, 23 Jul 2025 20:25:14 +0530 Subject: [PATCH 03/18] install/composefs: Use atomic writes Use `atomic_write` from `cap_std` crate Get rid of `opeanat` crate Call `fsync` after write to disk Signed-off-by: Johan-Liebert1 --- crates/lib/Cargo.toml | 2 - crates/lib/src/deploy.rs | 52 ++++++++++----- crates/lib/src/install.rs | 135 ++++++++++++++++++++++++-------------- 3 files changed, 118 insertions(+), 71 deletions(-) diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 56a02faa0..e87a2a87a 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -56,8 +56,6 @@ tini = "1.3.0" comfy-table = "7.1.1" thiserror = { workspace = true } canon-json = { workspace = true } -openat = "0.1.21" -openat-ext = "0.2.3" [dev-dependencies] similar-asserts = { workspace = true } diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 7daa4f8c5..21c7b6bd7 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -22,18 +22,17 @@ use ostree_ext::ostree::Deployment; use ostree_ext::ostree::{self, Sysroot}; use ostree_ext::sysroot::SysrootLock; use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten; +use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; use crate::bls_config::{parse_bls_config, BLSConfig}; use crate::install::{get_efi_uuid_source, get_user_config, BootType}; use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep}; use crate::spec::ImageReference; -use crate::spec::{BootOrder, HostSpec, BootEntry}; +use crate::spec::{BootEntry, BootOrder, HostSpec}; use crate::status::{composefs_deployment_status, labels_of_config}; use crate::store::Storage; use crate::utils::async_task_with_spinner; -use openat_ext::OpenatDirExt; - // TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc"; @@ -743,7 +742,6 @@ pub(crate) async fn stage( Ok(()) } - #[context("Rolling back UKI")] pub(crate) fn rollback_composefs_uki(current: &BootEntry, rollback: &BootEntry) -> Result<()> { let user_cfg_name = "grub2/user.cfg.staged"; @@ -782,7 +780,8 @@ pub(crate) fn rollback_composefs_uki(current: &BootEntry, rollback: &BootEntry) /// Filename for `loader/entries` const CURRENT_ENTRIES: &str = "entries"; -const ROLLBACK_ENTRIES: &str = "entries.staged"; +const STAGED_ENTRIES: &str = "entries.staged"; +const ROLLBACK_ENTRIES: &str = STAGED_ENTRIES; #[context("Getting boot entries")] pub(crate) fn get_sorted_boot_entries(ascending: bool) -> Result> { @@ -833,33 +832,50 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { let dir_path = PathBuf::from(format!("/sysroot/boot/loader/{ROLLBACK_ENTRIES}")); create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?; + let rollback_entries_dir = + cap_std::fs::Dir::open_ambient_dir(&dir_path, cap_std::ambient_authority()) + .with_context(|| format!("Opening {dir_path:?}"))?; + // Write the BLS configs in there for cfg in all_configs { let file_name = format!("bootc-composefs-{}.conf", cfg.version); - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .open(dir_path.join(&file_name)) - .with_context(|| format!("Opening {file_name}"))?; - - file.write_all(cfg.to_string().as_bytes()) + rollback_entries_dir + .atomic_write(&file_name, cfg.to_string().as_bytes()) .with_context(|| format!("Writing to {file_name}"))?; } + // Should we sync after every write? + fsync( + rollback_entries_dir + .reopen_as_ownedfd() + .with_context(|| format!("Reopening {dir_path:?} as owned fd"))?, + ) + .with_context(|| format!("fsync {dir_path:?}"))?; + // Atomically exchange "entries" <-> "entries.rollback" - let dir = openat::Dir::open("/sysroot/boot/loader").context("Opening loader dir")?; + let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority()) + .context("Opening loader dir")?; tracing::debug!("Atomically exchanging for {ROLLBACK_ENTRIES} and {CURRENT_ENTRIES}"); - dir.local_exchange(ROLLBACK_ENTRIES, CURRENT_ENTRIES) - .context("local exchange")?; + renameat_with( + &dir, + ROLLBACK_ENTRIES, + &dir, + CURRENT_ENTRIES, + RenameFlags::EXCHANGE, + ) + .context("renameat")?; tracing::debug!("Removing {ROLLBACK_ENTRIES}"); - dir.remove_all(ROLLBACK_ENTRIES) - .context("Removing entries.rollback")?; + rustix::fs::unlinkat(&dir, ROLLBACK_ENTRIES, AtFlags::REMOVEDIR).context("unlinkat")?; tracing::debug!("Syncing to disk"); - dir.syncfs().context("syncfs")?; + fsync( + dir.reopen_as_ownedfd() + .with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?, + ) + .context("fsync")?; Ok(()) } diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index dbeec343f..f0f8478c7 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -14,7 +14,6 @@ mod osbuild; pub(crate) mod osconfig; use std::collections::HashMap; -use std::fmt::write; use std::fs::create_dir_all; use std::io::{Read, Write}; use std::os::fd::{AsFd, AsRawFd}; @@ -1640,25 +1639,36 @@ pub(crate) fn setup_composefs_bls_boot( let path = boot_dir.join(&id_hex); create_dir_all(&path)?; - let vmlinuz_path = path.join("vmlinuz"); - let initrd_path = path.join("initrd"); + let entries_dir = + cap_std::fs::Dir::open_ambient_dir(&path, cap_std::ambient_authority()) + .with_context(|| format!("Opening {path}"))?; - std::fs::write( - &vmlinuz_path, - read_file(&usr_lib_modules_vmlinuz.vmlinuz, &repo).context("Reading vmlinuz")?, - ) - .context("Writing vmlinuz to path")?; + entries_dir + .atomic_write( + "vmlinuz", + read_file(&usr_lib_modules_vmlinuz.vmlinuz, &repo) + .context("Reading vmlinuz")?, + ) + .context("Writing vmlinuz to path")?; if let Some(initramfs) = &usr_lib_modules_vmlinuz.initramfs { - std::fs::write( - &initrd_path, - read_file(initramfs, &repo).context("Reading initramfs")?, - ) - .context("Writing initrd to path")?; + entries_dir + .atomic_write( + "initrd", + read_file(initramfs, &repo).context("Reading initrd")?, + ) + .context("Writing initrd to path")?; } else { anyhow::bail!("initramfs not found"); }; + // Can't call fsync on O_PATH fds, so re-open it as a non O_PATH fd + let owned_fd = entries_dir + .reopen_as_ownedfd() + .context("Reopen as owned fd")?; + + rustix::fs::fsync(owned_fd).context("fsync")?; + BLSConfig { title: Some(id_hex.clone()), version: 1, @@ -1682,18 +1692,27 @@ pub(crate) fn setup_composefs_bls_boot( create_dir_all(&entries_path).with_context(|| format!("Creating {:?}", entries_path))?; - std::fs::write( - entries_path.join(format!("bootc-composefs-{}.conf", bls_config.version)), + let loader_entries_dir = + cap_std::fs::Dir::open_ambient_dir(&entries_path, cap_std::ambient_authority()) + .with_context(|| format!("Opening {entries_path}"))?; + + loader_entries_dir.atomic_write( + format!("bootc-composefs-{}.conf", bls_config.version), bls_config.to_string().as_bytes(), )?; if let Some(booted_bls) = booted_bls { - std::fs::write( - entries_path.join(format!("bootc-composefs-{}.conf", booted_bls.version)), + loader_entries_dir.atomic_write( + format!("bootc-composefs-{}.conf", booted_bls.version), booted_bls.to_string().as_bytes(), )?; } + let owned_loader_entries_fd = loader_entries_dir + .reopen_as_ownedfd() + .context("Reopening as owned fd")?; + rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?; + Ok(()) } @@ -1801,11 +1820,23 @@ pub(crate) fn setup_composefs_uki_boot( } // Write the UKI to ESP - let efi_linux = mounted_esp.join("EFI/Linux"); - create_dir_all(&efi_linux).context("Creating EFI/Linux")?; + let efi_linux_path = mounted_esp.join("EFI/Linux"); + create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?; + + let efi_linux = + cap_std::fs::Dir::open_ambient_dir(&efi_linux_path, cap_std::ambient_authority()) + .with_context(|| format!("Opening {efi_linux_path:?}"))?; - let final_uki_path = efi_linux.join(format!("{}.efi", id.to_hex())); - std::fs::write(final_uki_path, uki).context("Writing UKI to final path")?; + efi_linux + .atomic_write(format!("{}.efi", id.to_hex()), uki) + .context("Writing UKI")?; + + rustix::fs::fsync( + efi_linux + .reopen_as_ownedfd() + .context("Reopening as owned fd")?, + ) + .context("fsync")?; boot_label } @@ -1830,24 +1861,24 @@ pub(crate) fn setup_composefs_uki_boot( let efi_uuid_source = get_efi_uuid_source(); let user_cfg_name = if is_upgrade { - "grub2/user.cfg.staged" + "user.cfg.staged" } else { - "grub2/user.cfg" + "user.cfg" }; - let user_cfg_path = boot_dir.join(user_cfg_name); + + let grub_dir = + cap_std::fs::Dir::open_ambient_dir(boot_dir.join("grub2"), cap_std::ambient_authority()) + .context("opening boot/grub2")?; // Iterate over all available deployments, and generate a menuentry for each // // TODO: We might find a staged deployment here if is_upgrade { - let mut usr_cfg = std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(user_cfg_path) - .with_context(|| format!("Opening {user_cfg_name}"))?; + let mut buffer = vec![]; - usr_cfg.write_all(efi_uuid_source.as_bytes())?; - usr_cfg.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; + // Shouldn't really fail so no context here + buffer.write_all(efi_uuid_source.as_bytes())?; + buffer.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; // root_path here will be /sysroot for entry in std::fs::read_dir(root_path.join(STATE_DIR_RELATIVE))? { @@ -1857,39 +1888,41 @@ pub(crate) fn setup_composefs_uki_boot( // SAFETY: Deployment file name shouldn't containg non UTF-8 chars let depl_file_name = depl_file_name.to_string_lossy(); - usr_cfg.write_all(get_user_config(&boot_label, &depl_file_name).as_bytes())?; + buffer.write_all(get_user_config(&boot_label, &depl_file_name).as_bytes())?; } + grub_dir + .atomic_write(user_cfg_name, buffer) + .with_context(|| format!("Writing to {user_cfg_name}"))?; + + rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?; + return Ok(()); } - let efi_uuid_file_path = format!("grub2/{EFI_UUID_FILE}"); - // Open grub2/efiuuid.cfg and write the EFI partition fs-UUID in there // This will be sourced by grub2/user.cfg to be used for `--fs-uuid` - let mut efi_uuid_file = std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(boot_dir.join(&efi_uuid_file_path)) - .with_context(|| format!("Opening {efi_uuid_file_path}"))?; - let esp_uuid = Task::new("blkid for ESP UUID", "blkid") .args(["-s", "UUID", "-o", "value", &esp_device]) .read()?; - efi_uuid_file - .write_all(format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes()) - .with_context(|| format!("Writing to {efi_uuid_file_path}"))?; + grub_dir.atomic_write( + EFI_UUID_FILE, + format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes(), + )?; // Write to grub2/user.cfg - let mut usr_cfg = std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(user_cfg_path) - .with_context(|| format!("Opening {user_cfg_name}"))?; - - usr_cfg.write_all(efi_uuid_source.as_bytes())?; - usr_cfg.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; + let mut buffer = vec![]; + + // Shouldn't really fail so no context here + buffer.write_all(efi_uuid_source.as_bytes())?; + buffer.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; + + grub_dir + .atomic_write(user_cfg_name, buffer) + .with_context(|| format!("Writing to {user_cfg_name}"))?; + + rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?; Ok(()) } From cf70d5f72daa43571ab309aa0b7296cfcd178e49 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Thu, 24 Jul 2025 12:19:25 +0530 Subject: [PATCH 04/18] cli/composefs: Change composefs options Remove `--boot` option as we can get it from the image itself. Allow `--insecure` option to `--composefs-native` to make fsverity validation optional in case the filesystem does not support it. Signed-off-by: Johan-Liebert1 --- crates/lib/src/install.rs | 100 ++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index f0f8478c7..86a458fff 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -288,8 +288,9 @@ impl From<&ComposefsBootEntry> for BootType { #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallComposefsOpts { - #[clap(long, value_enum, default_value_t)] - pub(crate) boot: BootType, + #[clap(long, default_value_t)] + #[serde(default)] + pub(crate) insecure: bool, } #[cfg(feature = "install-to-disk")] @@ -317,9 +318,11 @@ pub(crate) struct InstallToDiskOpts { pub(crate) via_loopback: bool, #[clap(long)] + #[serde(default)] pub(crate) composefs_native: bool, #[clap(flatten)] + #[serde(flatten)] pub(crate) composefs_opts: InstallComposefsOpts, } @@ -608,17 +611,12 @@ impl FromStr for MountSpec { impl InstallToDiskOpts { pub(crate) fn validate(&self) -> Result<()> { if !self.composefs_native { - // Reject using --boot without --composefs - if self.composefs_opts.boot != BootType::default() { - anyhow::bail!("--boot must not be provided without --composefs"); + // Reject using --insecure without --composefs + if self.composefs_opts.insecure != false { + anyhow::bail!("--insecure must not be provided without --composefs"); } } - // Can't add kargs to UKI - if self.composefs_opts.boot == BootType::Uki && self.config_opts.karg.is_some() { - anyhow::bail!("Cannot pass kargs to UKI"); - } - Ok(()) } } @@ -1592,7 +1590,7 @@ pub fn read_file( pub(crate) enum BootSetupType<'a> { /// For initial setup, i.e. install to-disk - Setup(&'a RootSetup), + Setup((&'a RootSetup, &'a State)), /// For `bootc upgrade` Upgrade, } @@ -1608,10 +1606,18 @@ pub(crate) fn setup_composefs_bls_boot( let id_hex = id.to_hex(); let (root_path, cmdline_refs) = match setup_type { - BootSetupType::Setup(root_setup) => { + BootSetupType::Setup((root_setup, state)) => { // root_setup.kargs has [root=UUID=, "rw"] let mut cmdline_options = String::from(root_setup.kargs.join(" ")); - cmdline_options.push_str(&format!(" composefs={id_hex}")); + + match &state.composefs_options { + Some(opt) if opt.insecure => { + cmdline_options.push_str(&format!(" composefs=?{id_hex}")); + } + None | Some(..) => { + cmdline_options.push_str(&format!(" composefs={id_hex}")); + } + }; (root_setup.physical_root_path.clone(), cmdline_options) } @@ -1766,8 +1772,14 @@ pub(crate) fn setup_composefs_uki_boot( id: &Sha256HashValue, entry: ComposefsBootEntry, ) -> Result<()> { - let (root_path, esp_device) = match setup_type { - BootSetupType::Setup(root_setup) => { + let (root_path, esp_device, is_insecure_from_opts) = match setup_type { + BootSetupType::Setup((root_setup, state)) => { + if let Some(v) = &state.config_opts.karg { + if v.len() > 0 { + tracing::warn!("kargs passed for UKI will be ignored"); + } + } + let esp_part = root_setup .device_info .partitions @@ -1775,7 +1787,11 @@ pub(crate) fn setup_composefs_uki_boot( .find(|p| p.parttype.as_str() == ESP_GUID) .ok_or_else(|| anyhow!("ESP partition not found"))?; - (root_setup.physical_root_path.clone(), esp_part.node.clone()) + ( + root_setup.physical_root_path.clone(), + esp_part.node.clone(), + state.composefs_options.as_ref().map(|x| x.insecure), + ) } BootSetupType::Upgrade => { @@ -1788,7 +1804,7 @@ pub(crate) fn setup_composefs_uki_boot( anyhow::bail!("Could not find parent device for mountpoint /sysroot"); }; - (sysroot, get_esp_partition(&parent)?.0) + (sysroot, get_esp_partition(&parent)?.0, None) } }; @@ -1809,7 +1825,27 @@ pub(crate) fn setup_composefs_uki_boot( ComposefsBootEntry::Type2(type2_entry) => { let uki = read_file(&type2_entry.file, &repo).context("Reading UKI")?; let cmdline = uki::get_cmdline(&uki).context("Getting UKI cmdline")?; - let (composefs_cmdline, _) = get_cmdline_composefs::(cmdline)?; + let (composefs_cmdline, insecure) = get_cmdline_composefs::(cmdline)?; + + // If the UKI cmdline does not match what the user has passed as cmdline option + // NOTE: This will only be checked for new installs and now upgrades/switches + if let Some(is_insecure_from_opts) = is_insecure_from_opts { + match is_insecure_from_opts { + true => { + if !insecure { + tracing::warn!( + "--insecure passed as option but UKI cmdline does not support it" + ) + } + } + + false => { + if insecure { + tracing::warn!("UKI cmdline has composefs set as insecure") + } + } + } + } let boot_label = uki::get_boot_label(&uki).context("Getting UKI boot label")?; @@ -1991,17 +2027,21 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - anyhow::bail!("No boot entries!"); }; - let Some(composefs_opts) = &state.composefs_options else { - anyhow::bail!("Could not find options for composefs") - }; - - match composefs_opts.boot { - BootType::Bls => { - setup_composefs_bls_boot(BootSetupType::Setup(&root_setup), repo, &id, entry)? - } - BootType::Uki => { - setup_composefs_uki_boot(BootSetupType::Setup(&root_setup), repo, &id, entry)? - } + let boot_type = BootType::from(&entry); + + match boot_type { + BootType::Bls => setup_composefs_bls_boot( + BootSetupType::Setup((&root_setup, &state)), + repo, + &id, + entry, + )?, + BootType::Uki => setup_composefs_uki_boot( + BootSetupType::Setup((&root_setup, &state)), + repo, + &id, + entry, + )?, }; write_composefs_state( @@ -2013,7 +2053,7 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - signature: None, }, false, - composefs_opts.boot, + boot_type, )?; Ok(()) From ca9307df046331a1a757bc8602097b161ed80b83 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Sat, 26 Jul 2025 16:06:05 +0530 Subject: [PATCH 05/18] Write nom parser for Grub menuentries Signed-off-by: Johan-Liebert1 --- Cargo.lock | 48 +--- crates/lib/Cargo.toml | 1 + crates/lib/src/parsers/grub_menuconfig.rs | 254 ++++++++++++++++++++++ crates/lib/src/parsers/mod.rs | 1 + 4 files changed, 267 insertions(+), 37 deletions(-) create mode 100644 crates/lib/src/parsers/grub_menuconfig.rs create mode 100644 crates/lib/src/parsers/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3b1c2e749..9ea0f7a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,8 +245,7 @@ dependencies = [ "liboverdrop", "libsystemd", "linkme", - "openat", - "openat-ext", + "nom 8.0.0", "openssl", "ostree-ext", "regex", @@ -1409,7 +1408,7 @@ dependencies = [ "libc", "log", "nix 0.27.1", - "nom", + "nom 7.1.3", "once_cell", "serde", "sha2", @@ -1581,19 +1580,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.25.1" @@ -1630,6 +1616,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1707,27 +1702,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "openat" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95aa7c05907b3ebde2610d602f4ddd992145cc6a84493647c30396f30ba83abe" -dependencies = [ - "libc", -] - -[[package]] -name = "openat-ext" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf3e4baa7f516441f58373f58aaf6e91a5dfa2e2b50e68a0d313b082014c61d" -dependencies = [ - "libc", - "nix 0.23.2", - "openat", - "rand 0.8.5", -] - [[package]] name = "openssh-keys" version = "0.6.4" diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index e87a2a87a..3a4477abf 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -56,6 +56,7 @@ tini = "1.3.0" comfy-table = "7.1.1" thiserror = { workspace = true } canon-json = { workspace = true } +nom = "8.0.0" [dev-dependencies] similar-asserts = { workspace = true } diff --git a/crates/lib/src/parsers/grub_menuconfig.rs b/crates/lib/src/parsers/grub_menuconfig.rs new file mode 100644 index 000000000..13af85570 --- /dev/null +++ b/crates/lib/src/parsers/grub_menuconfig.rs @@ -0,0 +1,254 @@ +use std::fmt::Display; + +use nom::{ + bytes::complete::{tag, take_until}, + character::complete::multispace0, + error::{Error, ErrorKind, ParseError}, + multi::many0, + sequence::{delimited, preceded}, + Err, IResult, Parser, +}; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct MenuentryBody<'a> { + insmod: Vec<&'a str>, + chainloader: &'a str, + search: &'a str, + version: u8, + extra: Vec<(&'a str, &'a str)>, +} + +impl<'a> Display for MenuentryBody<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for insmod in &self.insmod { + writeln!(f, "insmod {}", insmod)?; + } + + writeln!(f, "search {}", self.search)?; + // writeln!(f, "version {}", self.version)?; + writeln!(f, "chainloader {}", self.chainloader)?; + + for (k, v) in &self.extra { + writeln!(f, "{k} {v}")?; + } + + Ok(()) + } +} + +impl<'a> From> for MenuentryBody<'a> { + fn from(vec: Vec<(&'a str, &'a str)>) -> Self { + let mut entry = Self { + insmod: vec![], + chainloader: "", + search: "", + version: 0, + extra: vec![], + }; + + for (key, value) in vec { + match key { + "insmod" => entry.insmod.push(value), + "chainloader" => entry.chainloader = value, + "search" => entry.search = value, + "set" => {} + _ => entry.extra.push((key, value)), + } + } + + return entry; + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct MenuEntry<'a> { + pub(crate) title: &'a str, + pub(crate) body: MenuentryBody<'a>, +} + +impl<'a> Display for MenuEntry<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "menuentry \"{}\" {{", self.title)?; + write!(f, "{}", self.body)?; + writeln!(f, "}}") + } +} + +pub fn take_until_balanced_allow_nested( + opening_bracket: char, + closing_bracket: char, +) -> impl Fn(&str) -> IResult<&str, &str> { + move |i: &str| { + let mut index = 0; + let mut bracket_counter = 0; + + while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket, '\\'][..]) { + index += n; + let mut characters = i[index..].chars(); + + match characters.next().unwrap_or_default() { + c if c == '\\' => { + // Skip '\' + index += '\\'.len_utf8(); + // Skip char following '\' + let c = characters.next().unwrap_or_default(); + index += c.len_utf8(); + } + + c if c == opening_bracket => { + bracket_counter += 1; + index += opening_bracket.len_utf8(); + } + + c if c == closing_bracket => { + bracket_counter -= 1; + index += closing_bracket.len_utf8(); + } + + // Should not happen + _ => unreachable!(), + }; + + // We found the unmatched closing bracket. + if bracket_counter == -1 { + // Don't consume it as we'll "tag" it afterwards + index -= closing_bracket.len_utf8(); + return Ok((&i[index..], &i[0..index])); + }; + } + + if bracket_counter == 0 { + Ok(("", i)) + } else { + Err(Err::Error(Error::from_error_kind(i, ErrorKind::TakeUntil))) + } + } +} + +fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry> { + let (input, _) = take_until("menuentry")(input)?; // skip irrelevant prefix + let (input, _) = tag("menuentry").parse(input)?; + + // Skip the whitespace after "menuentry" + let (input, _) = multispace0.parse(input)?; + // Eat up the title + let (input, title) = delimited(tag("\""), take_until("\""), tag("\"")).parse(input)?; + + // Skip any whitespace after title + let (input, _) = multispace0.parse(input)?; + + // Eat up everything insde { .. } + let (input, body) = delimited( + tag("{"), + take_until_balanced_allow_nested('{', '}'), + tag("}"), + ) + .parse(input)?; + + let mut map = vec![]; + + for line in body.lines() { + let line = line.trim(); + + if line.is_empty() || line.starts_with('#') { + continue; + } + + if let Some((key, value)) = line.split_once(' ') { + map.push((key, value.trim())); + } + } + + Ok(( + input, + MenuEntry { + title, + body: MenuentryBody::from(map), + }, + )) +} + +#[rustfmt::skip] +fn parse_all(input: &str) -> IResult<&str, Vec> { + many0( + preceded( + multispace0, + parse_menuentry, + ) + ) + .parse(input) +} + +pub(crate) fn parse_grub_menuentry_file(contents: &str) -> anyhow::Result> { + let result = parse_all(&contents); + + return match result { + Ok((_, entries)) => Ok(entries), + Result::Err(_) => anyhow::bail!("Failed to parse grub menuentry"), + }; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_menuconfig_parser() { + let menuentry = r#" + if [ -f ${config_directory}/efiuuid.cfg ]; then + source ${config_directory}/efiuuid.cfg + fi + + # Skip this comment + + menuentry "Fedora 42: (Verity-42)" { + insmod fat + insmod chain + # This should also be skipped + search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}" + chainloader /EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi + } + + menuentry "Fedora 43: (Verity-43)" { + insmod fat + insmod chain + search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}" + chainloader /EFI/Linux/uki.efi + extra_field1 this is extra + extra_field2 this is also extra + } + "#; + + let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries"); + + let expected = vec![ + MenuEntry { + title: "Fedora 42: (Verity-42)", + body: MenuentryBody { + insmod: vec!["fat", "chain"], + search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"", + chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi", + version: 0, + extra: vec![], + }, + }, + MenuEntry { + title: "Fedora 43: (Verity-43)", + body: MenuentryBody { + insmod: vec!["fat", "chain"], + search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"", + chainloader: "/EFI/Linux/uki.efi", + version: 0, + extra: vec![ + ("extra_field1", "this is extra"), + ("extra_field2", "this is also extra") + ] + }, + }, + ]; + + println!("{}", expected[0]); + + assert_eq!(result, expected); + } +} diff --git a/crates/lib/src/parsers/mod.rs b/crates/lib/src/parsers/mod.rs new file mode 100644 index 000000000..ca3d0453a --- /dev/null +++ b/crates/lib/src/parsers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod grub_menuconfig; From c89efab8ba14683682093f6f730a696b05491783 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Sun, 27 Jul 2025 11:04:36 +0530 Subject: [PATCH 06/18] composefs/status: Read UKI entries to check for queued rollback Parse the Grub menuentry file, `boot/grub2/user.cfg` to get a list of bootable UKIs and figure out if a rollback is currently queued. Signed-off-by: Johan-Liebert1 --- crates/lib/src/deploy.rs | 12 ++++++++-- crates/lib/src/status.rs | 51 +++++++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 21c7b6bd7..56f4e997f 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -783,8 +783,16 @@ const CURRENT_ENTRIES: &str = "entries"; const STAGED_ENTRIES: &str = "entries.staged"; const ROLLBACK_ENTRIES: &str = STAGED_ENTRIES; +// Need str to store lifetime +pub(crate) fn get_sorted_uki_boot_entries<'a>(str: &'a mut String) -> Result>> { + let mut file = std::fs::File::open("/sysroot/boot/grub2/user.cfg")?; + file.read_to_string(str)?; + parse_grub_menuentry_file(str) +} + + #[context("Getting boot entries")] -pub(crate) fn get_sorted_boot_entries(ascending: bool) -> Result> { +pub(crate) fn get_sorted_bls_boot_entries(ascending: bool) -> Result> { let mut all_configs = vec![]; for entry in std::fs::read_dir(format!("/sysroot/boot/loader/{CURRENT_ENTRIES}"))? { @@ -819,7 +827,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { // After this: // all_configs[0] -> booted depl // all_configs[1] -> rollback depl - let mut all_configs = get_sorted_boot_entries(false)?; + let mut all_configs = get_sorted_bls_boot_entries(false)?; // Update the indicies so that they're swapped for (idx, cfg) in all_configs.iter_mut().enumerate() { diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index dc1d966ad..eabc36c1e 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -24,7 +24,8 @@ use ostree_ext::ostree; use tokio::io::AsyncReadExt; use crate::cli::OutputFormat; -use crate::deploy::get_sorted_boot_entries; +use crate::deploy::get_sorted_bls_boot_entries; +use crate::deploy::get_sorted_uki_boot_entries; use crate::install::BootType; use crate::install::ORIGIN_KEY_BOOT; use crate::install::ORIGIN_KEY_BOOT_TYPE; @@ -422,6 +423,9 @@ pub(crate) async fn composefs_deployment_status() -> Result { Err(e) => Err(e), }?; + // NOTE: This cannot work if we support both BLS and UKI at the same time + let mut boot_type: Option = None; + for depl in deployments { let depl = depl?; @@ -441,6 +445,21 @@ pub(crate) async fn composefs_deployment_status() -> Result { let boot_entry = boot_entry_from_composefs_deployment(ini, depl_file_name.to_string()).await?; + // SAFETY: boot_entry.composefs will always be present + let boot_type_from_origin = boot_entry.composefs.as_ref().unwrap().boot_type; + + match boot_type { + Some(current_type) => { + if current_type != boot_type_from_origin { + anyhow::bail!("Conflicting boot types") + } + } + + None => { + boot_type = Some(boot_type_from_origin); + } + }; + if depl.file_name() == booted_image_verity { host.spec.image = boot_entry.image.as_ref().map(|x| x.image.clone()); host.status.booted = Some(boot_entry); @@ -457,11 +476,31 @@ pub(crate) async fn composefs_deployment_status() -> Result { host.status.rollback = Some(boot_entry); } - host.status.rollback_queued = !get_sorted_boot_entries(false)? - .first() - .ok_or(anyhow::anyhow!("First boot entry not found"))? - .options - .contains(booted_image_verity); + // Shouldn't really happen, but for sanity nonetheless + let Some(boot_type) = boot_type else { + anyhow::bail!("Could not determine boot type"); + }; + + match boot_type { + BootType::Bls => { + host.status.rollback_queued = !get_sorted_bls_boot_entries(false)? + .first() + .ok_or(anyhow::anyhow!("First boot entry not found"))? + .options + .contains(booted_image_verity); + } + + BootType::Uki => { + let mut s = String::new(); + + host.status.rollback_queued = !get_sorted_uki_boot_entries(&mut s)? + .first() + .ok_or(anyhow::anyhow!("First boot entry not found"))? + .body + .chainloader + .contains(booted_image_verity); + } + }; if host.status.rollback_queued { host.spec.boot_order = BootOrder::Rollback From 07ad2d30f8926ebd4ff013cedf66fad2250a42bc Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Sun, 27 Jul 2025 12:51:02 +0530 Subject: [PATCH 07/18] parser/grub: Use String instead of &str Returning a local reference to a `&str` is quite tricky with rust. Update `title` and `chainloader`, the two dynamic fields in the grub menuentry, to be `String` instead of `&str` Signed-off-by: Johan-Liebert1 --- crates/lib/src/parsers/grub_menuconfig.rs | 33 ++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/crates/lib/src/parsers/grub_menuconfig.rs b/crates/lib/src/parsers/grub_menuconfig.rs index 13af85570..0a1c3245c 100644 --- a/crates/lib/src/parsers/grub_menuconfig.rs +++ b/crates/lib/src/parsers/grub_menuconfig.rs @@ -12,7 +12,7 @@ use nom::{ #[derive(Debug, PartialEq, Eq)] pub(crate) struct MenuentryBody<'a> { insmod: Vec<&'a str>, - chainloader: &'a str, + pub(crate) chainloader: String, search: &'a str, version: u8, extra: Vec<(&'a str, &'a str)>, @@ -40,7 +40,7 @@ impl<'a> From> for MenuentryBody<'a> { fn from(vec: Vec<(&'a str, &'a str)>) -> Self { let mut entry = Self { insmod: vec![], - chainloader: "", + chainloader: "".into(), search: "", version: 0, extra: vec![], @@ -49,7 +49,7 @@ impl<'a> From> for MenuentryBody<'a> { for (key, value) in vec { match key { "insmod" => entry.insmod.push(value), - "chainloader" => entry.chainloader = value, + "chainloader" => entry.chainloader = value.into(), "search" => entry.search = value, "set" => {} _ => entry.extra.push((key, value)), @@ -62,7 +62,7 @@ impl<'a> From> for MenuentryBody<'a> { #[derive(Debug, PartialEq, Eq)] pub(crate) struct MenuEntry<'a> { - pub(crate) title: &'a str, + pub(crate) title: String, pub(crate) body: MenuentryBody<'a>, } @@ -74,6 +74,21 @@ impl<'a> Display for MenuEntry<'a> { } } +impl<'a> MenuEntry<'a> { + pub(crate) fn new(boot_label: &str, uki_id: &str) -> Self { + Self { + title: format!("{boot_label}: ({uki_id})"), + body: MenuentryBody { + insmod: vec!["fat", "chain"], + chainloader: format!("/EFI/Linux/{uki_id}.efi"), + search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"", + version: 0, + extra: vec![], + }, + } + } +} + pub fn take_until_balanced_allow_nested( opening_bracket: char, closing_bracket: char, @@ -162,7 +177,7 @@ fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry> { Ok(( input, MenuEntry { - title, + title: title.to_string(), body: MenuentryBody::from(map), }, )) @@ -223,21 +238,21 @@ mod test { let expected = vec![ MenuEntry { - title: "Fedora 42: (Verity-42)", + title: "Fedora 42: (Verity-42)".into(), body: MenuentryBody { insmod: vec!["fat", "chain"], search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"", - chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi", + chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi".into(), version: 0, extra: vec![], }, }, MenuEntry { - title: "Fedora 43: (Verity-43)", + title: "Fedora 43: (Verity-43)".into(), body: MenuentryBody { insmod: vec!["fat", "chain"], search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"", - chainloader: "/EFI/Linux/uki.efi", + chainloader: "/EFI/Linux/uki.efi".into(), version: 0, extra: vec![ ("extra_field1", "this is extra"), From 573705854444be41d8329e372542f1ea206e259f Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Sun, 27 Jul 2025 14:28:49 +0530 Subject: [PATCH 08/18] composefs/rollback: Handle UKI rollback We parse the grub menuentries, get the rollback deployment then perform the rollback, which basically consists of writing a new .staged menuentry file then atomically swapping the staged and the current menuentry. Rollback while there is a staged deployment is still to be handled. Signed-off-by: Johan-Liebert1 --- crates/lib/src/deploy.rs | 88 +++++++++++++++--------- crates/lib/src/install.rs | 50 ++++++-------- crates/lib/src/lib.rs | 1 + crates/ostree-ext/src/container_utils.rs | 2 +- 4 files changed, 79 insertions(+), 62 deletions(-) diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 56f4e997f..c96962f4b 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -3,8 +3,9 @@ //! Create a merged filesystem tree with the image and mounted configmaps. use std::collections::HashSet; +use std::fmt::Write as _; use std::fs::create_dir_all; -use std::io::{BufRead, Write}; +use std::io::{BufRead, Read, Write}; use std::path::PathBuf; use anyhow::Ok; @@ -25,10 +26,11 @@ use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; use crate::bls_config::{parse_bls_config, BLSConfig}; -use crate::install::{get_efi_uuid_source, get_user_config, BootType}; +use crate::install::{get_efi_uuid_source, BootType}; +use crate::parsers::grub_menuconfig::{parse_grub_menuentry_file, MenuEntry}; use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep}; use crate::spec::ImageReference; -use crate::spec::{BootEntry, BootOrder, HostSpec}; +use crate::spec::{BootOrder, HostSpec}; use crate::status::{composefs_deployment_status, labels_of_config}; use crate::store::Storage; use crate::utils::async_task_with_spinner; @@ -742,38 +744,59 @@ pub(crate) async fn stage( Ok(()) } +/// Filename for `loader/entries` +pub(crate) const USER_CFG: &str = "user.cfg"; +pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; +pub(crate) const USER_CFG_ROLLBACK: &str = USER_CFG_STAGED; + #[context("Rolling back UKI")] -pub(crate) fn rollback_composefs_uki(current: &BootEntry, rollback: &BootEntry) -> Result<()> { - let user_cfg_name = "grub2/user.cfg.staged"; - let user_cfg_path = PathBuf::from("/sysroot/boot").join(user_cfg_name); +pub(crate) fn rollback_composefs_uki() -> Result<()> { + let user_cfg_path = PathBuf::from("/sysroot/boot/grub2"); - let efi_uuid_source = get_efi_uuid_source(); + let mut str = String::new(); + let mut menuentries = + get_sorted_uki_boot_entries(&mut str).context("Getting UKI boot entries")?; - // TODO: Need to check if user.cfg.staged exists - let mut usr_cfg = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(user_cfg_path) - .with_context(|| format!("Opening {user_cfg_name}"))?; + // TODO(Johan-Liebert): Currently assuming there are only two deployments + assert!(menuentries.len() == 2); - usr_cfg.write(efi_uuid_source.as_bytes())?; + let (first, second) = menuentries.split_at_mut(1); + std::mem::swap(&mut first[0], &mut second[0]); - let verity = if let Some(composefs) = &rollback.composefs { - composefs.verity.clone() - } else { - // Shouldn't really happen - anyhow::bail!("Verity not found for rollback deployment") - }; - usr_cfg.write(get_user_config(todo!(), &verity).as_bytes())?; + let mut buffer = get_efi_uuid_source(); - let verity = if let Some(composefs) = ¤t.composefs { - composefs.verity.clone() - } else { - // Shouldn't really happen - anyhow::bail!("Verity not found for booted deployment") - }; - usr_cfg.write(get_user_config(todo!(), &verity).as_bytes())?; + for entry in menuentries { + write!(buffer, "{entry}")?; + } + + let entries_dir = + cap_std::fs::Dir::open_ambient_dir(&user_cfg_path, cap_std::ambient_authority()) + .with_context(|| format!("Opening {user_cfg_path:?}"))?; + + entries_dir + .atomic_write(USER_CFG_ROLLBACK, buffer) + .with_context(|| format!("Writing to {USER_CFG_ROLLBACK}"))?; + + tracing::debug!("Atomically exchanging for {USER_CFG_ROLLBACK} and {USER_CFG}"); + renameat_with( + &entries_dir, + USER_CFG_ROLLBACK, + &entries_dir, + USER_CFG, + RenameFlags::EXCHANGE, + ) + .context("renameat")?; + + tracing::debug!("Removing {USER_CFG_ROLLBACK}"); + rustix::fs::unlinkat(&entries_dir, USER_CFG_ROLLBACK, AtFlags::empty()).context("unlinkat")?; + + tracing::debug!("Syncing to disk"); + fsync( + entries_dir + .reopen_as_ownedfd() + .with_context(|| format!("Reopening {user_cfg_path:?} as owned fd"))?, + ) + .with_context(|| format!("fsync {user_cfg_path:?}"))?; Ok(()) } @@ -834,6 +857,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { cfg.version = idx as u32; } + // TODO(Johan-Liebert): Currently assuming there are only two deployments assert!(all_configs.len() == 2); // Write these @@ -849,7 +873,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { let file_name = format!("bootc-composefs-{}.conf", cfg.version); rollback_entries_dir - .atomic_write(&file_name, cfg.to_string().as_bytes()) + .atomic_write(&file_name, cfg.to_string()) .with_context(|| format!("Writing to {file_name}"))?; } @@ -876,7 +900,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { .context("renameat")?; tracing::debug!("Removing {ROLLBACK_ENTRIES}"); - rustix::fs::unlinkat(&dir, ROLLBACK_ENTRIES, AtFlags::REMOVEDIR).context("unlinkat")?; + rustix::fs::unlinkat(&dir, ROLLBACK_ENTRIES, AtFlags::empty()).context("unlinkat")?; tracing::debug!("Syncing to disk"); fsync( @@ -920,7 +944,7 @@ pub(crate) async fn composefs_rollback() -> Result<()> { match rollback_composefs_entry.boot_type { BootType::Bls => rollback_composefs_bls(), - BootType::Uki => rollback_composefs_uki(&host.status.booted.unwrap(), &rollback_status), + BootType::Uki => rollback_composefs_uki(), }?; Ok(()) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 86a458fff..609d3208f 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -76,8 +76,12 @@ use self::baseline::InstallBlockDeviceOpts; use crate::bls_config::{parse_bls_config, BLSConfig}; use crate::boundimage::{BoundImage, ResolvedBoundImage}; use crate::containerenv::ContainerExecutionInfo; -use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult}; +use crate::deploy::{ + get_sorted_uki_boot_entries, prepare_for_pull, pull_from_prepared, PreparedImportMeta, + PreparedPullResult, USER_CFG, USER_CFG_STAGED, +}; use crate::lsm; +use crate::parsers::grub_menuconfig::MenuEntry; use crate::progress_jsonl::ProgressWriter; use crate::spec::ImageReference; use crate::store::Storage; @@ -1733,22 +1737,6 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { Ok((esp.node, esp.uuid)) } -pub(crate) fn get_user_config(boot_label: &String, uki_id: &str) -> String { - // TODO: Full EFI path here - let s = format!( - r#" -menuentry "{boot_label}: ({uki_id})" {{ - insmod fat - insmod chain - search --no-floppy --set=root --fs-uuid "${{EFI_PART_UUID}}" - chainloader /EFI/Linux/{uki_id}.efi -}} -"# - ); - - return s; -} - /// Contains the EFP's filesystem UUID. Used by grub pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg"; @@ -1897,9 +1885,9 @@ pub(crate) fn setup_composefs_uki_boot( let efi_uuid_source = get_efi_uuid_source(); let user_cfg_name = if is_upgrade { - "user.cfg.staged" + USER_CFG_STAGED } else { - "user.cfg" + USER_CFG }; let grub_dir = @@ -1914,17 +1902,17 @@ pub(crate) fn setup_composefs_uki_boot( // Shouldn't really fail so no context here buffer.write_all(efi_uuid_source.as_bytes())?; - buffer.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; - - // root_path here will be /sysroot - for entry in std::fs::read_dir(root_path.join(STATE_DIR_RELATIVE))? { - let entry = entry?; + buffer.write_all( + MenuEntry::new(&boot_label, &id.to_hex()) + .to_string() + .as_bytes(), + )?; - let depl_file_name = entry.file_name(); - // SAFETY: Deployment file name shouldn't containg non UTF-8 chars - let depl_file_name = depl_file_name.to_string_lossy(); + let mut str_buf = String::new(); + let entries = get_sorted_uki_boot_entries(&mut str_buf)?; - buffer.write_all(get_user_config(&boot_label, &depl_file_name).as_bytes())?; + for entry in entries { + buffer.write_all(entry.to_string().as_bytes())?; } grub_dir @@ -1952,7 +1940,11 @@ pub(crate) fn setup_composefs_uki_boot( // Shouldn't really fail so no context here buffer.write_all(efi_uuid_source.as_bytes())?; - buffer.write_all(get_user_config(&boot_label, &id.to_hex()).as_bytes())?; + buffer.write_all( + MenuEntry::new(&boot_label, &id.to_hex()) + .to_string() + .as_bytes(), + )?; grub_dir .atomic_write(user_cfg_name, buffer) diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index ad3550ec8..13ffa69a9 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -28,6 +28,7 @@ mod store; mod task; mod utils; mod bls_config; +pub(crate) mod parsers; #[cfg(feature = "docgen")] mod docgen; diff --git a/crates/ostree-ext/src/container_utils.rs b/crates/ostree-ext/src/container_utils.rs index 631a0c524..a06920815 100644 --- a/crates/ostree-ext/src/container_utils.rs +++ b/crates/ostree-ext/src/container_utils.rs @@ -78,7 +78,7 @@ pub fn ostree_booted() -> io::Result { } -/// Returns true if the system appears to have been booted with composefs. +/// Returns true if the system appears to have been booted with composefs without ostree. pub fn composefs_booted() -> io::Result { let cmdline = std::fs::read_to_string("/proc/cmdline")?; Ok(cmdline.contains("composefs=")) From 2c63434e0d757e388776ab42862df5e424a82bad Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Mon, 28 Jul 2025 10:44:50 +0530 Subject: [PATCH 09/18] composefs/state: Use atomic writes for origin and staged deployment files Signed-off-by: Johan-Liebert1 --- crates/lib/src/bls_config.rs | 4 +-- crates/lib/src/deploy.rs | 1 - crates/lib/src/install.rs | 32 +++++++++++++++--------- crates/lib/src/lib.rs | 4 +-- crates/lib/src/spec.rs | 1 - crates/lib/src/status.rs | 8 ++++-- crates/ostree-ext/src/container_utils.rs | 1 - 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/crates/lib/src/bls_config.rs b/crates/lib/src/bls_config.rs index 6b64be52d..cceb3a7fe 100644 --- a/crates/lib/src/bls_config.rs +++ b/crates/lib/src/bls_config.rs @@ -1,7 +1,7 @@ -use serde::{Deserialize, Deserializer}; +use anyhow::Result; use serde::de::Error; +use serde::{Deserialize, Deserializer}; use std::collections::HashMap; -use anyhow::Result; #[derive(Debug, Deserialize, Eq)] pub(crate) struct BLSConfig { diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index c96962f4b..3e85abb60 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -813,7 +813,6 @@ pub(crate) fn get_sorted_uki_boot_entries<'a>(str: &'a mut String) -> Result Result> { let mut all_configs = vec![]; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 609d3208f..f93348c35 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2052,7 +2052,8 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - } pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; -pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_PATH: &str = "/run/composefs/staged-deployment"; +/// File created in /run/composefs to record a staged-deployment +pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; /// Relative to /sysroot pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; @@ -2094,25 +2095,32 @@ pub(crate) fn write_composefs_state( .section(ORIGIN_KEY_BOOT) .item(ORIGIN_KEY_BOOT_TYPE, boot_type); - let mut origin_file = - std::fs::File::create(state_path.join(format!("{}.origin", deployment_id.to_hex()))) - .context("Failed to open .origin file")?; + let state_dir = cap_std::fs::Dir::open_ambient_dir(&state_path, cap_std::ambient_authority()) + .context("Opening state dir")?; - origin_file - .write(config.to_string().as_bytes()) + state_dir + .atomic_write( + format!("{}.origin", deployment_id.to_hex()), + config.to_string().as_bytes(), + ) .context("Falied to write to .origin file")?; if staged { std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR) .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; - let mut file = std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(COMPOSEFS_STAGED_DEPLOYMENT_PATH) - .context("Opening staged-deployment file")?; + let staged_depl_dir = cap_std::fs::Dir::open_ambient_dir( + COMPOSEFS_TRANSIENT_STATE_DIR, + cap_std::ambient_authority(), + ) + .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; - file.write_all(deployment_id.to_hex().as_bytes())?; + staged_depl_dir + .atomic_write( + COMPOSEFS_STAGED_DEPLOYMENT_FNAME, + deployment_id.to_hex().as_bytes(), + ) + .with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?; } Ok(()) diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 13ffa69a9..f99f977cc 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -4,6 +4,7 @@ //! to provide a fully "container native" tool for using //! bootable container images. +mod bls_config; mod boundimage; pub mod cli; pub(crate) mod deploy; @@ -18,6 +19,7 @@ pub(crate) mod kargs; mod lints; mod lsm; pub(crate) mod metadata; +pub(crate) mod parsers; mod podman; mod progress_jsonl; mod reboot; @@ -27,8 +29,6 @@ mod status; mod store; mod task; mod utils; -mod bls_config; -pub(crate) mod parsers; #[cfg(feature = "docgen")] mod docgen; diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index 4d945a3ca..e3a8c12b3 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -165,7 +165,6 @@ pub struct BootEntryOstree { pub deploy_serial: u32, } - /// A bootable entry #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index eabc36c1e..703c1cf12 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -29,7 +29,9 @@ use crate::deploy::get_sorted_uki_boot_entries; use crate::install::BootType; use crate::install::ORIGIN_KEY_BOOT; use crate::install::ORIGIN_KEY_BOOT_TYPE; -use crate::install::{COMPOSEFS_STAGED_DEPLOYMENT_PATH, STATE_DIR_RELATIVE}; +use crate::install::{ + COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, STATE_DIR_RELATIVE, +}; use crate::spec::ImageStatus; use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType}; use crate::spec::{ImageReference, ImageSignature}; @@ -412,7 +414,9 @@ pub(crate) async fn composefs_deployment_status() -> Result { let mut host = Host::new(host_spec); - let staged_deployment_id = match std::fs::File::open(COMPOSEFS_STAGED_DEPLOYMENT_PATH) { + let staged_deployment_id = match std::fs::File::open(format!( + "{COMPOSEFS_TRANSIENT_STATE_DIR}/{COMPOSEFS_STAGED_DEPLOYMENT_FNAME}" + )) { Ok(mut f) => { let mut s = String::new(); f.read_to_string(&mut s)?; diff --git a/crates/ostree-ext/src/container_utils.rs b/crates/ostree-ext/src/container_utils.rs index a06920815..3a33ae322 100644 --- a/crates/ostree-ext/src/container_utils.rs +++ b/crates/ostree-ext/src/container_utils.rs @@ -77,7 +77,6 @@ pub fn ostree_booted() -> io::Result { Path::new(&format!("/{OSTREE_BOOTED}")).try_exists() } - /// Returns true if the system appears to have been booted with composefs without ostree. pub fn composefs_booted() -> io::Result { let cmdline = std::fs::read_to_string("/proc/cmdline")?; From 425b4398e58218a5c5bc00ec2c785d338b65f532 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Mon, 28 Jul 2025 15:04:07 +0530 Subject: [PATCH 10/18] composefs/boot/bls: Handle duplicate VMLinuz + Initrd If two deployments have the same VMLinuz + Initrd then, we can use the same binaries for both the deployments. Before writing the BLS entries to disk we calculate the SHA256Sum of VMLinuz + Initrd combo, then test if any other deployment has the same SHA256Sum for the binaries. Store the hash in the origin file under `boot -> hash` for future lookups. Signed-off-by: Johan-Liebert1 --- crates/lib/src/cli.rs | 39 +++++-- crates/lib/src/install.rs | 216 ++++++++++++++++++++++++++++++-------- 2 files changed, 203 insertions(+), 52 deletions(-) diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index f055549f5..2e34eb2fc 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -798,13 +798,29 @@ async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { }; let boot_type = BootType::from(&entry); + let mut boot_digest = None; match boot_type { - BootType::Bls => setup_composefs_bls_boot(BootSetupType::Upgrade, repo, &id, entry), - BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry), - }?; + BootType::Bls => { + boot_digest = Some(setup_composefs_bls_boot( + BootSetupType::Upgrade, + repo, + &id, + entry, + )?) + } - write_composefs_state(&Utf8PathBuf::from("/sysroot"), id, imgref, true, boot_type)?; + BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry)?, + }; + + write_composefs_state( + &Utf8PathBuf::from("/sysroot"), + id, + imgref, + true, + boot_type, + boot_digest, + )?; Ok(()) } @@ -966,11 +982,19 @@ async fn switch_composefs(opts: SwitchOpts) -> Result<()> { }; let boot_type = BootType::from(&entry); + let mut boot_digest = None; match boot_type { - BootType::Bls => setup_composefs_bls_boot(BootSetupType::Upgrade, repo, &id, entry), - BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry), - }?; + BootType::Bls => { + boot_digest = Some(setup_composefs_bls_boot( + BootSetupType::Upgrade, + repo, + &id, + entry, + )?) + } + BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry)?, + }; write_composefs_state( &Utf8PathBuf::from("/sysroot"), @@ -978,6 +1002,7 @@ async fn switch_composefs(opts: SwitchOpts) -> Result<()> { &target_imgref, true, boot_type, + boot_digest, )?; Ok(()) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index f93348c35..b1db6d776 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -49,6 +49,7 @@ use ostree_ext::composefs::{ repository::Repository as ComposefsRepository, util::Sha256Digest, }; +use ostree_ext::composefs_boot::bootloader::UsrLibModulesVmlinuz; use ostree_ext::composefs_boot::{ bootloader::BootEntry as ComposefsBootEntry, cmdline::get_cmdline_composefs, uki, BootOps, }; @@ -1599,6 +1600,133 @@ pub(crate) enum BootSetupType<'a> { Upgrade, } +/// Compute SHA256Sum of VMlinuz + Initrd +/// +/// # Arguments +/// * entry - BootEntry containing VMlinuz and Initrd +/// * repo - The composefs repository +#[context("Computing boot digest")] +fn compute_boot_digest( + entry: &UsrLibModulesVmlinuz, + repo: &ComposefsRepository, +) -> Result { + let vmlinuz = read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?; + + let Some(initramfs) = &entry.initramfs else { + anyhow::bail!("initramfs not found"); + }; + + let initramfs = read_file(initramfs, &repo).context("Reading intird")?; + + let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256()) + .context("Creating hasher")?; + + hasher.update(&vmlinuz).context("hashing vmlinuz")?; + hasher.update(&initramfs).context("hashing initrd")?; + + let digest: &[u8] = &hasher.finish().context("Finishing digest")?; + + return Ok(hex::encode(digest)); +} + +/// Given the SHA256 sum of current VMlinuz + Initrd combo, find boot entry with the same SHA256Sum +/// +/// # Returns +/// Returns the verity of the deployment that has a boot digest same as the one passed in +#[context("Checking boot entry duplicates")] +fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result> { + let deployments = + cap_std::fs::Dir::open_ambient_dir(STATE_DIR_ABS, cap_std::ambient_authority()); + + let deployments = match deployments { + Ok(d) => d, + // The first ever deployment + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => anyhow::bail!(e), + }; + + let mut symlink_to: Option = None; + + for depl in deployments.entries()? { + let depl = depl?; + + let depl_file_name = depl.file_name(); + let depl_file_name = depl_file_name.as_str()?; + + let config = depl + .open_dir() + .with_context(|| format!("Opening {depl_file_name}"))? + .read_to_string(format!("{depl_file_name}.origin")) + .context("Reading origin file")?; + + let ini = tini::Ini::from_string(&config) + .with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?; + + match ini.get::(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST) { + Some(hash) => { + if hash == digest { + symlink_to = Some(depl_file_name.to_string()); + break; + } + } + + // No SHASum recorded in origin file + // `symlink_to` is already none, but being explicit here + None => symlink_to = None, + }; + } + + Ok(symlink_to) +} + +#[context("Writing BLS entries to disk")] +fn write_bls_boot_entries_to_disk( + boot_dir: &Utf8PathBuf, + deployment_id: &Sha256HashValue, + entry: &UsrLibModulesVmlinuz, + repo: &ComposefsRepository, +) -> Result<()> { + let id_hex = deployment_id.to_hex(); + + // Write the initrd and vmlinuz at /boot// + let path = boot_dir.join(&id_hex); + create_dir_all(&path)?; + + let entries_dir = cap_std::fs::Dir::open_ambient_dir(&path, cap_std::ambient_authority()) + .with_context(|| format!("Opening {path}"))?; + + entries_dir + .atomic_write( + "vmlinuz", + read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?, + ) + .context("Writing vmlinuz to path")?; + + let Some(initramfs) = &entry.initramfs else { + anyhow::bail!("initramfs not found"); + }; + + entries_dir + .atomic_write( + "initrd", + read_file(initramfs, &repo).context("Reading initrd")?, + ) + .context("Writing initrd to path")?; + + // Can't call fsync on O_PATH fds, so re-open it as a non O_PATH fd + let owned_fd = entries_dir + .reopen_as_ownedfd() + .context("Reopen as owned fd")?; + + rustix::fs::fsync(owned_fd).context("fsync")?; + + Ok(()) +} + +/// Sets up and writes BLS entries and binaries (VMLinuz + Initrd) to disk +/// +/// # Returns +/// Returns the SHA256Sum of VMLinuz + Initrd combo. Error if any #[context("Setting up BLS boot")] pub(crate) fn setup_composefs_bls_boot( setup_type: BootSetupType, @@ -1606,7 +1734,7 @@ pub(crate) fn setup_composefs_bls_boot( repo: ComposefsRepository, id: &Sha256HashValue, entry: ComposefsBootEntry, -) -> Result<()> { +) -> Result { let id_hex = id.to_hex(); let (root_path, cmdline_refs) = match setup_type { @@ -1639,58 +1767,38 @@ pub(crate) fn setup_composefs_bls_boot( let boot_dir = root_path.join("boot"); - let bls_config = match &entry { + let is_upgrade = matches!(setup_type, BootSetupType::Upgrade); + + let (bls_config, boot_digest) = match &entry { ComposefsBootEntry::Type1(..) => todo!(), ComposefsBootEntry::Type2(..) => todo!(), ComposefsBootEntry::UsrLibModulesUki(..) => todo!(), ComposefsBootEntry::UsrLibModulesVmLinuz(usr_lib_modules_vmlinuz) => { - // Write the initrd and vmlinuz at /boot// - let path = boot_dir.join(&id_hex); - create_dir_all(&path)?; - - let entries_dir = - cap_std::fs::Dir::open_ambient_dir(&path, cap_std::ambient_authority()) - .with_context(|| format!("Opening {path}"))?; - - entries_dir - .atomic_write( - "vmlinuz", - read_file(&usr_lib_modules_vmlinuz.vmlinuz, &repo) - .context("Reading vmlinuz")?, - ) - .context("Writing vmlinuz to path")?; - - if let Some(initramfs) = &usr_lib_modules_vmlinuz.initramfs { - entries_dir - .atomic_write( - "initrd", - read_file(initramfs, &repo).context("Reading initrd")?, - ) - .context("Writing initrd to path")?; - } else { - anyhow::bail!("initramfs not found"); - }; + let boot_digest = compute_boot_digest(usr_lib_modules_vmlinuz, &repo) + .context("Computing boot digest")?; - // Can't call fsync on O_PATH fds, so re-open it as a non O_PATH fd - let owned_fd = entries_dir - .reopen_as_ownedfd() - .context("Reopen as owned fd")?; - - rustix::fs::fsync(owned_fd).context("fsync")?; - - BLSConfig { + let mut bls_config = BLSConfig { title: Some(id_hex.clone()), version: 1, linux: format!("/boot/{id_hex}/vmlinuz"), initrd: format!("/boot/{id_hex}/initrd"), options: cmdline_refs, extra: HashMap::new(), + }; + + if let Some(symlink_to) = find_vmlinuz_initrd_duplicates(&boot_digest)? { + bls_config.linux = format!("/boot/{symlink_to}/vmlinuz"); + bls_config.initrd = format!("/boot/{symlink_to}/initrd"); + } else { + write_bls_boot_entries_to_disk(&boot_dir, id, usr_lib_modules_vmlinuz, &repo)?; } + + (bls_config, boot_digest) } }; - let (entries_path, booted_bls) = if matches!(setup_type, BootSetupType::Upgrade) { + let (entries_path, booted_bls) = if is_upgrade { let mut booted_bls = get_booted_bls()?; booted_bls.version = 0; // entries are sorted by their filename in reverse order @@ -1723,7 +1831,7 @@ pub(crate) fn setup_composefs_bls_boot( .context("Reopening as owned fd")?; rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?; - Ok(()) + Ok(boot_digest) } pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { @@ -2020,14 +2128,19 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - }; let boot_type = BootType::from(&entry); + let mut boot_digest: Option = None; match boot_type { - BootType::Bls => setup_composefs_bls_boot( - BootSetupType::Setup((&root_setup, &state)), - repo, - &id, - entry, - )?, + BootType::Bls => { + let digest = setup_composefs_bls_boot( + BootSetupType::Setup((&root_setup, &state)), + repo, + &id, + entry, + )?; + + boot_digest = Some(digest); + } BootType::Uki => setup_composefs_uki_boot( BootSetupType::Setup((&root_setup, &state)), repo, @@ -2046,6 +2159,7 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - }, false, boot_type, + boot_digest, )?; Ok(()) @@ -2054,11 +2168,16 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; /// File created in /run/composefs to record a staged-deployment pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; -/// Relative to /sysroot + +/// Absolute path to composefs-native state directory +pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy"; +/// Relative path to composefs-native state directory. Relative to /sysroot pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; pub(crate) const ORIGIN_KEY_BOOT: &str = "boot"; pub(crate) const ORIGIN_KEY_BOOT_TYPE: &str = "boot_type"; +/// Key to store the SHA256 sum of vmlinuz + initrd for a deployment +pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest"; /// Creates and populates /sysroot/state/deploy/image_id #[context("Writing composefs state")] @@ -2068,6 +2187,7 @@ pub(crate) fn write_composefs_state( imgref: &ImageReference, staged: bool, boot_type: BootType, + boot_digest: Option, ) -> Result<()> { let state_path = root_path.join(format!("{STATE_DIR_RELATIVE}/{}", deployment_id.to_hex())); @@ -2095,6 +2215,12 @@ pub(crate) fn write_composefs_state( .section(ORIGIN_KEY_BOOT) .item(ORIGIN_KEY_BOOT_TYPE, boot_type); + if let Some(boot_digest) = boot_digest { + config = config + .section(ORIGIN_KEY_BOOT) + .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest); + } + let state_dir = cap_std::fs::Dir::open_ambient_dir(&state_path, cap_std::ambient_authority()) .context("Opening state dir")?; From 5870f28a465f05a215421da3f812ab11091f9dc5 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Mon, 28 Jul 2025 15:26:11 +0530 Subject: [PATCH 11/18] parser/bls: `impl Display` for BLSConfig Signed-off-by: Johan-Liebert1 --- crates/lib/src/bls_config.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/lib/src/bls_config.rs b/crates/lib/src/bls_config.rs index cceb3a7fe..06f282da6 100644 --- a/crates/lib/src/bls_config.rs +++ b/crates/lib/src/bls_config.rs @@ -2,6 +2,7 @@ use anyhow::Result; use serde::de::Error; use serde::{Deserialize, Deserializer}; use std::collections::HashMap; +use std::fmt::Display; #[derive(Debug, Deserialize, Eq)] pub(crate) struct BLSConfig { @@ -34,24 +35,22 @@ impl Ord for BLSConfig { } } -impl BLSConfig { - pub(crate) fn to_string(&self) -> String { - let mut out = String::new(); - +impl Display for BLSConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(title) = &self.title { - out += &format!("title {}\n", title); + writeln!(f, "title {}", title)?; } - out += &format!("version {}\n", self.version); - out += &format!("linux {}\n", self.linux); - out += &format!("initrd {}\n", self.initrd); - out += &format!("options {}\n", self.options); + writeln!(f, "version {}", self.version)?; + writeln!(f, "linux {}", self.linux)?; + writeln!(f, "initrd {}", self.initrd)?; + writeln!(f, "options {}", self.options)?; for (key, value) in &self.extra { - out += &format!("{} {}\n", key, value); + writeln!(f, "{} {}", key, value)?; } - out + Ok(()) } } From 4ff4098624bc214aa047192308798b85b05b7afe Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Tue, 29 Jul 2025 10:08:59 +0530 Subject: [PATCH 12/18] lib/composefs: Centralize constants Centralize all constants in a separate file Signed-off-by: Johan-Liebert1 --- crates/lib/src/cli.rs | 4 +-- crates/lib/src/composefs_consts.rs | 36 ++++++++++++++++++++ crates/lib/src/deploy.rs | 34 +++++++++---------- crates/lib/src/install.rs | 42 +++++++++++------------- crates/lib/src/lib.rs | 1 + crates/lib/src/status.rs | 17 ++++++---- crates/lib/src/utils.rs | 8 +++++ crates/ostree-ext/src/container_utils.rs | 6 ---- 8 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 crates/lib/src/composefs_consts.rs diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 2e34eb2fc..03b4111c2 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -20,7 +20,7 @@ use ostree_container::store::PrepareResult; use ostree_ext::composefs::fsverity; use ostree_ext::composefs::fsverity::FsVerityHashValue; use ostree_ext::container as ostree_container; -use ostree_ext::container_utils::{composefs_booted, ostree_booted}; +use ostree_ext::container_utils::ostree_booted; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; use schemars::schema_for; @@ -36,7 +36,7 @@ use crate::progress_jsonl::{ProgressWriter, RawProgressFd}; use crate::spec::Host; use crate::spec::ImageReference; use crate::status::composefs_deployment_status; -use crate::utils::sigpolicy_from_opt; +use crate::utils::{composefs_booted, sigpolicy_from_opt}; /// Shared progress options #[derive(Debug, Parser, PartialEq, Eq)] diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs new file mode 100644 index 000000000..b7ff61ee9 --- /dev/null +++ b/crates/lib/src/composefs_consts.rs @@ -0,0 +1,36 @@ +/// composefs= paramter in kernel cmdline +pub const COMPOSEFS_CMDLINE: &str = "composefs="; +/// composefs=? paramter in kernel cmdline. The `?` signifies that the fs-verity validation is +/// optional in case the filesystem doesn't support it. +pub const COMPOSEFS_INSECURE_CMDLINE: &str = "composefs=?"; + +/// Directory to store transient state, such as staged deployemnts etc +pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; +/// File created in /run/composefs to record a staged-deployment +pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; + +/// Absolute path to composefs-native state directory +pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy"; +/// Relative path to composefs-native state directory. Relative to /sysroot +pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; + +/// Section in .origin file to store boot related metadata +pub(crate) const ORIGIN_KEY_BOOT: &str = "boot"; +/// Whether the deployment was booted with BLS or UKI +pub(crate) const ORIGIN_KEY_BOOT_TYPE: &str = "boot_type"; +/// Key to store the SHA256 sum of vmlinuz + initrd for a deployment +pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest"; + +/// Filename for `loader/entries` +pub(crate) const BOOT_LOADER_ENTRIES: &str = "entries"; +/// Filename for staged boot loader entries +pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged"; +/// Filename for rollback boot loader entries +pub(crate) const ROLLBACK_BOOT_LOADER_ENTRIES: &str = STAGED_BOOT_LOADER_ENTRIES; + +/// Filename for grub user config +pub(crate) const USER_CFG: &str = "user.cfg"; +/// Filename for staged grub user config +pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; +/// Filename for rollback grub user config +pub(crate) const USER_CFG_ROLLBACK: &str = USER_CFG_STAGED; diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 3e85abb60..5a849a530 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -26,6 +26,9 @@ use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; use crate::bls_config::{parse_bls_config, BLSConfig}; +use crate::composefs_consts::{ + BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK, +}; use crate::install::{get_efi_uuid_source, BootType}; use crate::parsers::grub_menuconfig::{parse_grub_menuentry_file, MenuEntry}; use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep}; @@ -744,11 +747,6 @@ pub(crate) async fn stage( Ok(()) } -/// Filename for `loader/entries` -pub(crate) const USER_CFG: &str = "user.cfg"; -pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; -pub(crate) const USER_CFG_ROLLBACK: &str = USER_CFG_STAGED; - #[context("Rolling back UKI")] pub(crate) fn rollback_composefs_uki() -> Result<()> { let user_cfg_path = PathBuf::from("/sysroot/boot/grub2"); @@ -801,14 +799,9 @@ pub(crate) fn rollback_composefs_uki() -> Result<()> { Ok(()) } -/// Filename for `loader/entries` -const CURRENT_ENTRIES: &str = "entries"; -const STAGED_ENTRIES: &str = "entries.staged"; -const ROLLBACK_ENTRIES: &str = STAGED_ENTRIES; - // Need str to store lifetime pub(crate) fn get_sorted_uki_boot_entries<'a>(str: &'a mut String) -> Result>> { - let mut file = std::fs::File::open("/sysroot/boot/grub2/user.cfg")?; + let mut file = std::fs::File::open(format!("/sysroot/boot/grub2/{USER_CFG}"))?; file.read_to_string(str)?; parse_grub_menuentry_file(str) } @@ -817,7 +810,7 @@ pub(crate) fn get_sorted_uki_boot_entries<'a>(str: &'a mut String) -> Result Result> { let mut all_configs = vec![]; - for entry in std::fs::read_dir(format!("/sysroot/boot/loader/{CURRENT_ENTRIES}"))? { + for entry in std::fs::read_dir(format!("/sysroot/boot/loader/{BOOT_LOADER_ENTRIES}"))? { let entry = entry?; let file_name = entry.file_name(); @@ -860,7 +853,9 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { assert!(all_configs.len() == 2); // Write these - let dir_path = PathBuf::from(format!("/sysroot/boot/loader/{ROLLBACK_ENTRIES}")); + let dir_path = PathBuf::from(format!( + "/sysroot/boot/loader/{ROLLBACK_BOOT_LOADER_ENTRIES}" + )); create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?; let rollback_entries_dir = @@ -888,18 +883,21 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority()) .context("Opening loader dir")?; - tracing::debug!("Atomically exchanging for {ROLLBACK_ENTRIES} and {CURRENT_ENTRIES}"); + tracing::debug!( + "Atomically exchanging for {ROLLBACK_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}" + ); renameat_with( &dir, - ROLLBACK_ENTRIES, + ROLLBACK_BOOT_LOADER_ENTRIES, &dir, - CURRENT_ENTRIES, + BOOT_LOADER_ENTRIES, RenameFlags::EXCHANGE, ) .context("renameat")?; - tracing::debug!("Removing {ROLLBACK_ENTRIES}"); - rustix::fs::unlinkat(&dir, ROLLBACK_ENTRIES, AtFlags::empty()).context("unlinkat")?; + tracing::debug!("Removing {ROLLBACK_BOOT_LOADER_ENTRIES}"); + rustix::fs::unlinkat(&dir, ROLLBACK_BOOT_LOADER_ENTRIES, AtFlags::empty()) + .context("unlinkat")?; tracing::debug!("Syncing to disk"); fsync( diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index b1db6d776..8babd295c 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -76,10 +76,16 @@ use serde::{Deserialize, Serialize}; use self::baseline::InstallBlockDeviceOpts; use crate::bls_config::{parse_bls_config, BLSConfig}; use crate::boundimage::{BoundImage, ResolvedBoundImage}; +use crate::composefs_consts::{ + BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE, + COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT, + ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, + STATE_DIR_RELATIVE, USER_CFG, USER_CFG_STAGED, +}; use crate::containerenv::ContainerExecutionInfo; use crate::deploy::{ get_sorted_uki_boot_entries, prepare_for_pull, pull_from_prepared, PreparedImportMeta, - PreparedPullResult, USER_CFG, USER_CFG_STAGED, + PreparedPullResult, }; use crate::lsm; use crate::parsers::grub_menuconfig::MenuEntry; @@ -1537,7 +1543,7 @@ async fn initialize_composefs_repository( rootfs_dir .create_dir_all("composefs") - .context("Creating dir 'composefs'")?; + .context("Creating dir composefs")?; let repo = open_composefs_repo(rootfs_dir)?; @@ -1552,7 +1558,10 @@ async fn initialize_composefs_repository( fn get_booted_bls() -> Result { let cmdline = crate::kernel::parse_cmdline()?; - let booted = cmdline.iter().find_map(|x| x.strip_prefix("composefs=")); + let booted = cmdline.iter().find_map(|x| { + x.strip_prefix(COMPOSEFS_INSECURE_CMDLINE) + .or_else(|| x.strip_prefix(COMPOSEFS_CMDLINE)) + }); let Some(booted) = booted else { anyhow::bail!("Failed to find composefs parameter in kernel cmdline"); @@ -1744,10 +1753,10 @@ pub(crate) fn setup_composefs_bls_boot( match &state.composefs_options { Some(opt) if opt.insecure => { - cmdline_options.push_str(&format!(" composefs=?{id_hex}")); + cmdline_options.push_str(&format!(" {COMPOSEFS_INSECURE_CMDLINE}{id_hex}")); } None | Some(..) => { - cmdline_options.push_str(&format!(" composefs={id_hex}")); + cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}{id_hex}")); } }; @@ -1759,7 +1768,7 @@ pub(crate) fn setup_composefs_bls_boot( vec![ format!("root=UUID={DPS_UUID}"), RW_KARG.to_string(), - format!("composefs={id_hex}"), + format!("{COMPOSEFS_CMDLINE}{id_hex}"), ] .join(" "), ), @@ -1803,9 +1812,12 @@ pub(crate) fn setup_composefs_bls_boot( booted_bls.version = 0; // entries are sorted by their filename in reverse order // This will be atomically renamed to 'loader/entries' on shutdown/reboot - (boot_dir.join("loader/entries.staged"), Some(booted_bls)) + ( + boot_dir.join(format!("loader/{STAGED_BOOT_LOADER_ENTRIES}")), + Some(booted_bls), + ) } else { - (boot_dir.join("loader/entries"), None) + (boot_dir.join(format!("loader/{BOOT_LOADER_ENTRIES}")), None) }; create_dir_all(&entries_path).with_context(|| format!("Creating {:?}", entries_path))?; @@ -2165,20 +2177,6 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - Ok(()) } -pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; -/// File created in /run/composefs to record a staged-deployment -pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; - -/// Absolute path to composefs-native state directory -pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy"; -/// Relative path to composefs-native state directory. Relative to /sysroot -pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; - -pub(crate) const ORIGIN_KEY_BOOT: &str = "boot"; -pub(crate) const ORIGIN_KEY_BOOT_TYPE: &str = "boot_type"; -/// Key to store the SHA256 sum of vmlinuz + initrd for a deployment -pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest"; - /// Creates and populates /sysroot/state/deploy/image_id #[context("Writing composefs state")] pub(crate) fn write_composefs_state( diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index f99f977cc..aeef113e9 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -7,6 +7,7 @@ mod bls_config; mod boundimage; pub mod cli; +mod composefs_consts; pub(crate) mod deploy; pub(crate) mod fsck; pub(crate) mod generator; diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index 703c1cf12..badb1683f 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -14,7 +14,6 @@ use ostree::glib; use ostree_container::OstreeImageReference; use ostree_ext::container as ostree_container; use ostree_ext::container::deploy::ORIGIN_CONTAINER; -use ostree_ext::container_utils::composefs_booted; use ostree_ext::container_utils::ostree_booted; use ostree_ext::containers_image_proxy; use ostree_ext::keyfileext::KeyFileExt; @@ -24,18 +23,18 @@ use ostree_ext::ostree; use tokio::io::AsyncReadExt; use crate::cli::OutputFormat; +use crate::composefs_consts::{ + COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, + COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE, +}; use crate::deploy::get_sorted_bls_boot_entries; use crate::deploy::get_sorted_uki_boot_entries; use crate::install::BootType; -use crate::install::ORIGIN_KEY_BOOT; -use crate::install::ORIGIN_KEY_BOOT_TYPE; -use crate::install::{ - COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, STATE_DIR_RELATIVE, -}; use crate::spec::ImageStatus; use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType}; use crate::spec::{ImageReference, ImageSignature}; use crate::store::{CachedImageStatus, ContainerImageStore, Storage}; +use crate::utils::composefs_booted; impl From for ImageSignature { fn from(sig: ostree_container::SignatureSource) -> Self { @@ -395,7 +394,11 @@ async fn boot_entry_from_composefs_deployment( #[context("Getting composefs deployment status")] pub(crate) async fn composefs_deployment_status() -> Result { let cmdline = crate::kernel::parse_cmdline()?; - let booted_image_verity = cmdline.iter().find_map(|x| x.strip_prefix("composefs=")); + + let booted_image_verity = cmdline.iter().find_map(|x| { + x.strip_prefix(COMPOSEFS_INSECURE_CMDLINE) + .or_else(|| x.strip_prefix(COMPOSEFS_CMDLINE)) + }); let Some(booted_image_verity) = booted_image_verity else { anyhow::bail!("Failed to find composefs parameter in kernel cmdline"); diff --git a/crates/lib/src/utils.rs b/crates/lib/src/utils.rs index a7ccb0687..073419c65 100644 --- a/crates/lib/src/utils.rs +++ b/crates/lib/src/utils.rs @@ -17,6 +17,14 @@ use ostree::glib; use ostree_ext::container::SignatureSource; use ostree_ext::ostree; +use crate::composefs_consts::{COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE}; + +/// Returns true if the system appears to have been booted with composefs without ostree. +pub fn composefs_booted() -> std::io::Result { + let cmdline = std::fs::read_to_string("/proc/cmdline")?; + Ok(cmdline.contains(COMPOSEFS_CMDLINE) || cmdline.contains(COMPOSEFS_INSECURE_CMDLINE)) +} + /// Try to look for keys injected by e.g. rpm-ostree requesting machine-local /// changes; if any are present, return `true`. pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool { diff --git a/crates/ostree-ext/src/container_utils.rs b/crates/ostree-ext/src/container_utils.rs index 3a33ae322..7737e986c 100644 --- a/crates/ostree-ext/src/container_utils.rs +++ b/crates/ostree-ext/src/container_utils.rs @@ -77,12 +77,6 @@ pub fn ostree_booted() -> io::Result { Path::new(&format!("/{OSTREE_BOOTED}")).try_exists() } -/// Returns true if the system appears to have been booted with composefs without ostree. -pub fn composefs_booted() -> io::Result { - let cmdline = std::fs::read_to_string("/proc/cmdline")?; - Ok(cmdline.contains("composefs=")) -} - /// Returns true if the target root appears to have been booted via ostree. pub fn is_ostree_booted_in(rootfs: &Dir) -> io::Result { rootfs.try_exists(OSTREE_BOOTED) From 09d77a5a2fe30b116d419f59406c0b9d87806e34 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Tue, 29 Jul 2025 11:09:11 +0530 Subject: [PATCH 13/18] composefs/state: Name state directory `default` Instead of `/sysroot/state/os/fedora` use `/sysroot/state/os/default` as the default state directory. Signed-off-by: Johan-Liebert1 --- crates/lib/src/composefs_consts.rs | 2 ++ crates/lib/src/install.rs | 16 +++++++----- crates/lib/src/utils.rs | 42 +++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index b7ff61ee9..e11318956 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -13,6 +13,8 @@ pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy"; /// Relative path to composefs-native state directory. Relative to /sysroot pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; +/// Relative path to the shared 'var' directory. Relative to /sysroot +pub(crate) const SHARED_VAR_PATH: &str = "state/os/default/var"; /// Section in .origin file to store boot related metadata pub(crate) const ORIGIN_KEY_BOOT: &str = "boot"; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 8babd295c..d112d3ff3 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -79,8 +79,8 @@ use crate::boundimage::{BoundImage, ResolvedBoundImage}; use crate::composefs_consts::{ BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT, - ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, - STATE_DIR_RELATIVE, USER_CFG, USER_CFG_STAGED, + ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH, STAGED_BOOT_LOADER_ENTRIES, + STATE_DIR_ABS, STATE_DIR_RELATIVE, USER_CFG, USER_CFG_STAGED, }; use crate::containerenv::ContainerExecutionInfo; use crate::deploy::{ @@ -93,7 +93,7 @@ use crate::progress_jsonl::ProgressWriter; use crate::spec::ImageReference; use crate::store::Storage; use crate::task::Task; -use crate::utils::sigpolicy_from_opt; +use crate::utils::{path_relative_to, sigpolicy_from_opt}; use bootc_mount::{inspect_filesystem, Filesystem}; /// The toplevel boot directory @@ -2192,11 +2192,15 @@ pub(crate) fn write_composefs_state( create_dir_all(state_path.join("etc/upper"))?; create_dir_all(state_path.join("etc/work"))?; - let actual_var_path = root_path.join(format!("state/os/fedora/var")); + let actual_var_path = root_path.join(SHARED_VAR_PATH); create_dir_all(&actual_var_path)?; - symlink(Path::new("../../os/fedora/var"), state_path.join("var")) - .context("Failed to create symlink for /var")?; + symlink( + path_relative_to(state_path.as_std_path(), actual_var_path.as_std_path()) + .context("Getting var symlink path")?, + state_path.join("var"), + ) + .context("Failed to create symlink for /var")?; let ImageReference { image: image_name, diff --git a/crates/lib/src/utils.rs b/crates/lib/src/utils.rs index 073419c65..96a0e8453 100644 --- a/crates/lib/src/utils.rs +++ b/crates/lib/src/utils.rs @@ -1,8 +1,9 @@ -use std::future::Future; use std::io::Write; use std::os::fd::BorrowedFd; +use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; +use std::{future::Future, path::Component}; use anyhow::{Context, Result}; use bootc_utils::CommandRunExt; @@ -194,6 +195,28 @@ pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String { format!("{image}@{digest}") } +/// Computes a relative path from `from` to `to`. +/// +/// Both `from` and `to` must be absolute paths. +pub(crate) fn path_relative_to(from: &Path, to: &Path) -> Result { + if !from.is_absolute() || !to.is_absolute() { + anyhow::bail!("Paths must be absolute"); + } + + let from = from.components().collect::>(); + let to = to.components().collect::>(); + + let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count(); + + let up = std::iter::repeat(Component::ParentDir).take(from.len() - common); + + let mut final_path = PathBuf::new(); + final_path.extend(up); + final_path.extend(&to[common..]); + + return Ok(final_path); +} + #[cfg(test)] mod tests { use super::*; @@ -231,4 +254,21 @@ mod tests { SignatureSource::ContainerPolicyAllowInsecure ); } + + #[test] + fn test_relative_path() { + let from = Path::new("/sysroot/state/deploy/image_id"); + let to = Path::new("/sysroot/state/os/default/var"); + + assert_eq!( + path_relative_to(from, to).unwrap(), + PathBuf::from("../../os/default/var") + ); + assert_eq!( + path_relative_to(&Path::new("state/deploy"), to) + .unwrap_err() + .to_string(), + "Paths must be absolute" + ); + } } From 4c31ddfc0828f9d35b1ad35498fc48a6a93f978a Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Tue, 29 Jul 2025 11:37:26 +0530 Subject: [PATCH 14/18] parser/bls: Add tests for bls parser Signed-off-by: Johan-Liebert1 --- crates/lib/src/bls_config.rs | 87 ------------ crates/lib/src/deploy.rs | 2 +- crates/lib/src/install.rs | 2 +- crates/lib/src/lib.rs | 1 - crates/lib/src/parsers/bls_config.rs | 201 +++++++++++++++++++++++++++ crates/lib/src/parsers/mod.rs | 1 + 6 files changed, 204 insertions(+), 90 deletions(-) delete mode 100644 crates/lib/src/bls_config.rs create mode 100644 crates/lib/src/parsers/bls_config.rs diff --git a/crates/lib/src/bls_config.rs b/crates/lib/src/bls_config.rs deleted file mode 100644 index 06f282da6..000000000 --- a/crates/lib/src/bls_config.rs +++ /dev/null @@ -1,87 +0,0 @@ -use anyhow::Result; -use serde::de::Error; -use serde::{Deserialize, Deserializer}; -use std::collections::HashMap; -use std::fmt::Display; - -#[derive(Debug, Deserialize, Eq)] -pub(crate) struct BLSConfig { - pub(crate) title: Option, - #[serde(deserialize_with = "deserialize_version")] - pub(crate) version: u32, - pub(crate) linux: String, - pub(crate) initrd: String, - pub(crate) options: String, - - #[serde(flatten)] - pub(crate) extra: HashMap, -} - -impl PartialEq for BLSConfig { - fn eq(&self, other: &Self) -> bool { - self.version == other.version - } -} - -impl PartialOrd for BLSConfig { - fn partial_cmp(&self, other: &Self) -> Option { - self.version.partial_cmp(&other.version) - } -} - -impl Ord for BLSConfig { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.version.cmp(&other.version) - } -} - -impl Display for BLSConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(title) = &self.title { - writeln!(f, "title {}", title)?; - } - - writeln!(f, "version {}", self.version)?; - writeln!(f, "linux {}", self.linux)?; - writeln!(f, "initrd {}", self.initrd)?; - writeln!(f, "options {}", self.options)?; - - for (key, value) in &self.extra { - writeln!(f, "{} {}", key, value)?; - } - - Ok(()) - } -} - -fn deserialize_version<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s: Option = Option::deserialize(deserializer)?; - - match s { - Some(s) => Ok(s.parse::().map_err(D::Error::custom)?), - None => Err(D::Error::custom("Version not found")), - } -} - -pub(crate) fn parse_bls_config(input: &str) -> Result { - let mut map = HashMap::new(); - - for line in input.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - - if let Some((key, value)) = line.split_once(' ') { - map.insert(key.to_string(), value.trim().to_string()); - } - } - - let value = serde_json::to_value(map)?; - let parsed: BLSConfig = serde_json::from_value(value)?; - - Ok(parsed) -} diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 5a849a530..48bb5e205 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -25,11 +25,11 @@ use ostree_ext::sysroot::SysrootLock; use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; -use crate::bls_config::{parse_bls_config, BLSConfig}; use crate::composefs_consts::{ BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK, }; use crate::install::{get_efi_uuid_source, BootType}; +use crate::parsers::bls_config::{parse_bls_config, BLSConfig}; use crate::parsers::grub_menuconfig::{parse_grub_menuentry_file, MenuEntry}; use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep}; use crate::spec::ImageReference; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index d112d3ff3..650f82454 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -74,7 +74,6 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "install-to-disk")] use self::baseline::InstallBlockDeviceOpts; -use crate::bls_config::{parse_bls_config, BLSConfig}; use crate::boundimage::{BoundImage, ResolvedBoundImage}; use crate::composefs_consts::{ BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE, @@ -88,6 +87,7 @@ use crate::deploy::{ PreparedPullResult, }; use crate::lsm; +use crate::parsers::bls_config::{parse_bls_config, BLSConfig}; use crate::parsers::grub_menuconfig::MenuEntry; use crate::progress_jsonl::ProgressWriter; use crate::spec::ImageReference; diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index aeef113e9..66ce17516 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -4,7 +4,6 @@ //! to provide a fully "container native" tool for using //! bootable container images. -mod bls_config; mod boundimage; pub mod cli; mod composefs_consts; diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs new file mode 100644 index 000000000..b5cbff240 --- /dev/null +++ b/crates/lib/src/parsers/bls_config.rs @@ -0,0 +1,201 @@ +use anyhow::Result; +use serde::de::Error; +use serde::{Deserialize, Deserializer}; +use std::collections::HashMap; +use std::fmt::Display; + +#[derive(Debug, Deserialize, Eq)] +pub(crate) struct BLSConfig { + pub(crate) title: Option, + #[serde(deserialize_with = "deserialize_version")] + pub(crate) version: u32, + pub(crate) linux: String, + pub(crate) initrd: String, + pub(crate) options: String, + + #[serde(flatten)] + pub(crate) extra: HashMap, +} + +impl PartialEq for BLSConfig { + fn eq(&self, other: &Self) -> bool { + self.version == other.version + } +} + +impl PartialOrd for BLSConfig { + fn partial_cmp(&self, other: &Self) -> Option { + self.version.partial_cmp(&other.version) + } +} + +impl Ord for BLSConfig { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.version.cmp(&other.version) + } +} + +impl Display for BLSConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(title) = &self.title { + writeln!(f, "title {}", title)?; + } + + writeln!(f, "version {}", self.version)?; + writeln!(f, "linux {}", self.linux)?; + writeln!(f, "initrd {}", self.initrd)?; + writeln!(f, "options {}", self.options)?; + + for (key, value) in &self.extra { + writeln!(f, "{} {}", key, value)?; + } + + Ok(()) + } +} + +fn deserialize_version<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + + match s { + Some(s) => Ok(s.parse::().map_err(D::Error::custom)?), + None => Err(D::Error::custom("Version not found")), + } +} + +pub(crate) fn parse_bls_config(input: &str) -> Result { + let mut map = HashMap::new(); + + for line in input.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + if let Some((key, value)) = line.split_once(' ') { + map.insert(key.to_string(), value.trim().to_string()); + } + } + + let value = serde_json::to_value(map)?; + let parsed: BLSConfig = serde_json::from_value(value)?; + + Ok(parsed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_valid_bls_config() -> Result<()> { + let input = r#" + title Fedora 42.20250623.3.1 (CoreOS) + version 2 + linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10 + initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img + options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6 + custom1 value1 + custom2 value2 + "#; + + let config = parse_bls_config(input)?; + + assert_eq!( + config.title, + Some("Fedora 42.20250623.3.1 (CoreOS)".to_string()) + ); + assert_eq!(config.version, 2); + assert_eq!(config.linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"); + assert_eq!(config.initrd, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"); + assert_eq!(config.options, "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6"); + assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string())); + assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string())); + + Ok(()) + } + + #[test] + fn test_parse_missing_version() { + let input = r#" + title Fedora + linux /vmlinuz + initrd /initramfs.img + options root=UUID=xyz ro quiet + "#; + + let parsed = parse_bls_config(input); + assert!(parsed.is_err()); + } + + #[test] + fn test_parse_invalid_version_format() { + let input = r#" + title Fedora + version not_an_int + linux /vmlinuz + initrd /initramfs.img + options root=UUID=abc composefs=some-uuid + "#; + + let parsed = parse_bls_config(input); + assert!(parsed.is_err()); + } + + #[test] + fn test_display_output() -> Result<()> { + let input = r#" + title Test OS + version 10 + linux /boot/vmlinuz + initrd /boot/initrd.img + options root=UUID=abc composefs=some-uuid + foo bar + "#; + + let config = parse_bls_config(input)?; + let output = format!("{}", config); + let mut output_lines = output.lines(); + + assert_eq!(output_lines.next().unwrap(), "title Test OS"); + assert_eq!(output_lines.next().unwrap(), "version 10"); + assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz"); + assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img"); + assert_eq!( + output_lines.next().unwrap(), + "options root=UUID=abc composefs=some-uuid" + ); + assert_eq!(output_lines.next().unwrap(), "foo bar"); + + Ok(()) + } + + #[test] + fn test_ordering() -> Result<()> { + let config1 = parse_bls_config( + r#" + title Entry 1 + version 3 + linux /vmlinuz-3 + initrd /initrd-3 + options opt1 + "#, + )?; + + let config2 = parse_bls_config( + r#" + title Entry 2 + version 5 + linux /vmlinuz-5 + initrd /initrd-5 + options opt2 + "#, + )?; + + assert!(config1 < config2); + Ok(()) + } +} diff --git a/crates/lib/src/parsers/mod.rs b/crates/lib/src/parsers/mod.rs index ca3d0453a..e3640c8ef 100644 --- a/crates/lib/src/parsers/mod.rs +++ b/crates/lib/src/parsers/mod.rs @@ -1 +1,2 @@ +pub(crate) mod bls_config; pub(crate) mod grub_menuconfig; From 1e73eaa5d41fd2bb297c039d809068829007eacd Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Thu, 31 Jul 2025 11:35:19 +0530 Subject: [PATCH 15/18] install/composefs/uki: Write only staged + booted menuentry on upgrade Instaed of writing all present menuentries, only write the menuentry for switch/upgrade and the menuentry for the currently booted deployment. Signed-off-by: Johan-Liebert1 --- crates/lib/src/install.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 650f82454..a0211f51a 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2031,9 +2031,10 @@ pub(crate) fn setup_composefs_uki_boot( let mut str_buf = String::new(); let entries = get_sorted_uki_boot_entries(&mut str_buf)?; - for entry in entries { - buffer.write_all(entry.to_string().as_bytes())?; - } + // Write out only the currently booted entry, which should be the very first one + // Even if we have booted into the second menuentry "boot entry", the default will be the + // first one + buffer.write_all(entries[0].to_string().as_bytes())?; grub_dir .atomic_write(user_cfg_name, buffer) From 782a3d6bc5b6701059f7836860805e3a78caf9ad Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Thu, 31 Jul 2025 11:44:01 +0530 Subject: [PATCH 16/18] rollback/composefs: Print whether we are reverting the queued rollback Signed-off-by: Johan-Liebert1 --- crates/lib/src/deploy.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 48bb5e205..41f31af24 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -944,6 +944,12 @@ pub(crate) async fn composefs_rollback() -> Result<()> { BootType::Uki => rollback_composefs_uki(), }?; + if reverting { + println!("Next boot: current deployment"); + } else { + println!("Next boot: rollback deployment"); + } + Ok(()) } From f0ffe64f2a91281e91a6359719018ce14c7f16a8 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 1 Aug 2025 15:10:58 +0530 Subject: [PATCH 17/18] refactor: Pass boot dir to boot entry readers This allows for easier testing Signed-off-by: Pragyan Poudyal --- crates/lib/src/deploy.rs | 41 +++++++++++++++++++++++++++++---------- crates/lib/src/install.rs | 4 +++- crates/lib/src/status.rs | 6 ++++-- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 41f31af24..8c958ab51 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -26,7 +26,8 @@ use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; use crate::composefs_consts::{ - BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK, + BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, + USER_CFG_ROLLBACK, }; use crate::install::{get_efi_uuid_source, BootType}; use crate::parsers::bls_config::{parse_bls_config, BLSConfig}; @@ -752,8 +753,11 @@ pub(crate) fn rollback_composefs_uki() -> Result<()> { let user_cfg_path = PathBuf::from("/sysroot/boot/grub2"); let mut str = String::new(); + let boot_dir = + cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority()) + .context("Opening boot dir")?; let mut menuentries = - get_sorted_uki_boot_entries(&mut str).context("Getting UKI boot entries")?; + get_sorted_uki_boot_entries(&boot_dir, &mut str).context("Getting UKI boot entries")?; // TODO(Johan-Liebert): Currently assuming there are only two deployments assert!(menuentries.len() == 2); @@ -800,17 +804,25 @@ pub(crate) fn rollback_composefs_uki() -> Result<()> { } // Need str to store lifetime -pub(crate) fn get_sorted_uki_boot_entries<'a>(str: &'a mut String) -> Result>> { - let mut file = std::fs::File::open(format!("/sysroot/boot/grub2/{USER_CFG}"))?; +pub(crate) fn get_sorted_uki_boot_entries<'a>( + boot_dir: &Dir, + str: &'a mut String, +) -> Result>> { + let mut file = boot_dir + .open(format!("grub2/{USER_CFG}")) + .with_context(|| format!("Opening {USER_CFG}"))?; file.read_to_string(str)?; parse_grub_menuentry_file(str) } -#[context("Getting boot entries")] -pub(crate) fn get_sorted_bls_boot_entries(ascending: bool) -> Result> { +#[context("Getting sorted BLS entries")] +pub(crate) fn get_sorted_bls_boot_entries( + boot_dir: &Dir, + ascending: bool, +) -> Result> { let mut all_configs = vec![]; - for entry in std::fs::read_dir(format!("/sysroot/boot/loader/{BOOT_LOADER_ENTRIES}"))? { + for entry in boot_dir.read_dir(format!("loader/{BOOT_LOADER_ENTRIES}"))? { let entry = entry?; let file_name = entry.file_name(); @@ -823,8 +835,13 @@ pub(crate) fn get_sorted_bls_boot_entries(ascending: bool) -> Result Result Result<()> { + let boot_dir = + cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority()) + .context("Opening boot dir")?; + // Sort in descending order as that's the order they're shown on the boot screen // After this: // all_configs[0] -> booted depl // all_configs[1] -> rollback depl - let mut all_configs = get_sorted_bls_boot_entries(false)?; + let mut all_configs = get_sorted_bls_boot_entries(&boot_dir, false)?; // Update the indicies so that they're swapped for (idx, cfg) in all_configs.iter_mut().enumerate() { diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index a0211f51a..4eebb5884 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2029,7 +2029,9 @@ pub(crate) fn setup_composefs_uki_boot( )?; let mut str_buf = String::new(); - let entries = get_sorted_uki_boot_entries(&mut str_buf)?; + let boot_dir = cap_std::fs::Dir::open_ambient_dir(boot_dir, cap_std::ambient_authority()) + .context("Opening boot dir")?; + let entries = get_sorted_uki_boot_entries(&boot_dir, &mut str_buf)?; // Write out only the currently booted entry, which should be the very first one // Even if we have booted into the second menuentry "boot entry", the default will be the diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index badb1683f..7c5f876ca 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -488,9 +488,11 @@ pub(crate) async fn composefs_deployment_status() -> Result { anyhow::bail!("Could not determine boot type"); }; + let boot_dir = sysroot.open_dir("boot").context("Opening boot dir")?; + match boot_type { BootType::Bls => { - host.status.rollback_queued = !get_sorted_bls_boot_entries(false)? + host.status.rollback_queued = !get_sorted_bls_boot_entries(&boot_dir, false)? .first() .ok_or(anyhow::anyhow!("First boot entry not found"))? .options @@ -500,7 +502,7 @@ pub(crate) async fn composefs_deployment_status() -> Result { BootType::Uki => { let mut s = String::new(); - host.status.rollback_queued = !get_sorted_uki_boot_entries(&mut s)? + host.status.rollback_queued = !get_sorted_uki_boot_entries(&boot_dir, &mut s)? .first() .ok_or(anyhow::anyhow!("First boot entry not found"))? .body From 3d2385d91d4b769547691004ad366e338983a7ec Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 1 Aug 2025 15:35:58 +0530 Subject: [PATCH 18/18] test: Add tests for reading boot entries Add tests for functions `get_sorted_bls_boot_entries` and `get_sorted_uki_boot_entries` Signed-off-by: Pragyan Poudyal --- crates/lib/src/deploy.rs | 122 +++++++++++++++++++++- crates/lib/src/parsers/grub_menuconfig.rs | 8 +- 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index 8c958ab51..de1f181cb 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -26,8 +26,7 @@ use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; use crate::composefs_consts::{ - BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, - USER_CFG_ROLLBACK, + BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK, }; use crate::install::{get_efi_uuid_source, BootType}; use crate::parsers::bls_config::{parse_bls_config, BLSConfig}; @@ -1183,6 +1182,10 @@ pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> { #[cfg(test)] mod tests { + use std::collections::HashMap; + + use crate::parsers::grub_menuconfig::MenuentryBody; + use super::*; #[test] @@ -1277,4 +1280,119 @@ UUID=6907-17CA /boot/efi vfat umask=0077,shortname=win assert_eq!(tempdir.read_to_string("etc/fstab")?, modified); Ok(()) } + + #[test] + fn test_sorted_bls_boot_entries() -> Result<()> { + let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + + let entry1 = r#" + title Fedora 42.20250623.3.1 (CoreOS) + version 1 + linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10 + initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img + options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6 + "#; + + let entry2 = r#" + title Fedora 41.20250214.2.0 (CoreOS) + version 2 + linux /boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10 + initrd /boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img + options root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01 + "#; + + tempdir.create_dir_all("loader/entries")?; + tempdir.atomic_write( + "loader/entries/random_file.txt", + "Random file that we won't parse", + )?; + tempdir.atomic_write("loader/entries/entry1.conf", entry1)?; + tempdir.atomic_write("loader/entries/entry2.conf", entry2)?; + + let result = get_sorted_bls_boot_entries(&tempdir, true); + + let mut expected = vec![ + BLSConfig { + title: Some("Fedora 42.20250623.3.1 (CoreOS)".into()), + version: 1, + linux: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(), + initrd: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into(), + options: "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into(), + extra: HashMap::new(), + }, + BLSConfig { + title: Some("Fedora 41.20250214.2.0 (CoreOS)".into()), + version: 2, + linux: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(), + initrd: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into(), + options: "root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into(), + extra: HashMap::new(), + }, + ]; + + assert_eq!(result.unwrap(), expected); + + let result = get_sorted_bls_boot_entries(&tempdir, false); + expected.reverse(); + assert_eq!(result.unwrap(), expected); + + Ok(()) + } + + #[test] + fn test_sorted_uki_boot_entries() -> Result<()> { + let user_cfg = r#" + if [ -f ${config_directory}/efiuuid.cfg ]; then + source ${config_directory}/efiuuid.cfg + fi + + menuentry "Fedora Bootc UKI: (f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346)" { + insmod fat + insmod chain + search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}" + chainloader /EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi + } + + menuentry "Fedora Bootc UKI: (7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6)" { + insmod fat + insmod chain + search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}" + chainloader /EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi + } + "#; + + let bootdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + bootdir.create_dir_all(format!("grub2"))?; + bootdir.atomic_write(format!("grub2/{USER_CFG}"), user_cfg)?; + + let mut s = String::new(); + let result = get_sorted_uki_boot_entries(&bootdir, &mut s)?; + + let expected = vec![ + MenuEntry { + title: "Fedora Bootc UKI: (f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346)".into(), + body: MenuentryBody { + insmod: vec!["fat", "chain"], + chainloader: "/EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi".into(), + search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"", + version: 0, + extra: vec![], + }, + }, + MenuEntry { + title: "Fedora Bootc UKI: (7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6)".into(), + body: MenuentryBody { + insmod: vec!["fat", "chain"], + chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi".into(), + search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"", + version: 0, + extra: vec![], + }, + }, + ]; + + assert_eq!(result, expected); + + Ok(()) + } } diff --git a/crates/lib/src/parsers/grub_menuconfig.rs b/crates/lib/src/parsers/grub_menuconfig.rs index 0a1c3245c..9e413c17f 100644 --- a/crates/lib/src/parsers/grub_menuconfig.rs +++ b/crates/lib/src/parsers/grub_menuconfig.rs @@ -11,11 +11,11 @@ use nom::{ #[derive(Debug, PartialEq, Eq)] pub(crate) struct MenuentryBody<'a> { - insmod: Vec<&'a str>, + pub(crate) insmod: Vec<&'a str>, pub(crate) chainloader: String, - search: &'a str, - version: u8, - extra: Vec<(&'a str, &'a str)>, + pub(crate) search: &'a str, + pub(crate) version: u8, + pub(crate) extra: Vec<(&'a str, &'a str)>, } impl<'a> Display for MenuentryBody<'a> {