sandbox.rs
1 use anyhow::{anyhow, Result}; 2 use std::collections::HashMap; 3 use std::path::PathBuf; 4 use std::process::{Child, Command, Stdio}; 5 use std::sync::{Arc, Mutex}; 6 use std::time::{Duration, Instant}; 7 use tokio::time::timeout; 8 use tracing::{debug, error, info, warn}; 9 10 /// Sandboxed executor for running external verification tools safely 11 /// 12 /// This executor provides: 13 /// - Resource limits (memory, CPU time, file descriptors) 14 /// - Network access policy enforcement 15 /// - Timeout handling with graceful termination 16 /// - File system access restrictions 17 #[derive(Debug, Clone)] 18 pub struct SandboxedExecutor { 19 allowed_paths: Vec<PathBuf>, 20 network_policy: NetworkPolicy, 21 limits: ResourceLimits, 22 timeout_duration: Duration, 23 } 24 25 #[derive(Debug, Clone)] 26 pub enum NetworkPolicy { 27 /// No network access allowed 28 Denied, 29 30 /// Allow connections to specified hosts only 31 AllowList(Vec<String>), 32 33 /// Allow all connections (user explicitly opted in) 34 Unrestricted { user_consent: bool }, 35 } 36 37 #[derive(Debug, Clone)] 38 pub struct ResourceLimits { 39 /// Maximum memory usage in bytes 40 pub max_memory: u64, 41 42 /// Maximum CPU time in seconds 43 pub max_cpu_time: u64, 44 45 /// Maximum number of open file descriptors 46 pub max_file_descriptors: u32, 47 48 /// Maximum number of child processes 49 pub max_processes: u32, 50 51 /// Maximum file size that can be created (bytes) 52 pub max_file_size: u64, 53 } 54 55 /// Configuration for network access consent 56 #[derive(Debug, Clone)] 57 pub struct NetworkConsent { 58 pub granted: bool, 59 pub timestamp: std::time::SystemTime, 60 pub scope: ConsentScope, 61 } 62 63 #[derive(Debug, Clone)] 64 pub enum ConsentScope { 65 /// Consent for specific hosts 66 Hosts(Vec<String>), 67 68 /// Consent for all network access 69 Unrestricted, 70 71 /// Consent for specific verification session 72 Session(String), 73 } 74 75 impl SandboxedExecutor { 76 /// Create a new sandboxed executor with default settings 77 pub fn new() -> Self { 78 Self { 79 allowed_paths: Vec::new(), 80 network_policy: NetworkPolicy::Denied, 81 limits: ResourceLimits::default(), 82 timeout_duration: Duration::from_secs(300), // 5 minutes default 83 } 84 } 85 86 /// Configure allowed file system paths 87 pub fn with_allowed_paths(mut self, paths: Vec<PathBuf>) -> Self { 88 self.allowed_paths = paths; 89 self 90 } 91 92 /// Configure network access policy 93 pub fn with_network_policy(mut self, policy: NetworkPolicy) -> Self { 94 self.network_policy = policy; 95 self 96 } 97 98 /// Configure resource limits 99 pub fn with_limits(mut self, limits: ResourceLimits) -> Self { 100 self.limits = limits; 101 self 102 } 103 104 /// Configure execution timeout 105 pub fn with_timeout(mut self, timeout: Duration) -> Self { 106 self.timeout_duration = timeout; 107 self 108 } 109 110 /// Execute a command in the sandbox with comprehensive safety measures 111 pub async fn execute( 112 &self, 113 command: &str, 114 args: &[&str], 115 env: HashMap<String, String>, 116 working_dir: Option<&PathBuf>, 117 ) -> Result<SandboxedOutput> { 118 info!("Executing sandboxed command: {} {:?}", command, args); 119 120 // Validate command and arguments 121 self.validate_command(command, args)?; 122 123 // Validate working directory 124 if let Some(dir) = working_dir { 125 self.validate_path_access(dir)?; 126 } 127 128 // Prepare command with security restrictions 129 let cmd = self.prepare_command(command, args, env, working_dir)?; 130 131 // Execute with timeout and resource monitoring 132 let execution_result = self.execute_with_timeout(cmd).await?; 133 134 // Validate execution results 135 self.validate_execution_result(&execution_result)?; 136 137 Ok(execution_result) 138 } 139 140 /// Validate that the command is safe to execute 141 fn validate_command(&self, command: &str, args: &[&str]) -> Result<()> { 142 // Check for dangerous commands 143 let dangerous_commands = [ 144 "rm", "rmdir", "del", "format", "fdisk", "dd", "mkfs", "mount", "umount", "sudo", "su", 145 "chmod", "chown", "curl", "wget", "nc", "netcat", "telnet", 146 ]; 147 148 if dangerous_commands.contains(&command) { 149 return Err(anyhow!("Command '{}' is not allowed in sandbox", command)); 150 } 151 152 // Check for suspicious arguments 153 for arg in args { 154 if arg.contains("..") || arg.starts_with('/') { 155 warn!("Suspicious argument detected: {}", arg); 156 } 157 158 // Check for network-related arguments 159 if self.network_policy == NetworkPolicy::Denied 160 && (arg.contains("http://") || arg.contains("https://") || arg.contains("ftp://")) 161 { 162 return Err(anyhow!("Network access denied: argument contains URL")); 163 } 164 } 165 166 Ok(()) 167 } 168 169 /// Validate that a path is within allowed access 170 fn validate_path_access(&self, path: &PathBuf) -> Result<()> { 171 if self.allowed_paths.is_empty() { 172 // If no restrictions specified, allow current directory and subdirectories 173 let current_dir = std::env::current_dir()?; 174 let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone()); 175 176 if !canonical_path.starts_with(¤t_dir) { 177 return Err(anyhow!( 178 "Path access denied: {:?} is outside current directory", 179 path 180 )); 181 } 182 } else { 183 // Check against allowed paths 184 let canonical_path = path.canonicalize().unwrap_or_else(|_| path.clone()); 185 let allowed = self 186 .allowed_paths 187 .iter() 188 .any(|allowed_path| canonical_path.starts_with(allowed_path)); 189 190 if !allowed { 191 return Err(anyhow!( 192 "Path access denied: {:?} is not in allowed paths", 193 path 194 )); 195 } 196 } 197 198 Ok(()) 199 } 200 201 /// Prepare command with security restrictions 202 fn prepare_command( 203 &self, 204 command: &str, 205 args: &[&str], 206 mut env: HashMap<String, String>, 207 working_dir: Option<&PathBuf>, 208 ) -> Result<Command> { 209 let mut cmd = Command::new(command); 210 cmd.args(args) 211 .stdin(Stdio::null()) 212 .stdout(Stdio::piped()) 213 .stderr(Stdio::piped()); 214 215 // Set working directory 216 if let Some(dir) = working_dir { 217 cmd.current_dir(dir); 218 } 219 220 // Configure environment variables for security 221 self.configure_environment(&mut env)?; 222 cmd.envs(&env); 223 224 // Apply resource limits 225 self.apply_resource_limits(&mut cmd)?; 226 227 // Apply network restrictions 228 self.apply_network_restrictions(&mut cmd)?; 229 230 Ok(cmd) 231 } 232 233 /// Configure environment variables for security 234 fn configure_environment(&self, env: &mut HashMap<String, String>) -> Result<()> { 235 // Remove potentially dangerous environment variables 236 env.remove("LD_PRELOAD"); 237 env.remove("DYLD_INSERT_LIBRARIES"); 238 env.remove("PATH"); // Will be set to restricted PATH 239 240 // Set restricted PATH 241 env.insert("PATH".to_string(), "/usr/bin:/bin".to_string()); 242 243 // Disable network access if policy requires it 244 match &self.network_policy { 245 NetworkPolicy::Denied => { 246 env.insert("NO_PROXY".to_string(), "*".to_string()); 247 env.insert("no_proxy".to_string(), "*".to_string()); 248 env.remove("HTTP_PROXY"); 249 env.remove("HTTPS_PROXY"); 250 env.remove("http_proxy"); 251 env.remove("https_proxy"); 252 } 253 NetworkPolicy::AllowList(hosts) => { 254 // Configure proxy settings to only allow specific hosts 255 let allowed_hosts = hosts.join(","); 256 env.insert("ALLOWED_HOSTS".to_string(), allowed_hosts); 257 } 258 NetworkPolicy::Unrestricted { user_consent } => { 259 if !user_consent { 260 return Err(anyhow!("Network access requires explicit user consent")); 261 } 262 } 263 } 264 265 Ok(()) 266 } 267 268 /// Apply resource limits to the command (platform-specific) 269 fn apply_resource_limits(&self, cmd: &mut Command) -> Result<()> { 270 debug!("Applying resource limits: {:?}", self.limits); 271 272 #[cfg(unix)] 273 { 274 use std::os::unix::process::CommandExt; 275 276 let limits = self.limits.clone(); 277 unsafe { 278 cmd.pre_exec(move || { 279 // Set memory limit (RLIMIT_AS - virtual memory) 280 let memory_limit = libc::rlimit { 281 rlim_cur: limits.max_memory, 282 rlim_max: limits.max_memory, 283 }; 284 if libc::setrlimit(libc::RLIMIT_AS, &memory_limit) != 0 { 285 eprintln!("Warning: Failed to set memory limit"); 286 } 287 288 // Set CPU time limit (RLIMIT_CPU) 289 let cpu_limit = libc::rlimit { 290 rlim_cur: limits.max_cpu_time, 291 rlim_max: limits.max_cpu_time, 292 }; 293 if libc::setrlimit(libc::RLIMIT_CPU, &cpu_limit) != 0 { 294 eprintln!("Warning: Failed to set CPU time limit"); 295 } 296 297 // Set file descriptor limit (RLIMIT_NOFILE) 298 let fd_limit = libc::rlimit { 299 rlim_cur: limits.max_file_descriptors as u64, 300 rlim_max: limits.max_file_descriptors as u64, 301 }; 302 if libc::setrlimit(libc::RLIMIT_NOFILE, &fd_limit) != 0 { 303 eprintln!("Warning: Failed to set file descriptor limit"); 304 } 305 306 // Set process limit (RLIMIT_NPROC) 307 let proc_limit = libc::rlimit { 308 rlim_cur: limits.max_processes as u64, 309 rlim_max: limits.max_processes as u64, 310 }; 311 if libc::setrlimit(libc::RLIMIT_NPROC, &proc_limit) != 0 { 312 eprintln!("Warning: Failed to set process limit"); 313 } 314 315 Ok(()) 316 }); 317 } 318 } 319 320 #[cfg(windows)] 321 { 322 // Windows resource limits would be implemented using Job Objects 323 warn!("Resource limits not yet implemented on Windows"); 324 } 325 326 Ok(()) 327 } 328 329 /// Apply network access restrictions 330 fn apply_network_restrictions(&self, _cmd: &mut Command) -> Result<()> { 331 match &self.network_policy { 332 NetworkPolicy::Denied => { 333 debug!("Network access denied for sandboxed execution"); 334 // Network restrictions are primarily handled through environment variables 335 // and firewall rules (platform-specific implementation) 336 } 337 NetworkPolicy::AllowList(hosts) => { 338 debug!("Network access restricted to hosts: {:?}", hosts); 339 // Implementation would involve configuring network namespace or firewall rules 340 } 341 NetworkPolicy::Unrestricted { user_consent } => { 342 if *user_consent { 343 debug!("Unrestricted network access granted with user consent"); 344 } else { 345 return Err(anyhow!("Network access requires user consent")); 346 } 347 } 348 } 349 350 Ok(()) 351 } 352 353 /// Execute command with timeout and monitoring 354 async fn execute_with_timeout(&self, mut cmd: Command) -> Result<SandboxedOutput> { 355 let start_time = Instant::now(); 356 357 // Spawn the process 358 let child = cmd 359 .spawn() 360 .map_err(|e| anyhow!("Failed to spawn process: {}", e))?; 361 362 // Create a shared handle for the child process 363 let child_handle = Arc::new(Mutex::new(Some(child))); 364 let child_handle_clone = Arc::clone(&child_handle); 365 366 // Set up timeout handling 367 let timeout_result = timeout(self.timeout_duration, async { 368 // Wait for the process to complete in a blocking task 369 tokio::task::spawn_blocking(move || { 370 let mut child_guard = child_handle_clone.lock().unwrap(); 371 if let Some(child) = child_guard.take() { 372 child.wait_with_output() 373 } else { 374 Err(std::io::Error::other("Child process not available")) 375 } 376 }) 377 .await 378 .unwrap_or_else(|e| Err(std::io::Error::other(e.to_string()))) 379 }) 380 .await; 381 382 let execution_time = start_time.elapsed(); 383 384 match timeout_result { 385 Ok(Ok(output)) => { 386 info!("Command completed successfully in {:?}", execution_time); 387 388 Ok(SandboxedOutput { 389 stdout: String::from_utf8_lossy(&output.stdout).to_string(), 390 stderr: String::from_utf8_lossy(&output.stderr).to_string(), 391 exit_code: output.status.code().unwrap_or(-1), 392 execution_time, 393 resource_usage: self.collect_resource_usage(), 394 timeout_occurred: false, 395 }) 396 } 397 Ok(Err(e)) => { 398 error!("Process execution failed: {}", e); 399 Err(anyhow!("Process execution failed: {}", e)) 400 } 401 Err(_) => { 402 warn!( 403 "Command timed out after {:?}, attempting graceful termination", 404 self.timeout_duration 405 ); 406 407 // Attempt graceful termination 408 self.terminate_process_gracefully(&child_handle).await?; 409 410 Ok(SandboxedOutput { 411 stdout: String::new(), 412 stderr: format!( 413 "Process terminated due to timeout ({:?})", 414 self.timeout_duration 415 ), 416 exit_code: -1, 417 execution_time, 418 resource_usage: self.collect_resource_usage(), 419 timeout_occurred: true, 420 }) 421 } 422 } 423 } 424 425 /// Terminate process gracefully with escalating signals 426 async fn terminate_process_gracefully( 427 &self, 428 child_handle: &Arc<Mutex<Option<Child>>>, 429 ) -> Result<()> { 430 let pid = { 431 let mut child_guard = child_handle.lock().unwrap(); 432 if let Some(ref mut child) = child_guard.as_mut() { 433 #[cfg(unix)] 434 { 435 // Try SIGTERM first 436 let pid = child.id(); 437 unsafe { 438 libc::kill(pid as i32, libc::SIGTERM); 439 } 440 Some(pid) 441 } 442 #[cfg(windows)] 443 { 444 // On Windows, use TerminateProcess 445 if let Err(e) = child.kill() { 446 error!("Failed to terminate process: {}", e); 447 } 448 None 449 } 450 } else { 451 None 452 } 453 }; // Drop the lock before await 454 455 // Wait a bit for graceful shutdown 456 tokio::time::sleep(Duration::from_secs(2)).await; 457 458 #[cfg(unix)] 459 if let Some(pid) = pid { 460 let mut child_guard = child_handle.lock().unwrap(); 461 // Check if process is still running 462 if let Some(ref mut child) = child_guard.as_mut() { 463 match child.try_wait() { 464 Ok(Some(_)) => { 465 debug!("Process terminated gracefully"); 466 return Ok(()); 467 } 468 Ok(None) => { 469 // Still running, try SIGKILL 470 warn!("Process did not respond to SIGTERM, sending SIGKILL"); 471 unsafe { 472 libc::kill(pid as i32, libc::SIGKILL); 473 } 474 } 475 Err(e) => { 476 error!("Error checking process status: {}", e); 477 } 478 } 479 } 480 } 481 482 Ok(()) 483 } 484 485 /// Collect resource usage statistics 486 fn collect_resource_usage(&self) -> ResourceUsage { 487 // This would collect actual resource usage statistics 488 // For now, return default values 489 ResourceUsage { 490 peak_memory: 0, 491 cpu_time: Duration::from_secs(0), 492 file_descriptors_used: 0, 493 processes_spawned: 1, 494 } 495 } 496 497 /// Validate execution results for security compliance 498 fn validate_execution_result(&self, result: &SandboxedOutput) -> Result<()> { 499 // Check for suspicious output patterns 500 if result.stderr.contains("Permission denied") && result.exit_code != 0 { 501 debug!("Process encountered permission restrictions (expected)"); 502 } 503 504 // Check for network access attempts when denied 505 if matches!(self.network_policy, NetworkPolicy::Denied) 506 && (result.stderr.contains("Connection refused") 507 || result.stderr.contains("Network is unreachable")) 508 { 509 debug!("Network access properly blocked"); 510 } 511 512 // Validate resource usage 513 if result.execution_time > self.timeout_duration { 514 warn!("Execution time exceeded configured timeout"); 515 } 516 517 Ok(()) 518 } 519 } 520 521 #[derive(Debug, Clone)] 522 pub struct SandboxedOutput { 523 pub stdout: String, 524 pub stderr: String, 525 pub exit_code: i32, 526 pub execution_time: Duration, 527 pub resource_usage: ResourceUsage, 528 pub timeout_occurred: bool, 529 } 530 531 #[derive(Debug, Clone)] 532 pub struct ResourceUsage { 533 pub peak_memory: u64, 534 pub cpu_time: Duration, 535 pub file_descriptors_used: u32, 536 pub processes_spawned: u32, 537 } 538 539 impl Default for ResourceLimits { 540 fn default() -> Self { 541 Self { 542 max_memory: 2 * 1024 * 1024 * 1024, // 2GB 543 max_cpu_time: 300, // 5 minutes 544 max_file_descriptors: 1024, 545 max_processes: 10, 546 max_file_size: 100 * 1024 * 1024, // 100MB 547 } 548 } 549 } 550 551 impl PartialEq for NetworkPolicy { 552 fn eq(&self, other: &Self) -> bool { 553 match (self, other) { 554 (NetworkPolicy::Denied, NetworkPolicy::Denied) => true, 555 (NetworkPolicy::AllowList(a), NetworkPolicy::AllowList(b)) => a == b, 556 ( 557 NetworkPolicy::Unrestricted { user_consent: a }, 558 NetworkPolicy::Unrestricted { user_consent: b }, 559 ) => a == b, 560 _ => false, 561 } 562 } 563 } 564 565 impl Default for SandboxedExecutor { 566 fn default() -> Self { 567 Self::new() 568 } 569 }