//! Bubblewrap Sandbox Integration for APT-OSTree //! //! This module implements bubblewrap integration for secure script execution //! in sandboxed environments, providing proper isolation and security for //! DEB package scripts. use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::collections::HashMap; use tracing::{info, warn, error}; use serde::{Serialize, Deserialize}; use crate::error::{AptOstreeError, AptOstreeResult}; /// Bubblewrap sandbox configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BubblewrapConfig { pub enable_sandboxing: bool, pub bind_mounts: Vec, pub readonly_paths: Vec, pub writable_paths: Vec, pub network_access: bool, pub user_namespace: bool, pub pid_namespace: bool, pub uts_namespace: bool, pub ipc_namespace: bool, pub mount_namespace: bool, pub cgroup_namespace: bool, pub capabilities: Vec, pub seccomp_profile: Option, } /// Bind mount configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BindMount { pub source: PathBuf, pub target: PathBuf, pub readonly: bool, } impl Default for BubblewrapConfig { fn default() -> Self { Self { enable_sandboxing: true, bind_mounts: vec![ // Essential system directories (read-only) BindMount { source: PathBuf::from("/usr"), target: PathBuf::from("/usr"), readonly: true, }, BindMount { source: PathBuf::from("/lib"), target: PathBuf::from("/lib"), readonly: true, }, BindMount { source: PathBuf::from("/lib64"), target: PathBuf::from("/lib64"), readonly: true, }, BindMount { source: PathBuf::from("/bin"), target: PathBuf::from("/bin"), readonly: true, }, BindMount { source: PathBuf::from("/sbin"), target: PathBuf::from("/sbin"), readonly: true, }, // Writable directories BindMount { source: PathBuf::from("/tmp"), target: PathBuf::from("/tmp"), readonly: false, }, BindMount { source: PathBuf::from("/var/tmp"), target: PathBuf::from("/var/tmp"), readonly: false, }, ], readonly_paths: vec![ PathBuf::from("/usr"), PathBuf::from("/lib"), PathBuf::from("/lib64"), PathBuf::from("/bin"), PathBuf::from("/sbin"), ], writable_paths: vec![ PathBuf::from("/tmp"), PathBuf::from("/var/tmp"), ], network_access: false, user_namespace: true, pid_namespace: true, uts_namespace: true, ipc_namespace: true, mount_namespace: true, cgroup_namespace: true, capabilities: vec![ "CAP_CHOWN".to_string(), "CAP_DAC_OVERRIDE".to_string(), "CAP_FOWNER".to_string(), "CAP_FSETID".to_string(), "CAP_KILL".to_string(), "CAP_SETGID".to_string(), "CAP_SETUID".to_string(), "CAP_SETPCAP".to_string(), "CAP_NET_BIND_SERVICE".to_string(), "CAP_SYS_CHROOT".to_string(), "CAP_MKNOD".to_string(), "CAP_AUDIT_WRITE".to_string(), ], seccomp_profile: None, } } } /// Bubblewrap sandbox manager pub struct BubblewrapSandbox { config: BubblewrapConfig, bubblewrap_path: PathBuf, } /// Sandbox execution result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SandboxResult { pub success: bool, pub exit_code: i32, pub stdout: String, pub stderr: String, pub execution_time: std::time::Duration, pub sandbox_id: String, } /// Sandbox environment configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SandboxEnvironment { pub working_directory: PathBuf, pub environment_variables: HashMap, pub bind_mounts: Vec, pub readonly_paths: Vec, pub writable_paths: Vec, pub network_access: bool, pub capabilities: Vec, } impl BubblewrapSandbox { /// Create a new bubblewrap sandbox manager pub fn new(config: BubblewrapConfig) -> AptOstreeResult { info!("Creating bubblewrap sandbox manager"); // Check if bubblewrap is available let bubblewrap_path = Self::find_bubblewrap()?; Ok(Self { config, bubblewrap_path, }) } /// Find bubblewrap executable fn find_bubblewrap() -> AptOstreeResult { let possible_paths = [ "/usr/bin/bwrap", "/usr/local/bin/bwrap", "/bin/bwrap", ]; for path in &possible_paths { if Path::new(path).exists() { info!("Found bubblewrap at: {}", path); return Ok(PathBuf::from(path)); } } Err(AptOstreeError::ScriptExecution( "bubblewrap not found. Please install bubblewrap (bwrap) package.".to_string() )) } /// Execute command in sandboxed environment pub async fn execute_sandboxed( &self, command: &[String], environment: &SandboxEnvironment, ) -> AptOstreeResult { let start_time = std::time::Instant::now(); let sandbox_id = format!("sandbox_{}", chrono::Utc::now().timestamp()); info!("Executing command in sandbox: {:?} (ID: {})", command, sandbox_id); if !self.config.enable_sandboxing { warn!("Sandboxing disabled, executing without bubblewrap"); return self.execute_without_sandbox(command, environment).await; } // Build bubblewrap command let mut bwrap_cmd = Command::new(&self.bubblewrap_path); // Add namespace options if self.config.user_namespace { bwrap_cmd.arg("--unshare-user"); } if self.config.pid_namespace { bwrap_cmd.arg("--unshare-pid"); } if self.config.uts_namespace { bwrap_cmd.arg("--unshare-uts"); } if self.config.ipc_namespace { bwrap_cmd.arg("--unshare-ipc"); } if self.config.mount_namespace { bwrap_cmd.arg("--unshare-net"); } if self.config.cgroup_namespace { bwrap_cmd.arg("--unshare-cgroup"); } // Add bind mounts for bind_mount in &environment.bind_mounts { if bind_mount.readonly { bwrap_cmd.args(&["--ro-bind", bind_mount.source.to_str().unwrap(), bind_mount.target.to_str().unwrap()]); } else { bwrap_cmd.args(&["--bind", bind_mount.source.to_str().unwrap(), bind_mount.target.to_str().unwrap()]); } } // Add readonly paths for path in &environment.readonly_paths { bwrap_cmd.args(&["--ro-bind", path.to_str().unwrap(), path.to_str().unwrap()]); } // Add writable paths for path in &environment.writable_paths { bwrap_cmd.args(&["--bind", path.to_str().unwrap(), path.to_str().unwrap()]); } // Add capabilities for capability in &environment.capabilities { bwrap_cmd.args(&["--cap-add", capability]); } // Set working directory bwrap_cmd.args(&["--chdir", environment.working_directory.to_str().unwrap()]); // Add environment variables for (key, value) in &environment.environment_variables { bwrap_cmd.args(&["--setenv", key, value]); } // Add the actual command bwrap_cmd.args(command); // Execute command let output = bwrap_cmd .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .map_err(|e| AptOstreeError::ScriptExecution(format!("Failed to execute sandboxed command: {}", e)))?; let execution_time = start_time.elapsed(); let result = SandboxResult { success: output.status.success(), exit_code: output.status.code().unwrap_or(-1), stdout: String::from_utf8_lossy(&output.stdout).to_string(), stderr: String::from_utf8_lossy(&output.stderr).to_string(), execution_time, sandbox_id, }; if result.success { info!("Sandboxed command executed successfully in {:?}", execution_time); } else { error!("Sandboxed command failed with exit code {}: {}", result.exit_code, result.stderr); } Ok(result) } /// Execute command without sandboxing (fallback) async fn execute_without_sandbox( &self, command: &[String], environment: &SandboxEnvironment, ) -> AptOstreeResult { let start_time = std::time::Instant::now(); let sandbox_id = format!("nosandbox_{}", chrono::Utc::now().timestamp()); warn!("Executing command without sandboxing: {:?}", command); let mut cmd = Command::new(&command[0]); cmd.args(&command[1..]); // Set working directory cmd.current_dir(&environment.working_directory); // Set environment variables for (key, value) in &environment.environment_variables { cmd.env(key, value); } let output = cmd .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .map_err(|e| AptOstreeError::ScriptExecution(format!("Failed to execute command: {}", e)))?; let execution_time = start_time.elapsed(); Ok(SandboxResult { success: output.status.success(), exit_code: output.status.code().unwrap_or(-1), stdout: String::from_utf8_lossy(&output.stdout).to_string(), stderr: String::from_utf8_lossy(&output.stderr).to_string(), execution_time, sandbox_id, }) } /// Create sandbox environment for DEB script execution pub fn create_deb_script_environment( &self, script_path: &Path, package_name: &str, script_type: &str, ) -> SandboxEnvironment { let mut env_vars = HashMap::new(); // Basic environment env_vars.insert("PATH".to_string(), "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string()); env_vars.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); env_vars.insert("DPKG_MAINTSCRIPT_NAME".to_string(), script_type.to_string()); env_vars.insert("DPKG_MAINTSCRIPT_PACKAGE".to_string(), package_name.to_string()); env_vars.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env_vars.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); // Script-specific environment match script_type { "preinst" => { env_vars.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env_vars.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } "postinst" => { env_vars.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env_vars.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } "prerm" => { env_vars.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env_vars.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } "postrm" => { env_vars.insert("DPKG_MAINTSCRIPT_ARCH".to_string(), "amd64".to_string()); env_vars.insert("DPKG_MAINTSCRIPT_VERSION".to_string(), "1.0".to_string()); } _ => {} } let working_directory = script_path.parent().unwrap_or_else(|| Path::new("/tmp")).to_path_buf(); SandboxEnvironment { working_directory, environment_variables: env_vars, bind_mounts: self.config.bind_mounts.clone(), readonly_paths: self.config.readonly_paths.clone(), writable_paths: self.config.writable_paths.clone(), network_access: self.config.network_access, capabilities: self.config.capabilities.clone(), } } /// Check if bubblewrap is available and working pub fn check_bubblewrap_availability(&self) -> AptOstreeResult { let output = Command::new(&self.bubblewrap_path) .arg("--version") .output(); match output { Ok(output) => { if output.status.success() { let version = String::from_utf8_lossy(&output.stdout); info!("Bubblewrap version: {}", version.trim()); Ok(true) } else { warn!("Bubblewrap version check failed"); Ok(false) } } Err(e) => { warn!("Bubblewrap not available: {}", e); Ok(false) } } } /// Get sandbox configuration pub fn get_config(&self) -> &BubblewrapConfig { &self.config } /// Update sandbox configuration pub fn update_config(&mut self, config: BubblewrapConfig) { self.config = config; info!("Updated bubblewrap sandbox configuration"); } } /// Sandbox manager for script execution pub struct ScriptSandboxManager { bubblewrap_sandbox: BubblewrapSandbox, } impl ScriptSandboxManager { /// Create a new script sandbox manager pub fn new(config: BubblewrapConfig) -> AptOstreeResult { let bubblewrap_sandbox = BubblewrapSandbox::new(config)?; Ok(Self { bubblewrap_sandbox }) } /// Execute DEB script in sandboxed environment pub async fn execute_deb_script( &self, script_path: &Path, package_name: &str, script_type: &str, ) -> AptOstreeResult { info!("Executing DEB script in sandbox: {} ({}) for package {}", script_path.display(), script_type, package_name); // Create sandbox environment let environment = self.bubblewrap_sandbox.create_deb_script_environment( script_path, package_name, script_type ); // Execute script let command = vec![script_path.to_str().unwrap().to_string()]; self.bubblewrap_sandbox.execute_sandboxed(&command, &environment).await } /// Execute arbitrary command in sandboxed environment pub async fn execute_command( &self, command: &[String], working_directory: &Path, environment_vars: &HashMap, ) -> AptOstreeResult { info!("Executing command in sandbox: {:?}", command); let environment = SandboxEnvironment { working_directory: working_directory.to_path_buf(), environment_variables: environment_vars.clone(), bind_mounts: self.bubblewrap_sandbox.get_config().bind_mounts.clone(), readonly_paths: self.bubblewrap_sandbox.get_config().readonly_paths.clone(), writable_paths: self.bubblewrap_sandbox.get_config().writable_paths.clone(), network_access: self.bubblewrap_sandbox.get_config().network_access, capabilities: self.bubblewrap_sandbox.get_config().capabilities.clone(), }; self.bubblewrap_sandbox.execute_sandboxed(command, &environment).await } /// Check sandbox availability pub fn is_sandbox_available(&self) -> bool { self.bubblewrap_sandbox.check_bubblewrap_availability().unwrap_or(false) } /// Get bubblewrap sandbox reference pub fn get_bubblewrap_sandbox(&self) -> &BubblewrapSandbox { &self.bubblewrap_sandbox } }