/ ferris-proof-plugins / src / sandbox.rs
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(&current_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  }