Rework handling of /run and ${RUNUSER} in blacklist mode
This commit is contained in:
@@ -43,7 +43,7 @@ pub fn resolve_path_context() -> Result<PathContext, SandboxError> {
|
|||||||
let run_user = std::env::var("XDG_RUNTIME_DIR")
|
let run_user = std::env::var("XDG_RUNTIME_DIR")
|
||||||
.ok()
|
.ok()
|
||||||
.or_else(resolve_run_user_from_proc)
|
.or_else(resolve_run_user_from_proc)
|
||||||
.unwrap_or_default();
|
.ok_or(SandboxError::RunUserNotFound)?;
|
||||||
Ok(PathContext { home, run_user })
|
Ok(PathContext { home, run_user })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SandboxError {
|
pub enum SandboxError {
|
||||||
HomeNotSet,
|
HomeNotSet,
|
||||||
|
RunUserNotFound,
|
||||||
BwrapNotFound,
|
BwrapNotFound,
|
||||||
CommandNotFound(PathBuf),
|
CommandNotFound(PathBuf),
|
||||||
CommandNotExecutable(PathBuf),
|
CommandNotExecutable(PathBuf),
|
||||||
@@ -21,6 +22,10 @@ impl std::fmt::Display for SandboxError {
|
|||||||
f,
|
f,
|
||||||
"$HOME is not set; cannot determine which paths to protect"
|
"$HOME is not set; cannot determine which paths to protect"
|
||||||
),
|
),
|
||||||
|
Self::RunUserNotFound => write!(
|
||||||
|
f,
|
||||||
|
"cannot determine XDG_RUNTIME_DIR; tried $XDG_RUNTIME_DIR and /proc/self/status"
|
||||||
|
),
|
||||||
Self::BwrapNotFound => write!(
|
Self::BwrapNotFound => write!(
|
||||||
f,
|
f,
|
||||||
"bwrap not found; install bubblewrap (e.g. `apt install bubblewrap` or `pacman -S bubblewrap`)"
|
"bwrap not found; install bubblewrap (e.g. `apt install bubblewrap` or `pacman -S bubblewrap`)"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
mod agents;
|
mod agents;
|
||||||
mod blacklist;
|
pub mod blacklist;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod preflight;
|
mod preflight;
|
||||||
mod sandbox;
|
mod sandbox;
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ fn add_blacklist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
|||||||
cmd.args(["--proc", "/proc"]);
|
cmd.args(["--proc", "/proc"]);
|
||||||
cmd.args(["--bind", "/tmp", "/tmp"]);
|
cmd.args(["--bind", "/tmp", "/tmp"]);
|
||||||
cmd.args(["--bind", "/var/tmp", "/var/tmp"]);
|
cmd.args(["--bind", "/var/tmp", "/var/tmp"]);
|
||||||
cmd.args(["--bind", "/run", "/run"]);
|
|
||||||
|
|
||||||
let overlays = blacklist::resolve_overlays(&ctx)?;
|
let overlays = blacklist::resolve_overlays(&ctx)?;
|
||||||
for dir in &overlays.tmpfs_dirs {
|
for dir in &overlays.tmpfs_dirs {
|
||||||
@@ -72,6 +71,26 @@ fn add_blacklist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
|||||||
cmd.arg("--ro-bind").arg("/dev/null").arg(file);
|
cmd.arg("--ro-bind").arg("/dev/null").arg(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.args(["--tmpfs", "/run"]);
|
||||||
|
ro_bind_under_tmpfs(
|
||||||
|
cmd,
|
||||||
|
"/run",
|
||||||
|
&[
|
||||||
|
"/run/dbus/system_bus_socket",
|
||||||
|
"/run/systemd/resolve",
|
||||||
|
"/run/systemd/journal",
|
||||||
|
"/run/log/journal",
|
||||||
|
"/run/udev",
|
||||||
|
"/run/NetworkManager/resolv.conf",
|
||||||
|
"/run/media",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure_parent_dirs(cmd, "/run", &ctx.run_user);
|
||||||
|
cmd.arg("--tmpfs").arg(&ctx.run_user);
|
||||||
|
let run_user_bus = format!("{}/bus", ctx.run_user);
|
||||||
|
ro_bind_under_tmpfs(cmd, &ctx.run_user, &[&run_user_bus]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +146,37 @@ fn add_whitelist_mode(cmd: &mut Command) -> Result<(), SandboxError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_parent_dirs(cmd: &mut Command, base: &str, path: &str) {
|
||||||
|
let base = Path::new(base);
|
||||||
|
let ancestors: Vec<_> = Path::new(path)
|
||||||
|
.ancestors()
|
||||||
|
.skip(1)
|
||||||
|
.take_while(|a| *a != base)
|
||||||
|
.collect();
|
||||||
|
for dir in ancestors.into_iter().rev() {
|
||||||
|
cmd.arg("--dir").arg(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ro_bind_under_tmpfs(cmd: &mut Command, base: &str, paths: &[&str]) {
|
||||||
|
let base = Path::new(base);
|
||||||
|
let mut dirs_created = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let ancestors: Vec<_> = Path::new(path)
|
||||||
|
.ancestors()
|
||||||
|
.skip(1) // skip the path itself
|
||||||
|
.take_while(|a| *a != base)
|
||||||
|
.filter(|a| dirs_created.insert(a.to_path_buf()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for dir in ancestors.into_iter().rev() {
|
||||||
|
cmd.arg("--dir").arg(dir);
|
||||||
|
}
|
||||||
|
cmd.args(["--ro-bind-try", path, path]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_rw_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
|
fn add_rw_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err(SandboxError::RwPathMissing(path.to_path_buf()));
|
return Err(SandboxError::RwPathMissing(path.to_path_buf()));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use agent_sandbox::blacklist;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
fn sandbox(extra_args: &[&str]) -> Command {
|
fn sandbox(extra_args: &[&str]) -> Command {
|
||||||
@@ -350,6 +351,62 @@ fn new_session_isolates_sid() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blacklist_run_is_tmpfs() {
|
||||||
|
let output = sandbox(&[])
|
||||||
|
.args([
|
||||||
|
"--",
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
"touch /run/test_canary 2>&1 && echo WRITABLE || echo BLOCKED",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(
|
||||||
|
stdout.contains("WRITABLE"),
|
||||||
|
"expected /run to be a writable tmpfs in blacklist mode, got: {stdout}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blacklist_run_dbus_socket_accessible() {
|
||||||
|
let output = sandbox(&[])
|
||||||
|
.args([
|
||||||
|
"--",
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
"test -e /run/dbus/system_bus_socket && echo EXISTS || echo MISSING",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
assert_eq!(
|
||||||
|
stdout, "EXISTS",
|
||||||
|
"expected /run/dbus/system_bus_socket to be accessible in blacklist mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blacklist_runuser_is_tmpfs() {
|
||||||
|
let ctx = blacklist::resolve_path_context().expect("failed to resolve path context");
|
||||||
|
let script = format!("ls -A {} | grep -v '^bus$'", ctx.run_user);
|
||||||
|
|
||||||
|
let output = sandbox(&[])
|
||||||
|
.args(["--", "bash", "-c", &script])
|
||||||
|
.output()
|
||||||
|
.expect("agent-sandbox binary failed to execute");
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
assert!(
|
||||||
|
stdout.is_empty(),
|
||||||
|
"expected only 'bus' (or empty) in {}, got unexpected entries: {stdout}",
|
||||||
|
ctx.run_user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rw_missing_path_errors() {
|
fn rw_missing_path_errors() {
|
||||||
let output = sandbox(&["--rw", "/nonexistent/xyz"])
|
let output = sandbox(&["--rw", "/nonexistent/xyz"])
|
||||||
|
|||||||
Reference in New Issue
Block a user