From 0a67a1804c7606368660b0a009ea63e928089fb4 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 3 Dec 2025 12:57:30 +0530 Subject: [PATCH 1/9] composefs/status: Check if deployment is soft rebootable Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/status.rs | 219 ++++++++++++++++++++--- crates/lib/src/cli.rs | 2 +- crates/lib/src/parsers/bls_config.rs | 17 ++ crates/lib/src/status.rs | 7 + 4 files changed, 222 insertions(+), 23 deletions(-) diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index c4967824e..6bbe51593 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize}; use crate::{ bootc_composefs::{boot::BootType, repo::get_imgref}, - composefs_consts::{COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, USER_CFG}, + composefs_consts::{ + COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG, + }, install::EFI_LOADER_INFO, parsers::{ bls_config::{parse_bls_config, BLSConfig, BLSConfigType}, @@ -100,14 +102,43 @@ pub(crate) fn get_sorted_grub_uki_boot_entries<'a>( parse_grub_menuentry_file(str) } -#[context("Getting sorted Type1 boot entries")] pub(crate) fn get_sorted_type1_boot_entries( boot_dir: &Dir, ascending: bool, +) -> Result> { + get_sorted_type1_boot_entries_helper(boot_dir, ascending, false) +} + +pub(crate) fn get_sorted_staged_type1_boot_entries( + boot_dir: &Dir, + ascending: bool, +) -> Result> { + get_sorted_type1_boot_entries_helper(boot_dir, ascending, true) +} + +#[context("Getting sorted Type1 boot entries")] +fn get_sorted_type1_boot_entries_helper( + boot_dir: &Dir, + ascending: bool, + get_staged_entries: bool, ) -> Result> { let mut all_configs = vec![]; - for entry in boot_dir.read_dir(TYPE1_ENT_PATH)? { + let dir = match get_staged_entries { + true => { + let dir = boot_dir.open_dir_optional(TYPE1_ENT_PATH_STAGED)?; + + let Some(dir) = dir else { + return Ok(all_configs); + }; + + dir.read_dir(".")? + } + + false => boot_dir.read_dir(TYPE1_ENT_PATH)?, + }; + + for entry in dir { let entry = entry?; let file_name = entry.file_name(); @@ -302,12 +333,140 @@ pub(crate) async fn get_composefs_status( composefs_deployment_status_from(&storage, booted_cfs.cmdline).await } +fn set_soft_reboot_capable_bls( + storage: &Storage, + host: &mut Host, + bls_entries: &Vec, + cmdline: &ComposefsCmdline, +) -> Result<()> { + let booted = host.require_composefs_booted()?; + + match booted.boot_type { + BootType::Bls => { + set_reboot_capable_type1_deployments(storage, cmdline, host, bls_entries)?; + } + + BootType::Uki => match booted.bootloader { + Bootloader::Grub => todo!(), + Bootloader::Systemd => todo!(), + }, + }; + + Ok(()) +} + +fn find_bls_entry<'a>( + verity: &str, + bls_entries: &'a Vec, +) -> Result> { + for ent in bls_entries { + if ent.get_verity()? == *verity { + return Ok(Some(ent)); + } + } + + Ok(None) +} + +/// Compares cmdline `first` and `second` skipping `composefs=` +fn compare_cmdline_skip_cfs(first: &Cmdline<'_>, second: &Cmdline<'_>) -> bool { + for param in first { + if param.key() == COMPOSEFS_CMDLINE.into() { + continue; + } + + let second_param = second.iter().find(|b| *b == param); + + let Some(found_param) = second_param else { + return false; + }; + + if found_param.value() != param.value() { + return false; + } + } + + return true; +} + +fn set_soft_reboot_capable_type1( + deployment: &mut BootEntry, + bls_entries: &Vec, + booted_bls_entry: &BLSConfig, + booted_boot_digest: &String, +) -> Result<()> { + let deployment_cfs = deployment.require_composefs()?; + + // TODO: Unwrap + if deployment_cfs.boot_digest.as_ref().unwrap() != booted_boot_digest { + deployment.soft_reboot_capable = false; + return Ok(()); + } + + let entry = find_bls_entry(&deployment_cfs.verity, bls_entries)? + .ok_or_else(|| anyhow::anyhow!("Entry not found"))?; + + let opts = entry.get_cmdline()?; + let booted_cmdline_opts = booted_bls_entry.get_cmdline()?; + + if opts.len() != booted_cmdline_opts.len() { + tracing::debug!("Soft reboot not allowed due to differing cmdline"); + deployment.soft_reboot_capable = false; + return Ok(()); + } + + deployment.soft_reboot_capable = compare_cmdline_skip_cfs(opts, booted_cmdline_opts) + && compare_cmdline_skip_cfs(booted_cmdline_opts, opts); + + return Ok(()); +} + +fn set_reboot_capable_type1_deployments( + storage: &Storage, + cmdline: &ComposefsCmdline, + host: &mut Host, + bls_entries: &Vec, +) -> Result<()> { + let booted = host + .status + .booted + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?; + + let booted_boot_digest = booted.composefs_boot_digest()?; + + let booted_bls_entry = find_bls_entry(&*cmdline.digest, bls_entries)? + .ok_or_else(|| anyhow::anyhow!("Booted bls entry not found"))?; + + if let Some(staged) = host.status.staged.as_mut() { + let staged_entries = + get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, true)?; + + set_soft_reboot_capable_type1( + staged, + &staged_entries, + booted_bls_entry, + booted_boot_digest, + )?; + } + + if let Some(rollback) = &mut host.status.rollback { + set_soft_reboot_capable_type1(rollback, bls_entries, booted_bls_entry, booted_boot_digest)?; + } + + for depl in &mut host.status.other_deployments { + set_soft_reboot_capable_type1(depl, bls_entries, booted_bls_entry, booted_boot_digest)?; + } + + Ok(()) +} + #[context("Getting composefs deployment status")] pub(crate) async fn composefs_deployment_status_from( storage: &Storage, cmdline: &ComposefsCmdline, ) -> Result { - let composefs_digest = &cmdline.digest; + let booted_composefs_digest = &cmdline.digest; let boot_dir = storage.require_boot_dir()?; @@ -373,7 +532,7 @@ pub(crate) async fn composefs_deployment_status_from( } }; - if depl.file_name() == composefs_digest.as_ref() { + if depl.file_name() == booted_composefs_digest.as_ref() { host.spec.image = boot_entry.image.as_ref().map(|x| x.image.clone()); host.status.booted = Some(boot_entry); continue; @@ -394,60 +553,72 @@ pub(crate) async fn composefs_deployment_status_from( anyhow::bail!("Could not determine boot type"); }; - let booted = host.require_composefs_booted()?; + let booted_cfs = host.require_composefs_booted()?; - let is_rollback_queued = match booted.bootloader { + let (is_rollback_queued, sorted_bls_config) = match booted_cfs.bootloader { Bootloader::Grub => match boot_type { BootType::Bls => { - let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?; - let bls_config = bls_config + let bls_configs = get_sorted_type1_boot_entries(boot_dir, false)?; + let bls_config = bls_configs .first() - .ok_or(anyhow::anyhow!("First boot entry not found"))?; + .ok_or_else(|| anyhow::anyhow!("First boot entry not found"))?; match &bls_config.cfg_type { - BLSConfigType::NonEFI { options, .. } => !options - .as_ref() - .ok_or(anyhow::anyhow!("options key not found in bls config"))? - .contains(composefs_digest.as_ref()), + BLSConfigType::NonEFI { options, .. } => { + let is_rollback_queued = !options + .as_ref() + .ok_or_else(|| anyhow::anyhow!("options key not found in bls config"))? + .contains(booted_composefs_digest.as_ref()); + + (is_rollback_queued, Some(bls_configs)) + } BLSConfigType::EFI { .. } => { anyhow::bail!("Found 'efi' field in Type1 boot entry") } + BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"), } } BootType::Uki => { let mut s = String::new(); + let menuentries = get_sorted_grub_uki_boot_entries(boot_dir, &mut s)?; - !get_sorted_grub_uki_boot_entries(boot_dir, &mut s)? + let is_rollback_queued = !menuentries .first() .ok_or(anyhow::anyhow!("First boot entry not found"))? .body .chainloader - .contains(composefs_digest.as_ref()) + .contains(booted_composefs_digest.as_ref()); + + (is_rollback_queued, None) } }, // We will have BLS stuff and the UKI stuff in the same DIR Bootloader::Systemd => { - let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?; - let bls_config = bls_config + let bls_configs = get_sorted_type1_boot_entries(boot_dir, true)?; + let bls_config = bls_configs .first() .ok_or(anyhow::anyhow!("First boot entry not found"))?; - match &bls_config.cfg_type { + let is_rollback_queued = match &bls_config.cfg_type { // For UKI boot - BLSConfigType::EFI { efi } => efi.as_str().contains(composefs_digest.as_ref()), + BLSConfigType::EFI { efi } => { + efi.as_str().contains(booted_composefs_digest.as_ref()) + } // For boot entry Type1 BLSConfigType::NonEFI { options, .. } => !options .as_ref() .ok_or(anyhow::anyhow!("options key not found in bls config"))? - .contains(composefs_digest.as_ref()), + .contains(booted_composefs_digest.as_ref()), BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"), - } + }; + + (is_rollback_queued, Some(bls_configs)) } }; @@ -457,6 +628,10 @@ pub(crate) async fn composefs_deployment_status_from( host.spec.boot_order = BootOrder::Rollback }; + if let Some(bls_configs) = sorted_bls_config { + set_soft_reboot_capable_bls(storage, &mut host, &bls_configs, cmdline)?; + } + Ok(host) } diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 952efd79d..b69167fc4 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -11,8 +11,8 @@ use std::sync::Arc; use anyhow::{anyhow, ensure, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; -use cap_std_ext::cap_std; use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::cap_std; use clap::Parser; use clap::ValueEnum; use composefs::dumpfile; diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index 606b990c7..71a714e03 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -203,6 +203,23 @@ impl BLSConfig { BLSConfigType::Unknown => anyhow::bail!("Unknown config type"), } } + + /// Gets the `options` field from the config + /// Returns an error if the field doesn't exist + /// or if the config is of type `EFI` + pub(crate) fn get_cmdline(&self) -> Result<&Cmdline<'_>> { + match &self.cfg_type { + BLSConfigType::NonEFI { options, .. } => { + let options = options + .as_ref() + .ok_or_else(|| anyhow::anyhow!("No cmdline found for config"))?; + + Ok(options) + } + + _ => anyhow::bail!("No cmdline found for config"), + } + } } pub(crate) fn parse_bls_config(input: &str) -> Result { diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index 30b8a39dd..3a60f14ec 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -309,6 +309,13 @@ impl BootEntry { "BootEntry is not a composefs native boot entry" )) } + + pub(crate) fn composefs_boot_digest(&self) -> Result<&String> { + self.require_composefs()? + .boot_digest + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Could not find boot digest for deployment")) + } } /// A variant of [`get_status`] that requires a booted deployment. From 9fbe6f48ff48fa98c474c7f80c7d333b52bda040 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 09:39:39 +0530 Subject: [PATCH 2/9] initramfs: Allow passing target for mounting `target` field in Args was not being used. Use it if it is passed in the args. Also helps us mount the new root at `/run/nextroot` Also, use Cmdline struct instead of String to represent the kernel command line Signed-off-by: Pragyan Poudyal --- Cargo.lock | 1 + crates/initramfs/Cargo.toml | 1 + crates/initramfs/src/lib.rs | 41 +++++++++++++++++++++++-------------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a05ae3b4..01cec42b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,7 @@ name = "bootc-initramfs-setup" version = "0.1.0" dependencies = [ "anyhow", + "bootc-kernel-cmdline", "clap", "composefs", "composefs-boot", diff --git a/crates/initramfs/Cargo.toml b/crates/initramfs/Cargo.toml index 94bebd858..b8d8156a7 100644 --- a/crates/initramfs/Cargo.toml +++ b/crates/initramfs/Cargo.toml @@ -15,6 +15,7 @@ composefs.workspace = true composefs-boot.workspace = true toml.workspace = true fn-error-context.workspace = true +bootc-kernel-cmdline = { path = "../kernel_cmdline", version = "0.0.0" } [lints] workspace = true diff --git a/crates/initramfs/src/lib.rs b/crates/initramfs/src/lib.rs index 63cf71392..36ea9ec2c 100644 --- a/crates/initramfs/src/lib.rs +++ b/crates/initramfs/src/lib.rs @@ -31,6 +31,8 @@ use composefs_boot::cmdline::get_cmdline_composefs; use fn_error_context::context; +use bootc_kernel_cmdline::utf8::Cmdline; + // mount_setattr syscall support const MOUNT_ATTR_RDONLY: u64 = 0x00000001; @@ -74,13 +76,17 @@ fn set_mount_readonly(fd: impl AsFd) -> Result<()> { mount_setattr(fd, libc::AT_EMPTY_PATH, &attr) } -// Config file +/// Types of mounts supported by the configuration #[derive(Clone, Copy, Debug, Deserialize)] #[serde(rename_all = "lowercase")] -enum MountType { +pub enum MountType { + /// No mount None, + /// Bind mount Bind, + /// Overlay mount Overlay, + /// Transient mount Transient, } @@ -90,11 +96,14 @@ struct RootConfig { transient: bool, } +/// Configuration for mount operations #[derive(Debug, Default, Deserialize)] -struct MountConfig { - mount: Option, +pub struct MountConfig { + /// The type of mount to use + pub mount: Option, #[serde(default)] - transient: bool, + /// Whether this mount should be transient (temporary) + pub transient: bool, } #[derive(Deserialize, Default)] @@ -138,7 +147,7 @@ pub struct Args { #[arg(long, help = "Kernel commandline args (for testing)")] /// Kernel commandline args (for testing) - pub cmdline: Option, + pub cmdline: Option>, #[arg(long, help = "Mountpoint (don't replace sysroot, for testing)")] /// Mountpoint (don't replace sysroot, for testing) @@ -265,8 +274,9 @@ pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> R Ok(rootfs) } +/// Mounts a subdirectory with the specified configuration #[context("Mounting subdirectory")] -fn mount_subdir( +pub fn mount_subdir( new_root: impl AsFd, state: impl AsFd, subdir: &str, @@ -331,12 +341,11 @@ pub fn setup_root(args: Args) -> Result<()> { let sysroot = open_dir(CWD, &args.sysroot) .with_context(|| format!("Failed to open sysroot {:?}", args.sysroot))?; - let cmdline = match &args.cmdline { - Some(cmdline) => cmdline, - // TODO: Deduplicate this with composefs branch karg parser - None => &std::fs::read_to_string("/proc/cmdline")?, - }; - let (image, insecure) = get_cmdline_composefs::(cmdline)?; + let cmdline = args + .cmdline + .unwrap_or(Cmdline::from_proc().context("Failed to read cmdline")?); + + let (image, insecure) = get_cmdline_composefs::(&cmdline)?; let new_root = match args.root_fs { Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?, @@ -348,11 +357,13 @@ pub fn setup_root(args: Args) -> Result<()> { set_mount_readonly(&sysroot_clone)?; + let mount_target = args.target.unwrap_or(args.sysroot.clone()); + // Ideally we build the new root filesystem together before we mount it, but that only works on // 6.15 and later. Before 6.15 we can't mount into a floating tree, so mount it first. This // will leave an abandoned clone of the sysroot mounted under it, but that's OK for now. if cfg!(feature = "pre-6.15") { - mount_at_wrapper(&new_root, CWD, &args.sysroot)?; + mount_at_wrapper(&new_root, CWD, &mount_target)?; } if config.root.transient { @@ -372,7 +383,7 @@ pub fn setup_root(args: Args) -> Result<()> { if cfg!(not(feature = "pre-6.15")) { // Replace the /sysroot with the new composed root filesystem unmount(&args.sysroot, UnmountFlags::DETACH)?; - mount_at_wrapper(&new_root, CWD, &args.sysroot)?; + mount_at_wrapper(&new_root, CWD, &mount_target)?; } Ok(()) From fc59c786a9c45fca5d90cff8923a6469a02b660a Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 10:53:43 +0530 Subject: [PATCH 3/9] composefs: Implement soft reboot Add an internal command for soft rebooting the system. Similar to how it's done for ostree, we only allow soft reboot if the other deployment has the same kernel state, i.e. the SHASum of kernel + initrd is the same as that of the current deployment. soft reboot is not possible in case of UKI deployment Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/mod.rs | 1 + crates/lib/src/bootc_composefs/soft_reboot.rs | 68 +++++++++++++++++++ crates/lib/src/cli.rs | 21 +++++- crates/lib/src/spec.rs | 5 ++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 crates/lib/src/bootc_composefs/soft_reboot.rs diff --git a/crates/lib/src/bootc_composefs/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index a9ced452d..3874417c4 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod gc; pub(crate) mod repo; pub(crate) mod rollback; pub(crate) mod service; +pub(crate) mod soft_reboot; pub(crate) mod state; pub(crate) mod status; pub(crate) mod switch; diff --git a/crates/lib/src/bootc_composefs/soft_reboot.rs b/crates/lib/src/bootc_composefs/soft_reboot.rs new file mode 100644 index 000000000..1bc3ba070 --- /dev/null +++ b/crates/lib/src/bootc_composefs/soft_reboot.rs @@ -0,0 +1,68 @@ +use crate::{ + bootc_composefs::{ + service::start_finalize_stated_svc, status::composefs_deployment_status_from, + }, + composefs_consts::COMPOSEFS_CMDLINE, + store::{BootedComposefs, Storage}, +}; +use anyhow::{Context, Result}; +use bootc_initramfs_setup::setup_root; +use bootc_kernel_cmdline::utf8::Cmdline; +use bootc_mount::{bind_mount_from_pidns, PID1}; +use camino::Utf8Path; +use std::{fs::create_dir_all, os::unix::process::CommandExt, path::PathBuf, process::Command}; + +const NEXTROOT: &str = "/run/nextroot"; + +pub(crate) async fn soft_reboot_to_deployment( + storage: &Storage, + booted_cfs: &BootedComposefs, + deployment_id: &String, + reboot: bool, +) -> Result<()> { + if *deployment_id == *booted_cfs.cmdline.digest { + anyhow::bail!("Cannot soft-reboot to currently booted deployment"); + } + + let host = composefs_deployment_status_from(storage, booted_cfs.cmdline).await?; + + let all_deployments = host.all_composefs_deployments()?; + + let requred_deployment = all_deployments + .iter() + .find(|entry| entry.deployment.verity == *deployment_id) + .ok_or_else(|| anyhow::anyhow!("Deployment '{deployment_id}' not found"))?; + + if !requred_deployment.soft_reboot_capable { + anyhow::bail!("Cannot soft-reboot to deployment with a different kernel state"); + } + + start_finalize_stated_svc()?; + + // escape to global mnt namespace + let run = Utf8Path::new("/run"); + bind_mount_from_pidns(PID1, &run, &run, false).context("Bind mounting /run")?; + + create_dir_all(NEXTROOT).context("Creating nextroot")?; + + let cmdline = Cmdline::from(format!("{COMPOSEFS_CMDLINE}={deployment_id}")); + + let args = bootc_initramfs_setup::Args { + cmd: vec![], + sysroot: PathBuf::from("/sysroot"), + config: Default::default(), + root_fs: None, + cmdline: Some(cmdline), + target: Some(NEXTROOT.into()), + }; + + setup_root(args)?; + + if reboot { + // Replacing the current process should be fine as we restart userspace anyway + let err = Command::new("systemctl").arg("soft-reboot").exec(); + return Err(anyhow::Error::from(err).context("Failed to exec 'systemctl soft-reboot'")); + } + + Ok(()) +} diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index b69167fc4..0c542fd69 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -11,8 +11,8 @@ use std::sync::Arc; use anyhow::{anyhow, ensure, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; -use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::cap_std; +use cap_std_ext::cap_std::fs::Dir; use clap::Parser; use clap::ValueEnum; use composefs::dumpfile; @@ -35,6 +35,7 @@ use serde::{Deserialize, Serialize}; use tempfile::tempdir_in; use crate::bootc_composefs::delete::delete_composefs_deployment; +use crate::bootc_composefs::soft_reboot::soft_reboot_to_deployment; use crate::bootc_composefs::{ finalize::{composefs_backend_finalize, get_etc_diff}, rollback::composefs_rollback, @@ -593,6 +594,11 @@ pub(crate) enum InternalsOpts { #[cfg(feature = "docgen")] /// Dump CLI structure as JSON for documentation generation DumpCliJson, + PrepSoftReboot { + deployment: String, + #[clap(long)] + reboot: bool, + }, } #[derive(Debug, clap::Subcommand, PartialEq, Eq)] @@ -1762,6 +1768,19 @@ async fn run_from_opt(opt: Opt) -> Result<()> { Ok(()) } + InternalsOpts::PrepSoftReboot { deployment, reboot } => { + let storage = &get_storage().await?; + + match storage.kind()? { + BootedStorageKind::Ostree(..) => { + // TODO: Call ostree implementation? + anyhow::bail!("soft-reboot only implemented for composefs") + } + BootedStorageKind::Composefs(booted_cfs) => { + soft_reboot_to_deployment(&storage, &booted_cfs, &deployment, reboot).await + } + } + } }, Opt::State(opts) => match opts { StateOpts::WipeOstree => { diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index 743e1eb2f..cacffd8f9 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -295,6 +295,7 @@ pub(crate) struct DeploymentEntry<'a> { pub(crate) ty: Option, pub(crate) deployment: &'a BootEntryComposefs, pub(crate) pinned: bool, + pub(crate) soft_reboot_capable: bool, } /// The result of a `bootc container inspect` command. @@ -362,6 +363,7 @@ impl Host { ty: Some(Slot::Booted), deployment: booted, pinned: false, + soft_reboot_capable: false, }); if let Some(staged) = &self.status.staged { @@ -369,6 +371,7 @@ impl Host { ty: Some(Slot::Staged), deployment: staged.require_composefs()?, pinned: false, + soft_reboot_capable: staged.soft_reboot_capable, }); } @@ -377,6 +380,7 @@ impl Host { ty: Some(Slot::Rollback), deployment: rollback.require_composefs()?, pinned: false, + soft_reboot_capable: rollback.soft_reboot_capable, }); } @@ -385,6 +389,7 @@ impl Host { ty: None, deployment: pinned.require_composefs()?, pinned: true, + soft_reboot_capable: pinned.soft_reboot_capable, }); } From ed36245cd83dd4cc50d7429fa2555eecbae08439 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 12:18:07 +0530 Subject: [PATCH 4/9] composefs: Handle bootc status after a soft reboot After a soft reboot the kernel cmdline doesn't change so we can't rely on the `composefs=` parameter in the cmdline. Instead, we check the source of the root mount point Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/status.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 6bbe51593..0277a8962 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -2,6 +2,7 @@ use std::{io::Read, sync::OnceLock}; use anyhow::{Context, Result}; use bootc_kernel_cmdline::utf8::Cmdline; +use bootc_mount::inspect_filesystem; use fn_error_context::context; use serde::{Deserialize, Serialize}; @@ -86,7 +87,23 @@ pub(crate) fn composefs_booted() -> Result> { }; let Some(v) = kv.value() else { return Ok(None) }; let v = ComposefsCmdline::new(v); - let r = CACHED_DIGEST_VALUE.get_or_init(|| Some(v)); + + // Find the source of / mountpoint as the cmdline doesn't change on soft-reboot + let root_mnt = inspect_filesystem("/".into())?; + + // This is of the format composefs: + let verity_from_mount_src = root_mnt + .source + .strip_prefix("composefs:") + .ok_or_else(|| anyhow::anyhow!("Root not mounted using composefs"))?; + + let r = if *verity_from_mount_src != *v.digest { + // soft rebooted into another deployment + CACHED_DIGEST_VALUE.get_or_init(|| Some(ComposefsCmdline::new(verity_from_mount_src))) + } else { + CACHED_DIGEST_VALUE.get_or_init(|| Some(v)) + }; + Ok(r.as_ref()) } From 9e150aea1c8767b070de1702471af634647ac480 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 13:57:22 +0530 Subject: [PATCH 5/9] composefs: Soft Reboot after update/switch if specified Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 2 +- crates/lib/src/bootc_composefs/soft_reboot.rs | 12 +++- crates/lib/src/bootc_composefs/state.rs | 2 +- crates/lib/src/bootc_composefs/switch.rs | 27 ++++++++- crates/lib/src/bootc_composefs/update.rs | 60 ++++++++++++++++--- crates/lib/src/cli.rs | 5 +- 6 files changed, 91 insertions(+), 17 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index d669a441e..2857e1bd8 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -1276,7 +1276,7 @@ pub(crate) async fn setup_composefs_boot( write_composefs_state( &root_setup.physical_root_path, - id, + &id, &crate::spec::ImageReference::from(state.target_imgref.clone()), false, boot_type, diff --git a/crates/lib/src/bootc_composefs/soft_reboot.rs b/crates/lib/src/bootc_composefs/soft_reboot.rs index 1bc3ba070..4d1f3cf31 100644 --- a/crates/lib/src/bootc_composefs/soft_reboot.rs +++ b/crates/lib/src/bootc_composefs/soft_reboot.rs @@ -10,20 +10,30 @@ use bootc_initramfs_setup::setup_root; use bootc_kernel_cmdline::utf8::Cmdline; use bootc_mount::{bind_mount_from_pidns, PID1}; use camino::Utf8Path; +use fn_error_context::context; +use ostree_ext::systemd_has_soft_reboot; use std::{fs::create_dir_all, os::unix::process::CommandExt, path::PathBuf, process::Command}; const NEXTROOT: &str = "/run/nextroot"; -pub(crate) async fn soft_reboot_to_deployment( +/// Checks if the provided deployment is soft reboot capable, and soft reboots the system if +/// argument `reboot` is true +#[context("Soft rebooting")] +pub(crate) async fn prepare_soft_reboot_composefs( storage: &Storage, booted_cfs: &BootedComposefs, deployment_id: &String, reboot: bool, ) -> Result<()> { + if !systemd_has_soft_reboot() { + anyhow::bail!("System does not support soft reboots") + } + if *deployment_id == *booted_cfs.cmdline.digest { anyhow::bail!("Cannot soft-reboot to currently booted deployment"); } + // We definitely need to re-query the state as some deployment might've been staged let host = composefs_deployment_status_from(storage, booted_cfs.cmdline).await?; let all_deployments = host.all_composefs_deployments()?; diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index b4f22d672..f8e625032 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -180,7 +180,7 @@ pub(crate) fn update_target_imgref_in_origin( #[context("Writing composefs state")] pub(crate) async fn write_composefs_state( root_path: &Utf8PathBuf, - deployment_id: Sha512HashValue, + deployment_id: &Sha512HashValue, target_imgref: &ImageReference, staged: bool, boot_type: BootType, diff --git a/crates/lib/src/bootc_composefs/switch.rs b/crates/lib/src/bootc_composefs/switch.rs index 2ce890e16..8a71dc328 100644 --- a/crates/lib/src/bootc_composefs/switch.rs +++ b/crates/lib/src/bootc_composefs/switch.rs @@ -5,7 +5,7 @@ use crate::{ bootc_composefs::{ state::update_target_imgref_in_origin, status::get_composefs_status, - update::{do_upgrade, is_image_pulled, validate_update, UpdateAction}, + update::{do_upgrade, is_image_pulled, validate_update, DoUpgradeOpts, UpdateAction}, }, cli::{imgref_for_switch, SwitchOpts}, store::{BootedComposefs, Storage}, @@ -42,6 +42,11 @@ pub(crate) async fn switch_composefs( let repo = &*booted_cfs.repo; let (image, img_config) = is_image_pulled(repo, &target_imgref).await?; + let do_upgrade_opts = DoUpgradeOpts { + soft_reboot: opts.soft_reboot, + apply: opts.apply, + }; + if let Some(cfg_verity) = image { let action = validate_update( storage, @@ -59,7 +64,15 @@ pub(crate) async fn switch_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, &target_imgref, &img_config).await; + return do_upgrade( + storage, + booted_cfs, + &host, + &target_imgref, + &img_config, + &do_upgrade_opts, + ) + .await; } UpdateAction::UpdateOrigin => { @@ -71,7 +84,15 @@ pub(crate) async fn switch_composefs( } } - do_upgrade(storage, &host, &target_imgref, &img_config).await?; + do_upgrade( + storage, + booted_cfs, + &host, + &target_imgref, + &img_config, + &do_upgrade_opts, + ) + .await?; Ok(()) } diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index e22ae7984..025e56a71 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -15,13 +15,14 @@ use crate::{ boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType}, repo::{get_imgref, pull_composefs_repo}, service::start_finalize_stated_svc, + soft_reboot::prepare_soft_reboot_composefs, state::write_composefs_state, status::{ get_bootloader, get_composefs_status, get_container_manifest_and_config, get_imginfo, ImgConfigManifest, }, }, - cli::UpgradeOpts, + cli::{SoftRebootMode, UpgradeOpts}, composefs_consts::{STATE_DIR_RELATIVE, TYPE1_ENT_PATH_STAGED, USER_CFG_STAGED}, spec::{Bootloader, Host, ImageReference}, store::{BootedComposefs, ComposefsRepository, Storage}, @@ -208,13 +209,21 @@ pub(crate) fn validate_update( Ok(UpdateAction::Proceed) } +/// This is just an intersection of SwitchOpts and UpgradeOpts +pub(crate) struct DoUpgradeOpts { + pub(crate) apply: bool, + pub(crate) soft_reboot: Option, +} + /// Performs the Update or Switch operation #[context("Performing Upgrade Operation")] pub(crate) async fn do_upgrade( storage: &Storage, + booted_cfs: &BootedComposefs, host: &Host, imgref: &ImageReference, img_manifest_config: &ImgConfigManifest, + opts: &DoUpgradeOpts, ) -> Result<()> { start_finalize_stated_svc()?; @@ -254,7 +263,7 @@ pub(crate) async fn do_upgrade( write_composefs_state( &Utf8PathBuf::from("/sysroot"), - id, + &id, imgref, true, boot_type, @@ -263,6 +272,14 @@ pub(crate) async fn do_upgrade( ) .await?; + if opts.apply { + return crate::reboot::reboot(); + } + + if opts.soft_reboot.is_some() { + prepare_soft_reboot_composefs(storage, booted_cfs, &id.to_hex(), true).await?; + } + Ok(()) } @@ -299,6 +316,11 @@ pub(crate) async fn upgrade_composefs( // Or if we have another staged deployment with a different image let staged_image = host.status.staged.as_ref().and_then(|i| i.image.as_ref()); + let do_upgrade_opts = DoUpgradeOpts { + soft_reboot: opts.soft_reboot, + apply: opts.apply, + }; + if let Some(staged_image) = staged_image { // We have a staged image and it has the same digest as the currently booted image's latest // digest @@ -337,7 +359,15 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, booted_imgref, &img_config).await; + return do_upgrade( + storage, + composefs, + &host, + booted_imgref, + &img_config, + &do_upgrade_opts, + ) + .await; } UpdateAction::UpdateOrigin => { @@ -365,7 +395,15 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, booted_imgref, &img_config).await; + return do_upgrade( + storage, + composefs, + &host, + booted_imgref, + &img_config, + &do_upgrade_opts, + ) + .await; } UpdateAction::UpdateOrigin => { @@ -382,11 +420,15 @@ pub(crate) async fn upgrade_composefs( return Ok(()); } - do_upgrade(storage, &host, booted_imgref, &img_config).await?; - - if opts.apply { - return crate::reboot::reboot(); - } + do_upgrade( + storage, + composefs, + &host, + booted_imgref, + &img_config, + &do_upgrade_opts, + ) + .await?; Ok(()) } diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 0c542fd69..1ff141ebf 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize}; use tempfile::tempdir_in; use crate::bootc_composefs::delete::delete_composefs_deployment; -use crate::bootc_composefs::soft_reboot::soft_reboot_to_deployment; +use crate::bootc_composefs::soft_reboot::prepare_soft_reboot_composefs; use crate::bootc_composefs::{ finalize::{composefs_backend_finalize, get_etc_diff}, rollback::composefs_rollback, @@ -1777,7 +1777,8 @@ async fn run_from_opt(opt: Opt) -> Result<()> { anyhow::bail!("soft-reboot only implemented for composefs") } BootedStorageKind::Composefs(booted_cfs) => { - soft_reboot_to_deployment(&storage, &booted_cfs, &deployment, reboot).await + prepare_soft_reboot_composefs(&storage, &booted_cfs, &deployment, reboot) + .await } } } From c59be87297a61fe7171e7e09f00d41c9a1e8f7c8 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 10 Dec 2025 15:15:50 +0530 Subject: [PATCH 6/9] composefs/uki: Save boot digest Similar to what we do with Type1 entries, we save the SHA256Sum of .linux + .initrd sections of the UKI under `boot_digest` key in the origin file Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 85 +++++++++++++++--------- crates/lib/src/bootc_composefs/update.rs | 21 +++--- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 2857e1bd8..67d5d5229 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -338,6 +338,30 @@ fn compute_boot_digest( Ok(hex::encode(digest)) } +/// Compute SHA256Sum of .linux + .initrd section of the UKI +/// +/// # Arguments +/// * entry - BootEntry containing VMlinuz and Initrd +/// * repo - The composefs repository +#[context("Computing boot digest")] +pub(crate) fn compute_boot_digest_uki(uki: &[u8]) -> Result { + let vmlinuz = composefs_boot::uki::get_section(uki, ".linux") + .ok_or_else(|| anyhow::anyhow!(".linux not present"))??; + + let initramfs = composefs_boot::uki::get_section(uki, ".initrd") + .ok_or_else(|| anyhow::anyhow!(".initrd not present"))??; + + 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")?; + + Ok(hex::encode(digest)) +} + /// Given the SHA256 sum of current VMlinuz + Initrd combo, find boot entry with the same SHA256Sum /// /// # Returns @@ -773,10 +797,11 @@ pub(crate) fn setup_composefs_bls_boot( Ok(boot_digest) } -struct UKILabels { +struct UKIInfo { boot_label: String, version: Option, os_id: Option, + boot_digest: String, } /// Writes a PortableExecutable to ESP along with any PE specific or Global addons @@ -790,10 +815,10 @@ fn write_pe_to_esp( is_insecure_from_opts: bool, mounted_efi: impl AsRef, bootloader: &Bootloader, -) -> Result> { +) -> Result> { let efi_bin = read_file(file, &repo).context("Reading .efi binary")?; - let mut boot_label: Option = None; + let mut boot_label: Option = None; // UKI Extension might not even have a cmdline // TODO: UKI Addon might also have a composefs= cmdline? @@ -828,10 +853,13 @@ fn write_pe_to_esp( let parsed_osrel = OsReleaseInfo::parse(osrel); - boot_label = Some(UKILabels { + let boot_digest = compute_boot_digest_uki(&efi_bin)?; + + boot_label = Some(UKIInfo { boot_label: uki::get_boot_label(&efi_bin).context("Getting UKI boot label")?, version: parsed_osrel.get_version(), os_id: parsed_osrel.get_value(&["ID"]), + boot_digest, }); } @@ -981,7 +1009,7 @@ fn write_grub_uki_menuentry( fn write_systemd_uki_config( esp_dir: &Dir, setup_type: &BootSetupType, - boot_label: UKILabels, + boot_label: UKIInfo, id: &Sha512HashValue, ) -> Result<()> { let os_id = boot_label.os_id.as_deref().unwrap_or("bootc"); @@ -1052,7 +1080,7 @@ pub(crate) fn setup_composefs_uki_boot( repo: crate::store::ComposefsRepository, id: &Sha512HashValue, entries: Vec>, -) -> Result<()> { +) -> Result { let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type { BootSetupType::Setup((root_setup, state, postfetch, ..)) => { state.require_no_kargs_for_uki()?; @@ -1085,7 +1113,7 @@ pub(crate) fn setup_composefs_uki_boot( let esp_mount = mount_esp(&esp_device).context("Mounting ESP")?; - let mut uki_label: Option = None; + let mut uki_info: Option = None; for entry in entries { match entry { @@ -1134,28 +1162,26 @@ pub(crate) fn setup_composefs_uki_boot( )?; if let Some(label) = ret { - uki_label = Some(label); + uki_info = Some(label); } } }; } - let uki_label = uki_label - .ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?; + let uki_info = + uki_info.ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?; + + let boot_digest = uki_info.boot_digest.clone(); match bootloader { - Bootloader::Grub => write_grub_uki_menuentry( - root_path, - &setup_type, - uki_label.boot_label, - id, - &esp_device, - )?, + Bootloader::Grub => { + write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)? + } - Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_label, id)?, + Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?, }; - Ok(()) + Ok(boot_digest) } pub struct SecurebootKeys { @@ -1252,20 +1278,15 @@ pub(crate) async fn setup_composefs_boot( }; let boot_type = BootType::from(entry); - let mut boot_digest: Option = None; - - match boot_type { - BootType::Bls => { - let digest = setup_composefs_bls_boot( - BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)), - repo, - &id, - entry, - &mounted_fs, - )?; - boot_digest = Some(digest); - } + let boot_digest = match boot_type { + BootType::Bls => setup_composefs_bls_boot( + BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)), + repo, + &id, + entry, + &mounted_fs, + )?, BootType::Uki => setup_composefs_uki_boot( BootSetupType::Setup((&root_setup, &state, &postfetch, &fs)), repo, diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 025e56a71..84afdb416 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -240,18 +240,15 @@ pub(crate) async fn do_upgrade( )?; let boot_type = BootType::from(entry); - let mut boot_digest = None; - - match boot_type { - BootType::Bls => { - boot_digest = Some(setup_composefs_bls_boot( - BootSetupType::Upgrade((storage, &fs, &host)), - repo, - &id, - entry, - &mounted_fs, - )?) - } + + let boot_digest = match boot_type { + BootType::Bls => setup_composefs_bls_boot( + BootSetupType::Upgrade((storage, &fs, &host)), + repo, + &id, + entry, + &mounted_fs, + )?, BootType::Uki => setup_composefs_uki_boot( BootSetupType::Upgrade((storage, &fs, &host)), From 2bbc6d75e6824918e271d604652b1291f2e8bc92 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 11 Dec 2025 12:21:53 +0530 Subject: [PATCH 7/9] composefs/soft-reboot: Handle soft reboot for UKIs Similar to soft reboots for Type1 entries, we compute the SHA256Sum of .linux + .initrd sections in the UKI, and compare them to check for kernel skew Next, compare the .cmdline section skipping the `composefs=` parameter as that will always be different Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 8 +- crates/lib/src/bootc_composefs/mod.rs | 1 + crates/lib/src/bootc_composefs/state.rs | 63 ++++++--- crates/lib/src/bootc_composefs/status.rs | 160 +++++++++++++++-------- crates/lib/src/bootc_composefs/utils.rs | 58 ++++++++ 5 files changed, 210 insertions(+), 80 deletions(-) create mode 100644 crates/lib/src/bootc_composefs/utils.rs diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 67d5d5229..8a7086ac7 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -79,11 +79,8 @@ use cap_std_ext::{ use clap::ValueEnum; use composefs::fs::read_file; use composefs::tree::RegularFile; +use composefs_boot::bootloader::{PEType, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT}; use composefs_boot::BootOps; -use composefs_boot::{ - bootloader::{PEType, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT}, - uki::UkiError, -}; use fn_error_context::context; use ostree_ext::composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use ostree_ext::composefs_boot::bootloader::UsrLibModulesVmlinuz; @@ -848,8 +845,7 @@ fn write_pe_to_esp( ); } - let osrel = uki::get_text_section(&efi_bin, ".osrel") - .ok_or(UkiError::PortableExecutableError)??; + let osrel = uki::get_text_section(&efi_bin, ".osrel")?; let parsed_osrel = OsReleaseInfo::parse(osrel); diff --git a/crates/lib/src/bootc_composefs/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index 3874417c4..c13824014 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -10,3 +10,4 @@ pub(crate) mod state; pub(crate) mod status; pub(crate) mod switch; pub(crate) mod update; +pub(crate) mod utils; diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index f8e625032..cc7b1e78b 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -1,5 +1,4 @@ use std::io::Write; -use std::ops::Deref; use std::os::unix::fs::symlink; use std::path::Path; use std::{fs::create_dir_all, process::Command}; @@ -108,20 +107,22 @@ pub(crate) fn copy_etc_to_state( cp_ret } -/// Updates the currently booted image's target imgref -pub(crate) fn update_target_imgref_in_origin( +/// Adds or updates the provided key/value pairs in the .origin file of the deployment pointed to +/// by the `deployment_id` +fn add_update_in_origin( storage: &Storage, - booted_cfs: &BootedComposefs, - imgref: &ImageReference, + deployment_id: &str, + section: &str, + kv_pairs: &[(&str, &str)], ) -> Result<()> { - let path = Path::new(STATE_DIR_RELATIVE).join(booted_cfs.cmdline.digest.deref()); + let path = Path::new(STATE_DIR_RELATIVE).join(deployment_id); let state_dir = storage .physical_root .open_dir(path) .context("Opening state dir")?; - let origin_filename = format!("{}.origin", booted_cfs.cmdline.digest.deref()); + let origin_filename = format!("{deployment_id}.origin"); let origin_file = state_dir .read_to_string(&origin_filename) @@ -130,11 +131,9 @@ pub(crate) fn update_target_imgref_in_origin( let mut ini = tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?; - // Replace the origin - ini = ini.section("origin").item( - ORIGIN_CONTAINER, - format!("ostree-unverified-image:{imgref}"), - ); + for (key, value) in kv_pairs { + ini = ini.section(section).item(*key, *value); + } state_dir .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> { @@ -151,6 +150,36 @@ pub(crate) fn update_target_imgref_in_origin( Ok(()) } +/// Updates the currently booted image's target imgref +pub(crate) fn update_target_imgref_in_origin( + storage: &Storage, + booted_cfs: &BootedComposefs, + imgref: &ImageReference, +) -> Result<()> { + add_update_in_origin( + storage, + booted_cfs.cmdline.digest.as_ref(), + "origin", + &[( + ORIGIN_CONTAINER, + &format!("ostree-unverified-image:{imgref}"), + )], + ) +} + +pub(crate) fn update_boot_digest_in_origin( + storage: &Storage, + digest: &str, + boot_digest: &str, +) -> Result<()> { + add_update_in_origin( + storage, + digest, + ORIGIN_KEY_BOOT, + &[(ORIGIN_KEY_BOOT_DIGEST, boot_digest)], + ) +} + /// Creates and populates the composefs state directory for a deployment. /// /// This function sets up the state directory structure and configuration files @@ -184,7 +213,7 @@ pub(crate) async fn write_composefs_state( target_imgref: &ImageReference, staged: bool, boot_type: BootType, - boot_digest: Option, + boot_digest: String, container_details: &ImgConfigManifest, ) -> Result<()> { let state_path = root_path @@ -223,11 +252,9 @@ pub(crate) async 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); - } + config = config + .section(ORIGIN_KEY_BOOT) + .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest); let state_dir = Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?; diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 0277a8962..579c506d9 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -7,7 +7,11 @@ use fn_error_context::context; use serde::{Deserialize, Serialize}; use crate::{ - bootc_composefs::{boot::BootType, repo::get_imgref}, + bootc_composefs::{ + boot::BootType, + repo::get_imgref, + utils::{compute_store_boot_digest_for_uki, get_uki_cmdline}, + }, composefs_consts::{ COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG, }, @@ -350,26 +354,33 @@ pub(crate) async fn get_composefs_status( composefs_deployment_status_from(&storage, booted_cfs.cmdline).await } -fn set_soft_reboot_capable_bls( +/// Check whether any deployment is capable of being soft rebooted or not +#[context("Checking soft reboot capability")] +fn set_soft_reboot_capability( storage: &Storage, host: &mut Host, - bls_entries: &Vec, + bls_entries: Option>, cmdline: &ComposefsCmdline, ) -> Result<()> { let booted = host.require_composefs_booted()?; match booted.boot_type { BootType::Bls => { - set_reboot_capable_type1_deployments(storage, cmdline, host, bls_entries)?; - } + let mut bls_entries = + bls_entries.ok_or_else(|| anyhow::anyhow!("BLS entries not provided"))?; - BootType::Uki => match booted.bootloader { - Bootloader::Grub => todo!(), - Bootloader::Systemd => todo!(), - }, - }; + let staged_entries = + get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, false)?; - Ok(()) + // We will have a duplicate booted entry here, but that's fine as we only use this + // vector to check for existence of an entry + bls_entries.extend(staged_entries); + + set_reboot_capable_type1_deployments(cmdline, host, bls_entries) + } + + BootType::Uki => set_reboot_capable_uki_deployments(storage, cmdline, host), + } } fn find_bls_entry<'a>( @@ -406,43 +417,73 @@ fn compare_cmdline_skip_cfs(first: &Cmdline<'_>, second: &Cmdline<'_>) -> bool { return true; } -fn set_soft_reboot_capable_type1( - deployment: &mut BootEntry, - bls_entries: &Vec, - booted_bls_entry: &BLSConfig, - booted_boot_digest: &String, +#[context("Setting soft reboot capability for Type1 entries")] +fn set_reboot_capable_type1_deployments( + booted_cmdline: &ComposefsCmdline, + host: &mut Host, + bls_entries: Vec, ) -> Result<()> { - let deployment_cfs = deployment.require_composefs()?; + let booted = host + .status + .booted + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?; + + let booted_boot_digest = booted.composefs_boot_digest()?; + + let booted_bls_entry = find_bls_entry(&*booted_cmdline.digest, &bls_entries)? + .ok_or_else(|| anyhow::anyhow!("Booted BLS entry not found"))?; - // TODO: Unwrap - if deployment_cfs.boot_digest.as_ref().unwrap() != booted_boot_digest { - deployment.soft_reboot_capable = false; - return Ok(()); + let booted_cmdline = booted_bls_entry.get_cmdline()?; + + for depl in host + .status + .staged + .iter_mut() + .chain(host.status.rollback.iter_mut()) + .chain(host.status.other_deployments.iter_mut()) + { + let entry = find_bls_entry(&depl.require_composefs()?.verity, &bls_entries)? + .ok_or_else(|| anyhow::anyhow!("Entry not found"))?; + + let depl_cmdline = entry.get_cmdline()?; + + depl.soft_reboot_capable = is_soft_rebootable( + depl.composefs_boot_digest()?, + booted_boot_digest, + depl_cmdline, + booted_cmdline, + ); } - let entry = find_bls_entry(&deployment_cfs.verity, bls_entries)? - .ok_or_else(|| anyhow::anyhow!("Entry not found"))?; + Ok(()) +} - let opts = entry.get_cmdline()?; - let booted_cmdline_opts = booted_bls_entry.get_cmdline()?; +fn is_soft_rebootable( + depl_boot_digest: &str, + booted_boot_digest: &str, + depl_cmdline: &Cmdline, + booted_cmdline: &Cmdline, +) -> bool { + if depl_boot_digest != booted_boot_digest { + tracing::debug!("Soft reboot not allowed due to kernel skew"); + return false; + } - if opts.len() != booted_cmdline_opts.len() { + if depl_cmdline.as_bytes().len() != booted_cmdline.as_bytes().len() { tracing::debug!("Soft reboot not allowed due to differing cmdline"); - deployment.soft_reboot_capable = false; - return Ok(()); + return false; } - deployment.soft_reboot_capable = compare_cmdline_skip_cfs(opts, booted_cmdline_opts) - && compare_cmdline_skip_cfs(booted_cmdline_opts, opts); - - return Ok(()); + return compare_cmdline_skip_cfs(depl_cmdline, booted_cmdline) + && compare_cmdline_skip_cfs(booted_cmdline, depl_cmdline); } -fn set_reboot_capable_type1_deployments( +#[context("Setting soft reboot capability for UKI deployments")] +fn set_reboot_capable_uki_deployments( storage: &Storage, cmdline: &ComposefsCmdline, host: &mut Host, - bls_entries: &Vec, ) -> Result<()> { let booted = host .status @@ -450,29 +491,38 @@ fn set_reboot_capable_type1_deployments( .as_ref() .ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?; - let booted_boot_digest = booted.composefs_boot_digest()?; - - let booted_bls_entry = find_bls_entry(&*cmdline.digest, bls_entries)? - .ok_or_else(|| anyhow::anyhow!("Booted bls entry not found"))?; + // Since older booted systems won't have the boot digest for UKIs + let booted_boot_digest = match booted.composefs_boot_digest() { + Ok(d) => d, + Err(_) => &compute_store_boot_digest_for_uki(storage, &cmdline.digest)?, + }; - if let Some(staged) = host.status.staged.as_mut() { - let staged_entries = - get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, true)?; + let booted_cmdline = get_uki_cmdline(storage, &booted.require_composefs()?.verity)?; - set_soft_reboot_capable_type1( - staged, - &staged_entries, - booted_bls_entry, - booted_boot_digest, - )?; - } + for deployment in host + .status + .staged + .iter_mut() + .chain(host.status.rollback.iter_mut()) + .chain(host.status.other_deployments.iter_mut()) + { + // Since older booted systems won't have the boot digest for UKIs + let depl_boot_digest = match deployment.composefs_boot_digest() { + Ok(d) => d, + Err(_) => &compute_store_boot_digest_for_uki( + storage, + &deployment.require_composefs()?.verity, + )?, + }; - if let Some(rollback) = &mut host.status.rollback { - set_soft_reboot_capable_type1(rollback, bls_entries, booted_bls_entry, booted_boot_digest)?; - } + let depl_cmdline = get_uki_cmdline(storage, &deployment.require_composefs()?.verity)?; - for depl in &mut host.status.other_deployments { - set_soft_reboot_capable_type1(depl, bls_entries, booted_bls_entry, booted_boot_digest)?; + deployment.soft_reboot_capable = is_soft_rebootable( + depl_boot_digest, + booted_boot_digest, + &depl_cmdline, + &booted_cmdline, + ); } Ok(()) @@ -645,9 +695,7 @@ pub(crate) async fn composefs_deployment_status_from( host.spec.boot_order = BootOrder::Rollback }; - if let Some(bls_configs) = sorted_bls_config { - set_soft_reboot_capable_bls(storage, &mut host, &bls_configs, cmdline)?; - } + set_soft_reboot_capability(storage, &mut host, sorted_bls_config, cmdline)?; Ok(host) } diff --git a/crates/lib/src/bootc_composefs/utils.rs b/crates/lib/src/bootc_composefs/utils.rs new file mode 100644 index 000000000..90512a313 --- /dev/null +++ b/crates/lib/src/bootc_composefs/utils.rs @@ -0,0 +1,58 @@ +use crate::{ + bootc_composefs::{ + boot::{compute_boot_digest_uki, SYSTEMD_UKI_DIR}, + state::update_boot_digest_in_origin, + }, + store::Storage, +}; +use anyhow::Result; +use bootc_kernel_cmdline::utf8::Cmdline; +use fn_error_context::context; + +fn get_uki(storage: &Storage, deployment_verity: &str) -> Result> { + let uki_dir = storage + .esp + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ESP not mounted"))? + .fd + .open_dir(SYSTEMD_UKI_DIR)?; + + let req_fname = format!("{deployment_verity}.efi"); + + for entry in uki_dir.entries_utf8()? { + let pe = entry?; + + let filename = pe.file_name()?; + + if filename != req_fname { + continue; + } + + return Ok(uki_dir.read(filename)?); + } + + anyhow::bail!("UKI for deployment {deployment_verity} not found") +} + +#[context("Computing and storing boot digest for UKI")] +pub(crate) fn compute_store_boot_digest_for_uki( + storage: &Storage, + deployment_verity: &str, +) -> Result { + let uki = get_uki(storage, deployment_verity)?; + let digest = compute_boot_digest_uki(&uki)?; + + update_boot_digest_in_origin(storage, &deployment_verity, &digest)?; + return Ok(digest); +} + +#[context("Getting UKI cmdline")] +pub(crate) fn get_uki_cmdline( + storage: &Storage, + deployment_verity: &str, +) -> Result> { + let uki = get_uki(storage, deployment_verity)?; + let cmdline = composefs_boot::uki::get_cmdline(&uki)?; + + return Ok(Cmdline::from(cmdline.to_owned())); +} From 3a1faf704fd11a12482ea22a9ae02a608b7bd692 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 16 Dec 2025 15:13:12 +0530 Subject: [PATCH 8/9] Bump composefs-rs Signed-off-by: Pragyan Poudyal --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01cec42b2..d0888849b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,7 +560,7 @@ checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" [[package]] name = "composefs" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=0f636031a1ec81cdd9e7f674909ef6b75c2642cb#0f636031a1ec81cdd9e7f674909ef6b75c2642cb" +source = "git+https://github.com/containers/composefs-rs?rev=1498349e11d0e4bd545feab533f0b7ab5294ae9a#1498349e11d0e4bd545feab533f0b7ab5294ae9a" dependencies = [ "anyhow", "hex", @@ -580,7 +580,7 @@ dependencies = [ [[package]] name = "composefs-boot" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=0f636031a1ec81cdd9e7f674909ef6b75c2642cb#0f636031a1ec81cdd9e7f674909ef6b75c2642cb" +source = "git+https://github.com/containers/composefs-rs?rev=1498349e11d0e4bd545feab533f0b7ab5294ae9a#1498349e11d0e4bd545feab533f0b7ab5294ae9a" dependencies = [ "anyhow", "composefs", @@ -593,7 +593,7 @@ dependencies = [ [[package]] name = "composefs-oci" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=0f636031a1ec81cdd9e7f674909ef6b75c2642cb#0f636031a1ec81cdd9e7f674909ef6b75c2642cb" +source = "git+https://github.com/containers/composefs-rs?rev=1498349e11d0e4bd545feab533f0b7ab5294ae9a#1498349e11d0e4bd545feab533f0b7ab5294ae9a" dependencies = [ "anyhow", "async-compression", diff --git a/Cargo.toml b/Cargo.toml index ff0288f7a..842960e3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,9 +41,9 @@ chrono = { version = "0.4.38", default-features = false } clap = "4.5.4" clap_mangen = { version = "0.2.20" } # Reviewers (including AI tools): This hash is duplicated as there's not a much better way to handle it right now -composefs = { git = "https://github.com/containers/composefs-rs", rev = "0f636031a1ec81cdd9e7f674909ef6b75c2642cb", package = "composefs", features = ["rhel9"] } -composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "0f636031a1ec81cdd9e7f674909ef6b75c2642cb", package = "composefs-boot" } -composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "0f636031a1ec81cdd9e7f674909ef6b75c2642cb", package = "composefs-oci" } +composefs = { git = "https://github.com/containers/composefs-rs", rev = "1498349e11d0e4bd545feab533f0b7ab5294ae9a", package = "composefs", features = ["rhel9"] } +composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "1498349e11d0e4bd545feab533f0b7ab5294ae9a", package = "composefs-boot" } +composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "1498349e11d0e4bd545feab533f0b7ab5294ae9a", package = "composefs-oci" } fn-error-context = "0.2.1" hex = "0.4.3" indicatif = "0.18.0" From 2a9d6d7e13d8b6d628577f5a0d6f418eac69cd2a Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 17 Dec 2025 13:42:37 +0530 Subject: [PATCH 9/9] composefs/state: Fix imgref format in origin file On `UpdateAction::UpdateOrigin` the origin for the container image used was not properly formatted. Fixed it Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/state.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index cc7b1e78b..8c714d8a0 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -162,7 +162,10 @@ pub(crate) fn update_target_imgref_in_origin( "origin", &[( ORIGIN_CONTAINER, - &format!("ostree-unverified-image:{imgref}"), + &format!( + "ostree-unverified-image:{}", + get_imgref(&imgref.transport, &imgref.image) + ), )], ) }