use std::path::{Path, PathBuf}; use std::process::Command; use crate::agents; use crate::blacklist; use crate::env; use crate::seccomp; use crate::{EnvEntry, SandboxConfig, SandboxError, SandboxMode}; pub fn build_command(config: &SandboxConfig) -> Result { let mut cmd = Command::new("bwrap"); let hardened = config.hardened || matches!(config.mode, SandboxMode::Whitelist); if hardened { cmd.args(["--unshare-ipc", "--unshare-pid", "--unshare-uts"]); cmd.args(["--hostname", "sandbox"]); } if config.unshare_net { cmd.arg("--unshare-net"); } match config.mode { SandboxMode::Blacklist => add_blacklist_mode(&mut cmd)?, SandboxMode::Whitelist => add_whitelist_mode(&mut cmd)?, } for path in agents::agent_rw_paths() { cmd.arg("--bind-try").arg(&path).arg(&path); } for path in &config.extra_ro { add_ro_bind(&mut cmd, path)?; } add_rw_bind(&mut cmd, &config.chdir)?; for path in &config.extra_rw { add_rw_bind(&mut cmd, path)?; } add_env_policy(&mut cmd, config); add_user_env_overrides(&mut cmd, config); cmd.args(["--remount-ro", "/"]); cmd.arg("--die-with-parent"); cmd.arg("--chdir").arg(&config.chdir); apply_masks(&mut cmd, &config.mask); if config.seccomp { add_seccomp_filter(&mut cmd)?; } cmd.args(&config.bwrap_args); cmd.arg("--") .arg(&config.command) .args(&config.command_args); Ok(cmd) } fn add_env_policy(cmd: &mut Command, config: &SandboxConfig) { if !config.env_filter { return; } let parent_env: Vec<(String, String)> = std::env::vars().collect(); let args = match config.mode { SandboxMode::Blacklist => env::blacklist_env_args(&parent_env), SandboxMode::Whitelist => env::whitelist_env_args(&parent_env), }; cmd.args(args); } fn add_user_env_overrides(cmd: &mut Command, config: &SandboxConfig) { let mut keep_keys: Vec = Vec::new(); for entry in &config.env { match entry { EnvEntry::Set(key, value) => { cmd.arg("--setenv").arg(key).arg(value); } EnvEntry::Keep(key) => keep_keys.push(key.clone()), } } if !keep_keys.is_empty() { let parent_env: Vec<(String, String)> = std::env::vars().collect(); cmd.args(env::keepenv_args(&keep_keys, &parent_env)); } for key in &config.unsetenv { cmd.arg("--unsetenv").arg(key); } } fn apply_masks(cmd: &mut Command, masks: &[PathBuf]) { for path in masks { if path.is_file() { cmd.arg("--ro-bind").arg("/dev/null").arg(path); } else { cmd.arg("--tmpfs").arg(path); } } } fn add_blacklist_mode(cmd: &mut Command) -> Result<(), SandboxError> { let ctx = blacklist::resolve_path_context()?; cmd.args(["--ro-bind", "/", "/"]); cmd.args(["--dev-bind", "/dev", "/dev"]); cmd.args(["--proc", "/proc"]); cmd.args(["--bind", "/tmp", "/tmp"]); cmd.args(["--bind", "/var/tmp", "/var/tmp"]); let overlays = blacklist::resolve_overlays(&ctx)?; for dir in &overlays.tmpfs_dirs { cmd.arg("--tmpfs").arg(dir); } for file in &overlays.null_bind_files { 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(()) } fn add_whitelist_mode(cmd: &mut Command) -> Result<(), SandboxError> { let home = crate::require_home()?; cmd.args(["--ro-bind", "/usr", "/usr"]); for path in ["/lib", "/lib64", "/lib32", "/bin", "/sbin"] { cmd.args(["--ro-bind-try", path, path]); } for path in [ "/etc/ld.so.cache", "/etc/ld.so.conf", "/etc/ld.so.conf.d", "/etc/alternatives", "/etc/ssl", "/etc/ca-certificates", "/etc/resolv.conf", "/etc/nsswitch.conf", "/etc/passwd", "/etc/group", "/etc/hosts", "/etc/gai.conf", "/etc/services", "/etc/protocols", "/etc/hostname", "/etc/localtime", "/etc/machine-id", ] { cmd.args(["--ro-bind-try", path, path]); } cmd.args(["--ro-bind-try", "/sys", "/sys"]); let local_bin = format!("{home}/.local/bin"); cmd.arg("--ro-bind-try").arg(&local_bin).arg(&local_bin); let cache_dir = format!("{home}/.cache"); cmd.arg("--tmpfs").arg(&cache_dir); cmd.args(["--tmpfs", "/tmp"]); cmd.args(["--tmpfs", "/var/tmp"]); cmd.args(["--dev", "/dev"]); cmd.args(["--tmpfs", "/dev/shm"]); cmd.args(["--tmpfs", "/run"]); cmd.args(["--proc", "/proc"]); 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> { let source = resolve_bind_source(path)?; cmd.arg("--bind").arg(source).arg(path); Ok(()) } fn add_ro_bind(cmd: &mut Command, path: &Path) -> Result<(), SandboxError> { let source = resolve_bind_source(path)?; cmd.arg("--ro-bind").arg(source).arg(path); Ok(()) } fn resolve_bind_source(path: &Path) -> Result { std::fs::canonicalize(path).map_err(|_| SandboxError::PathMissing(path.to_path_buf())) } fn add_seccomp_filter(cmd: &mut Command) -> Result<(), SandboxError> { let fd = seccomp::write_program_to_memfd()?; cmd.arg("--seccomp").arg(fd.to_string()); Ok(()) }