rust: Introduce systemd-run based isolation mod, use in live

I was thinking about privilege separation today with
systemd units, and that led me to the problem of "lifecycle binding".
We really want e.g. `systemctl stop rpm-ostreed` to kill any
separate systemd units we're managing.

systemd already has a mechanism for this with `BindsTo=`.

And then I realized we weren't doing this for the systemd-tmpfiles
invocations in the `live.rs` code.

Generalize this into a small `isolation` module that fixes this
and several other things at the same time.  I'd like to build
on this to further improve our multi-process isolation story
later.
This commit is contained in:
Colin Walters 2021-03-11 21:54:42 +00:00
parent 445af087d6
commit 9126831b8b
3 changed files with 78 additions and 40 deletions

52
rust/src/isolation.rs Normal file
View File

@ -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(())
}

View File

@ -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;

View File

@ -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<ostree::Deployment> {
@ -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);