diff --git a/rust/src/isolation.rs b/rust/src/isolation.rs new file mode 100644 index 00000000..68fb12c3 --- /dev/null +++ b/rust/src/isolation.rs @@ -0,0 +1,52 @@ +//! APIs for multi-process isolation +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use anyhow::{anyhow, Result}; +use fn_error_context::context; +use std::process::Command; + +const SELF_UNIT: &str = "rpm-ostreed.service"; +/// Run as a child process, synchronously. +const BASE_ARGS: &[&str] = &["--wait", "--pipe", "--no-ask-password", "--quiet"]; + +/// Configuration for transient unit. +pub(crate) struct UnitConfig<'a> { + /// If provided, will be used as the name of the unit + pub(crate) name: Option<&'a str>, + /// Unit/Service properties, e.g. DynamicUser=yes + pub(crate) properties: &'a [&'a str], + /// The command to execute + pub(crate) exec_args: &'a [&'a str], +} + +/// Create a child process via `systemd-run` and synchronously wait +/// for its completion. This runs in `--pipe` mode, so e.g. stdout/stderr +/// will go to the parent process. +/// Use this for isolation, as well as to escape the parent rpm-ostreed.service +/// isolation like `ProtectHome=true`. +#[context("Running systemd worker")] +pub(crate) fn run_systemd_worker_sync(cfg: &UnitConfig) -> Result<()> { + if !systemd::daemon::booted()? { + return Err(anyhow!("Not running under systemd")); + } + let mut cmd = Command::new("systemd-run"); + cmd.args(BASE_ARGS); + if let Some(name) = cfg.name { + cmd.arg("--unit"); + cmd.arg(name); + } + for prop in cfg.properties.iter() { + cmd.arg("--property"); + cmd.arg(prop); + } + // This ensures that this unit won't escape our process. + cmd.arg(format!("--property=BindsTo={}", SELF_UNIT)); + cmd.arg(format!("--property=After={}", SELF_UNIT)); + cmd.arg("--"); + cmd.args(cfg.exec_args); + let status = cmd.status()?; + if !status.success() { + return Err(anyhow!("{}", status)); + } + Ok(()) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index e9dbceb2..e75e19f4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -379,6 +379,7 @@ pub(crate) use extensions::*; mod fedora_integration; mod history; pub use self::history::*; +mod isolation; mod journal; pub(crate) use self::journal::*; mod initramfs; diff --git a/rust/src/live.rs b/rust/src/live.rs index 2ad3751b..5bbae999 100644 --- a/rust/src/live.rs +++ b/rust/src/live.rs @@ -8,8 +8,10 @@ */ use crate::ffi::LiveApplyState; +use crate::isolation; use crate::{cxxrsutil::*, variant_utils}; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, Context, Result}; +use fn_error_context::context; use nix::sys::statvfs; use openat_ext::OpenatDirExt; use ostree::DeploymentUnlockedState; @@ -18,7 +20,6 @@ use std::borrow::Cow; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::pin::Pin; -use std::process::Command; use variant_utils::{variant_dict_lookup_bool, variant_dict_lookup_str}; /// GVariant `s`: Choose a specific commit @@ -301,50 +302,34 @@ fn update_etc( // we actually need to escape our mount namespace and affect // the "main" mount namespace so that other processes will // see the overlayfs. +#[context("Creating overlayfs")] fn unlock_transient(sysroot: &ostree::Sysroot) -> Result<()> { // Temporarily drop the lock sysroot.unlock(); - let status = Command::new("systemd-run") - .args(&[ - "-u", - "rpm-ostree-unlock", - "--wait", - "--", - "ostree", - "admin", - "unlock", - "--transient", - ]) - .status(); - sysroot.lock()?; - let status = status?; - if !status.success() { - bail!("Failed to unlock --transient"); - } + isolation::run_systemd_worker_sync(&isolation::UnitConfig { + name: Some("rpm-ostree-unlock"), + properties: &[], + exec_args: &["ostree", "admin", "unlock", "--transient"], + })?; Ok(()) } -/// Run `systemd-tmpfiles` via `systemd-run` so we escape our mount namespace. -/// This allows our `ProtectHome=` in the unit file to work. +/// Run `systemd-tmpfiles` as a separate systemd unit to escape +/// our mount namespace. +/// This allows our `ProtectHome=` in the unit file to work +/// for example. Longer term I'd like to protect even more of `/var`. +#[context("Running tmpfiles for /run and /var")] fn rerun_tmpfiles() -> Result<()> { - for prefix in &["/run", "/var"] { - let status = Command::new("systemd-run") - .args(&[ - "-u", - "rpm-ostree-tmpfiles", - "--wait", - "--", - "systemd-tmpfiles", - "--create", - "--prefix", - prefix, - ]) - .status()?; - if !status.success() { - bail!("Failed to invoke systemd-tmpfiles"); - } - } - Ok(()) + isolation::run_systemd_worker_sync(&isolation::UnitConfig { + name: Some("rpm-ostree-tmpfiles"), + properties: &[], + exec_args: &[ + "systemd-tmpfiles", + "--create", + "--prefix=/run", + "--prefix=/var", + ], + }) } fn get_required_booted_deployment(sysroot: &ostree::Sysroot) -> Result { @@ -508,7 +493,7 @@ pub(crate) fn transaction_apply_live( &openat::Dir::open("/etc")?, )?; std::mem::drop(task); - let task = crate::ffi::progress_begin_task("Running systemd-tmpfiles for /var"); + let task = crate::ffi::progress_begin_task("Running systemd-tmpfiles for /run and /var"); rerun_tmpfiles()?; std::mem::drop(task);