From e9d4cf12c2e2828af8957136539529c1f53b15bd Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 3 Dec 2025 12:57:30 +0530 Subject: [PATCH 1/7] composefs/status: Check if deployment is soft rebootable Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/status.rs | 221 ++++++++++++++++++++--- crates/lib/src/cli.rs | 2 +- crates/lib/src/parsers/bls_config.rs | 17 ++ crates/lib/src/status.rs | 7 + 4 files changed, 223 insertions(+), 24 deletions(-) diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 0194d0c33..349c5c5cb 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -6,7 +6,9 @@ use fn_error_context::context; use crate::{ bootc_composefs::boot::BootType, - 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}, @@ -20,7 +22,7 @@ use crate::{ use std::str::FromStr; use bootc_utils::try_deserialize_timestamp; -use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt}; use ostree_container::OstreeImageReference; use ostree_ext::container::deploy::ORIGIN_CONTAINER; use ostree_ext::container::{self as ostree_container}; @@ -92,14 +94,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(); @@ -259,12 +290,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()?; @@ -330,7 +489,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; @@ -351,60 +510,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)) } }; @@ -414,6 +585,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 49d59aebc..ebcd35c20 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 95eb04be2..6f548bdfc 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 debe49f1b265e4f35059c04914d1a5f0feca04da Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 09:39:39 +0530 Subject: [PATCH 2/7] 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 bfe5f188a..c4e3dddd6 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 e4ebf1cb30099072bfc975b4e413e64c34d64ff2 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 10:53:43 +0530 Subject: [PATCH 3/7] 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 ebcd35c20..973197deb 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, @@ -572,6 +573,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)] @@ -1694,6 +1700,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 5bd807e8d..5dff89bdb 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -277,6 +277,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. @@ -344,6 +345,7 @@ impl Host { ty: Some(Slot::Booted), deployment: booted, pinned: false, + soft_reboot_capable: false, }); if let Some(staged) = &self.status.staged { @@ -351,6 +353,7 @@ impl Host { ty: Some(Slot::Staged), deployment: staged.require_composefs()?, pinned: false, + soft_reboot_capable: staged.soft_reboot_capable, }); } @@ -359,6 +362,7 @@ impl Host { ty: Some(Slot::Rollback), deployment: rollback.require_composefs()?, pinned: false, + soft_reboot_capable: rollback.soft_reboot_capable, }); } @@ -367,6 +371,7 @@ impl Host { ty: None, deployment: pinned.require_composefs()?, pinned: true, + soft_reboot_capable: pinned.soft_reboot_capable, }); } From 5f98c9e78e1e214229cb8fdf2f9d7830048f9557 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 12:18:07 +0530 Subject: [PATCH 4/7] 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 349c5c5cb..8b55233a4 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 crate::{ @@ -78,7 +79,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 87262970ad0d59a3607464359689cf361b2c7793 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 4 Dec 2025 13:57:22 +0530 Subject: [PATCH 5/7] 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 | 12 ++++-- crates/lib/src/bootc_composefs/update.rs | 38 ++++++++++++++----- crates/lib/src/cli.rs | 5 ++- 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 30ecc5ccc..38d46e068 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -1212,7 +1212,7 @@ pub(crate) 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 18ef34e0a..683d9d6d6 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -155,7 +155,7 @@ pub(crate) fn update_target_imgref_in_origin( #[context("Writing composefs state")] pub(crate) fn write_composefs_state( root_path: &Utf8PathBuf, - deployment_id: Sha512HashValue, + deployment_id: &Sha512HashValue, 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 4f0190f54..b37034e87 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, manifest, _) = 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,8 @@ pub(crate) async fn switch_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, &target_imgref).await; + return do_upgrade(storage, booted_cfs, &host, &target_imgref, &do_upgrade_opts) + .await; } UpdateAction::UpdateOrigin => { @@ -71,7 +77,7 @@ pub(crate) async fn switch_composefs( } } - do_upgrade(storage, &host, &target_imgref).await?; + do_upgrade(storage, booted_cfs, &host, &target_imgref, &do_upgrade_opts).await?; Ok(()) } diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 37578fad4..553869d33 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -15,10 +15,11 @@ 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}, }, - 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}, @@ -205,12 +206,20 @@ 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, + opts: &DoUpgradeOpts, ) -> Result<()> { start_finalize_stated_svc()?; @@ -250,13 +259,21 @@ pub(crate) async fn do_upgrade( write_composefs_state( &Utf8PathBuf::from("/sysroot"), - id, + &id, imgref, true, boot_type, boot_digest, )?; + 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(()) } @@ -285,6 +302,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 @@ -325,7 +347,8 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, booted_imgref).await; + return do_upgrade(storage, composefs, &host, booted_imgref, &do_upgrade_opts) + .await; } UpdateAction::UpdateOrigin => { @@ -353,7 +376,8 @@ pub(crate) async fn upgrade_composefs( } UpdateAction::Proceed => { - return do_upgrade(storage, &host, booted_imgref).await; + return do_upgrade(storage, composefs, &host, booted_imgref, &do_upgrade_opts) + .await; } UpdateAction::UpdateOrigin => { @@ -390,11 +414,7 @@ pub(crate) async fn upgrade_composefs( return Ok(()); } - do_upgrade(storage, &host, booted_imgref).await?; - - if opts.apply { - return crate::reboot::reboot(); - } + do_upgrade(storage, composefs, &host, booted_imgref, &do_upgrade_opts).await?; Ok(()) } diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 973197deb..302ffacd3 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, @@ -1709,7 +1709,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 e0f64c510c5a0b7f3cc3a9f6ffdcf19c2aa25882 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 10 Dec 2025 15:15:50 +0530 Subject: [PATCH 6/7] 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 38d46e068..e7ee128bd 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -329,6 +329,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 @@ -756,10 +780,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 @@ -773,10 +798,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? @@ -811,10 +836,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, }); } @@ -964,7 +992,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"); @@ -1035,7 +1063,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()?; @@ -1068,7 +1096,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 { @@ -1117,28 +1145,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) } #[context("Setting up composefs boot")] @@ -1188,20 +1214,15 @@ pub(crate) 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 553869d33..633b0477a 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -236,18 +236,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 33c84f1ece497e9c79cc4c443351f78194db1945 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 11 Dec 2025 12:21:53 +0530 Subject: [PATCH 7/7] 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/mod.rs | 1 + crates/lib/src/bootc_composefs/state.rs | 63 ++++++--- crates/lib/src/bootc_composefs/status.rs | 159 +++++++++++++++-------- crates/lib/src/bootc_composefs/utils.rs | 58 +++++++++ 4 files changed, 207 insertions(+), 74 deletions(-) create mode 100644 crates/lib/src/bootc_composefs/utils.rs 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 683d9d6d6..de8e1cea0 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 /sysroot/state/deploy/image_id #[context("Writing composefs state")] pub(crate) fn write_composefs_state( @@ -159,7 +188,7 @@ pub(crate) fn write_composefs_state( imgref: &ImageReference, staged: bool, boot_type: BootType, - boot_digest: Option, + boot_digest: String, ) -> Result<()> { let state_path = root_path .join(STATE_DIR_RELATIVE) @@ -197,11 +226,9 @@ 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); - } + 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 8b55233a4..12229b5ef 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -6,7 +6,10 @@ use bootc_mount::inspect_filesystem; use fn_error_context::context; use crate::{ - bootc_composefs::boot::BootType, + bootc_composefs::{ + boot::BootType, + 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, }, @@ -307,26 +310,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>( @@ -363,43 +373,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 @@ -407,29 +447,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(()) @@ -602,9 +651,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())); +}