mod agents; mod blacklist; pub mod cli; pub mod config; mod env; mod errors; mod memfd; mod persistent_tmp; mod preflight; mod process_tree; mod sandbox; mod sandbox_info; mod seccomp; mod session_key; mod signals; pub use errors::SandboxError; pub use signals::exit_with_status; use std::ffi::OsString; use std::fs; use std::path::PathBuf; use std::process::ExitStatus; use std::time::Duration; use crate::sandbox::Bwrap; use crate::signals::ForwardedSignals; const CLEANUP_GRACE_PERIOD: Duration = Duration::from_secs(5); pub enum SandboxMode { Blacklist, Whitelist, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum EnvEntry { Keep(String), Set(String, String), } impl EnvEntry { pub fn key(&self) -> &str { match self { Self::Keep(k) => k, Self::Set(k, _) => k, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct BindSpec { pub source: PathBuf, pub target: PathBuf, } pub struct SandboxConfig { pub mode: SandboxMode, pub hardened: bool, pub unshare_net: bool, pub seccomp: bool, pub env_filter: bool, pub extra_rw: Vec, pub extra_ro: Vec, pub mask: Vec, pub env: Vec, pub unsetenv: Vec, pub bwrap_args: Vec, pub command: PathBuf, pub command_args: Vec, pub chdir: PathBuf, pub dry_run: bool, pub persistent_tmp_key: Option, } pub fn require_home() -> Result { std::env::var("HOME") .ok() .filter(|h| !h.is_empty()) .ok_or(SandboxError::HomeNotSet) } pub fn require_run_user() -> Result { std::env::var("XDG_RUNTIME_DIR") .ok() .or_else(resolve_run_user_from_proc) .ok_or(SandboxError::RunUserNotFound) } fn resolve_run_user_from_proc() -> Option { let status = fs::read_to_string("/proc/self/status").ok()?; for line in status.lines() { if let Some(rest) = line.strip_prefix("Uid:") { let uid = rest.split_whitespace().next()?; return Some(format!("/run/user/{uid}")); } } None } pub fn run(config: SandboxConfig) -> Result { preflight::check(&config)?; let forwarded_signals = ForwardedSignals::detect(); let mut bwrap = Bwrap::build(&config, &forwarded_signals.build_signal_reset_prefix())?; if config.dry_run { println!("{}", bwrap.shell_quoted()); return Ok(ExitStatus::default()); } process_tree::become_subreaper()?; let signal_forwarder = forwarded_signals.capture()?; // bwrap ignores them: its death would SIGKILL the entrypoint through --die-with-parent. let bwrap_process = bwrap.spawn(signal_forwarder.ignore_forwarded_signals())?; let status = signal_forwarder.forward_until_exit(&bwrap_process)?; process_tree::terminate_and_reap_all(CLEANUP_GRACE_PERIOD, signals::has_pending_interrupt)?; Ok(status) }