Skip to main content

bootc_lib/bootc_composefs/
soft_reboot.rs

1use crate::{
2    bootc_composefs::{
3        service::start_finalize_stated_svc,
4        status::{build_composefs_karg, get_composefs_status},
5    },
6    cli::SoftRebootMode,
7    store::{BootedComposefs, Storage},
8};
9use anyhow::{Context, Result};
10use bootc_initramfs_setup::setup_root;
11use bootc_mount::{PID1, bind_mount_from_pidns};
12use camino::Utf8Path;
13use cap_std_ext::cap_std::ambient_authority;
14use cap_std_ext::cap_std::fs::Dir;
15use cap_std_ext::dirext::CapStdExtDirExt;
16use composefs_ctl::composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
17use fn_error_context::context;
18use linux_kernel_cmdline::utf8::Cmdline;
19use ostree_ext::systemd_has_soft_reboot;
20use rustix::mount::{UnmountFlags, unmount};
21use std::{fs::create_dir_all, os::unix::process::CommandExt, path::PathBuf, process::Command};
22
23const NEXTROOT: &str = "/run/nextroot";
24
25#[context("Resetting soft reboot state")]
26pub(crate) fn reset_soft_reboot() -> Result<()> {
27    // NOTE: By default bootc runs in an unshared mount namespace;
28    // this sets up our /runto actually be the same as the host/run
29    // so the umount (at the end of this function) actually affects the host
30    //
31    // Similar operation is performed in `prepare_soft_reboot_composefs`
32    let run = Utf8Path::new("/run");
33    bind_mount_from_pidns(PID1, &run, &run, true).context("Bind mounting /run")?;
34
35    let run_dir = Dir::open_ambient_dir("/run", ambient_authority()).context("Opening run")?;
36
37    let nextroot = run_dir
38        .open_dir_optional("nextroot")
39        .context("Opening nextroot")?;
40
41    let Some(nextroot) = nextroot else {
42        tracing::debug!("Nextroot does not exist");
43        println!("No deployment staged for soft rebooting");
44        return Ok(());
45    };
46
47    let nextroot_mounted = nextroot
48        .is_mountpoint(".")?
49        .ok_or_else(|| anyhow::anyhow!("Failed to get mount info"))?;
50
51    if !nextroot_mounted {
52        tracing::debug!("Nextroot is not a mountpoint");
53        println!("No deployment staged for soft rebooting");
54        return Ok(());
55    }
56
57    unmount(NEXTROOT, UnmountFlags::DETACH).context("Unmounting nextroot")?;
58
59    println!("Cleared soft reboot queued state");
60
61    Ok(())
62}
63
64/// Checks if the provided deployment is soft reboot capable, and soft reboots the system if
65/// argument `reboot` is true
66#[context("Soft rebooting")]
67pub(crate) async fn prepare_soft_reboot_composefs(
68    storage: &Storage,
69    booted_cfs: &BootedComposefs,
70    deployment_id: Option<&str>,
71    soft_reboot_mode: SoftRebootMode,
72    reboot: bool,
73) -> Result<()> {
74    if !systemd_has_soft_reboot() {
75        anyhow::bail!("System does not support soft reboots")
76    }
77
78    let deployment_id = deployment_id.ok_or_else(|| anyhow::anyhow!("Expected deployment id"))?;
79
80    if *deployment_id == *booted_cfs.cmdline.digest {
81        anyhow::bail!("Cannot soft-reboot to currently booted deployment");
82    }
83
84    // We definitely need to re-query the state as some deployment might've been staged
85    let host = get_composefs_status(storage, &booted_cfs).await?;
86
87    let all_deployments = host.all_composefs_deployments()?;
88
89    let required_deployment = all_deployments
90        .iter()
91        .find(|entry| entry.deployment.verity == *deployment_id)
92        .ok_or_else(|| anyhow::anyhow!("Deployment '{deployment_id}' not found"))?;
93
94    if !required_deployment.soft_reboot_capable {
95        match soft_reboot_mode {
96            SoftRebootMode::Required => {
97                anyhow::bail!("Cannot soft-reboot to deployment with a different kernel state")
98            }
99
100            SoftRebootMode::Auto => return Ok(()),
101        }
102    }
103
104    start_finalize_stated_svc()?;
105
106    // escape to global mnt namespace
107    let run = Utf8Path::new("/run");
108    bind_mount_from_pidns(PID1, &run, &run, false).context("Bind mounting /run")?;
109
110    create_dir_all(NEXTROOT).context("Creating nextroot")?;
111
112    let deployment_digest = Sha512HashValue::from_hex(deployment_id)
113        .with_context(|| format!("Parsing deployment id '{deployment_id}'"))?;
114    // We don't persist which EROFS format each deployment was written with, so
115    // fall back to the repo's currently configured default.  This only affects
116    // the karg's self-description, not whether the soft-reboot actually
117    // succeeds: `setup_root` (below) resolves the deployment purely from the
118    // digest, independent of the composefs=/composefs.digest= tag.
119    let cmdline = build_composefs_karg(
120        deployment_digest,
121        booted_cfs.repo.erofs_version(),
122        booted_cfs.cmdline.allow_missing_fsverity,
123    );
124
125    let args = bootc_initramfs_setup::Args {
126        cmd: vec![],
127        sysroot: PathBuf::from("/sysroot"),
128        config: Default::default(),
129        root_fs: None,
130        cmdline: Some(Cmdline::from(cmdline)),
131        target: Some(NEXTROOT.into()),
132    };
133
134    setup_root(args)?;
135
136    println!("Soft reboot setup complete");
137
138    if reboot {
139        // Replacing the current process should be fine as we restart userspace anyway
140        let err = Command::new("systemctl").arg("soft-reboot").exec();
141        return Err(anyhow::Error::from(err).context("Failed to exec 'systemctl soft-reboot'"));
142    }
143
144    Ok(())
145}