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(()) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 30ecc5ccc..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, @@ -1212,7 +1233,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/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index a9ced452d..c13824014 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -5,7 +5,9 @@ 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; pub(crate) mod update; +pub(crate) mod utils; 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..4d1f3cf31 --- /dev/null +++ b/crates/lib/src/bootc_composefs/soft_reboot.rs @@ -0,0 +1,78 @@ +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 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"; + +/// 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()?; + + 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/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 18ef34e0a..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,15 +150,45 @@ 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( root_path: &Utf8PathBuf, - deployment_id: Sha512HashValue, + deployment_id: &Sha512HashValue, 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 0194d0c33..12229b5ef 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -2,11 +2,17 @@ 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::{ - bootc_composefs::boot::BootType, - composefs_consts::{COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, USER_CFG}, + 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, + }, install::EFI_LOADER_INFO, parsers::{ bls_config::{parse_bls_config, BLSConfig, BLSConfigType}, @@ -20,7 +26,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}; @@ -76,7 +82,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()) } @@ -92,14 +114,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 +310,186 @@ pub(crate) async fn get_composefs_status( composefs_deployment_status_from(&storage, booted_cfs.cmdline).await } +/// 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: Option>, + cmdline: &ComposefsCmdline, +) -> Result<()> { + let booted = host.require_composefs_booted()?; + + match booted.boot_type { + BootType::Bls => { + let mut bls_entries = + bls_entries.ok_or_else(|| anyhow::anyhow!("BLS entries not provided"))?; + + let staged_entries = + get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, false)?; + + // 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>( + 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; +} + +#[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 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"))?; + + 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, + ); + } + + Ok(()) +} + +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 depl_cmdline.as_bytes().len() != booted_cmdline.as_bytes().len() { + tracing::debug!("Soft reboot not allowed due to differing cmdline"); + return false; + } + + return compare_cmdline_skip_cfs(depl_cmdline, booted_cmdline) + && compare_cmdline_skip_cfs(booted_cmdline, depl_cmdline); +} + +#[context("Setting soft reboot capability for UKI deployments")] +fn set_reboot_capable_uki_deployments( + storage: &Storage, + cmdline: &ComposefsCmdline, + host: &mut Host, +) -> Result<()> { + let booted = host + .status + .booted + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?; + + // 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)?, + }; + + let booted_cmdline = get_uki_cmdline(storage, &booted.require_composefs()?.verity)?; + + 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, + )?, + }; + + let depl_cmdline = get_uki_cmdline(storage, &deployment.require_composefs()?.verity)?; + + deployment.soft_reboot_capable = is_soft_rebootable( + depl_boot_digest, + booted_boot_digest, + &depl_cmdline, + &booted_cmdline, + ); + } + + 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 +555,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 +576,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 +651,8 @@ pub(crate) async fn composefs_deployment_status_from( host.spec.boot_order = BootOrder::Rollback }; + set_soft_reboot_capability(storage, &mut host, sorted_bls_config, cmdline)?; + Ok(host) } 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..633b0477a 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()?; @@ -227,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)), @@ -250,13 +256,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 +299,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 +344,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 +373,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 +411,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/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())); +} diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 49d59aebc..302ffacd3 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -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::prepare_soft_reboot_composefs; 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,20 @@ 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) => { + prepare_soft_reboot_composefs(&storage, &booted_cfs, &deployment, reboot) + .await + } + } + } }, Opt::State(opts) => match opts { StateOpts::WipeOstree => { 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/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, }); } 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.