sandbox_rs/
controller.rs

1//! Main sandbox controller
2
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6use std::time::{Duration, Instant};
7
8use log::warn;
9use nix::sys::signal::{Signal, kill};
10use nix::unistd::Pid;
11
12use crate::errors::{Result, SandboxError};
13use crate::execution::process::{ProcessConfig, ProcessExecutor};
14use crate::isolation::namespace::NamespaceConfig;
15use crate::isolation::seccomp::SeccompProfile;
16use crate::resources::cgroup::{Cgroup, CgroupConfig};
17use crate::utils;
18
19/// Sandbox configuration
20#[derive(Debug, Clone)]
21pub struct SandboxConfig {
22    /// Root directory for sandbox
23    pub root: PathBuf,
24    /// Memory limit in bytes
25    pub memory_limit: Option<u64>,
26    /// CPU quota (microseconds)
27    pub cpu_quota: Option<u64>,
28    /// CPU period (microseconds)
29    pub cpu_period: Option<u64>,
30    /// Maximum PIDs
31    pub max_pids: Option<u32>,
32    /// Seccomp profile
33    pub seccomp_profile: SeccompProfile,
34    /// Namespace configuration
35    pub namespace_config: NamespaceConfig,
36    /// Timeout
37    pub timeout: Option<Duration>,
38    /// Unique sandbox ID
39    pub id: String,
40}
41
42impl Default for SandboxConfig {
43    fn default() -> Self {
44        Self {
45            root: PathBuf::from("/var/lib/sandbox"),
46            memory_limit: None,
47            cpu_quota: None,
48            cpu_period: None,
49            max_pids: None,
50            seccomp_profile: SeccompProfile::Minimal,
51            namespace_config: NamespaceConfig::default(),
52            timeout: None,
53            id: "default".to_string(),
54        }
55    }
56}
57
58impl SandboxConfig {
59    /// Validate configuration
60    pub fn validate(&self) -> Result<()> {
61        utils::require_root()?;
62
63        self.validate_invariants()
64    }
65
66    fn validate_invariants(&self) -> Result<()> {
67        if self.id.is_empty() {
68            return Err(SandboxError::InvalidConfig(
69                "Sandbox ID cannot be empty".to_string(),
70            ));
71        }
72
73        if self.namespace_config.enabled_count() == 0 {
74            return Err(SandboxError::InvalidConfig(
75                "At least one namespace must be enabled".to_string(),
76            ));
77        }
78
79        Ok(())
80    }
81}
82
83/// Builder pattern for sandbox creation
84pub struct SandboxBuilder {
85    config: SandboxConfig,
86}
87
88impl SandboxBuilder {
89    /// Create new builder
90    pub fn new(id: &str) -> Self {
91        Self {
92            config: SandboxConfig {
93                id: id.to_string(),
94                ..Default::default()
95            },
96        }
97    }
98
99    /// Set memory limit
100    pub fn memory_limit(mut self, bytes: u64) -> Self {
101        self.config.memory_limit = Some(bytes);
102        self
103    }
104
105    /// Set memory limit from string (e.g., "100M")
106    pub fn memory_limit_str(self, s: &str) -> Result<Self> {
107        let bytes = utils::parse_memory_size(s)?;
108        Ok(self.memory_limit(bytes))
109    }
110
111    /// Set CPU quota
112    pub fn cpu_quota(mut self, quota: u64, period: u64) -> Self {
113        self.config.cpu_quota = Some(quota);
114        self.config.cpu_period = Some(period);
115        self
116    }
117
118    /// Set CPU limit by percentage (0-100)
119    pub fn cpu_limit_percent(self, percent: u32) -> Self {
120        if percent == 0 || percent > 100 {
121            return self;
122        }
123        let quota = (percent as u64) * 1000; // percent * period/100 with period=100000
124        let period = 100000;
125        self.cpu_quota(quota, period)
126    }
127
128    /// Set maximum PIDs
129    pub fn max_pids(mut self, max: u32) -> Self {
130        self.config.max_pids = Some(max);
131        self
132    }
133
134    /// Set seccomp profile
135    pub fn seccomp_profile(mut self, profile: SeccompProfile) -> Self {
136        self.config.seccomp_profile = profile;
137        self
138    }
139
140    /// Set root directory
141    pub fn root(mut self, path: impl AsRef<Path>) -> Self {
142        self.config.root = path.as_ref().to_path_buf();
143        self
144    }
145
146    /// Set timeout
147    pub fn timeout(mut self, duration: Duration) -> Self {
148        self.config.timeout = Some(duration);
149        self
150    }
151
152    /// Set namespace configuration
153    pub fn namespaces(mut self, config: NamespaceConfig) -> Self {
154        self.config.namespace_config = config;
155        self
156    }
157
158    /// Build sandbox
159    pub fn build(self) -> Result<Sandbox> {
160        self.config.validate()?;
161        Sandbox::new(self.config)
162    }
163}
164
165/// Sandbox execution result
166#[derive(Debug, Clone)]
167pub struct SandboxResult {
168    /// Exit code
169    pub exit_code: i32,
170    /// Signal that killed process (if any)
171    pub signal: Option<i32>,
172    /// Whether timeout occurred
173    pub timed_out: bool,
174    /// Memory usage in bytes
175    pub memory_peak: u64,
176    /// CPU time in microseconds
177    pub cpu_time_us: u64,
178    /// Wall clock time in seconds
179    pub wall_time_ms: u64,
180}
181
182/// Active sandbox
183pub struct Sandbox {
184    config: SandboxConfig,
185    pid: Option<Pid>,
186    cgroup: Option<Cgroup>,
187    start_time: Option<Instant>,
188}
189
190impl Sandbox {
191    /// Create new sandbox
192    fn new(config: SandboxConfig) -> Result<Self> {
193        // Create root directory
194        fs::create_dir_all(&config.root).map_err(|e| {
195            SandboxError::Io(std::io::Error::other(format!(
196                "Failed to create root directory: {}",
197                e
198            )))
199        })?;
200
201        Ok(Self {
202            config,
203            pid: None,
204            cgroup: None,
205            start_time: None,
206        })
207    }
208
209    /// Get sandbox ID
210    pub fn id(&self) -> &str {
211        &self.config.id
212    }
213
214    /// Get sandbox root
215    pub fn root(&self) -> &Path {
216        &self.config.root
217    }
218
219    /// Check if sandbox is running
220    pub fn is_running(&self) -> bool {
221        self.pid.is_some()
222    }
223
224    /// Run program in sandbox
225    pub fn run(&mut self, program: &str, args: &[&str]) -> Result<SandboxResult> {
226        if self.is_running() {
227            return Err(SandboxError::AlreadyRunning);
228        }
229
230        self.start_time = Some(Instant::now());
231
232        if utils::is_root() {
233            let cgroup_name = format!("sandbox-{}", self.config.id);
234            let cgroup = Cgroup::new(&cgroup_name, Pid::from_raw(std::process::id() as i32))?;
235
236            let cgroup_config = CgroupConfig {
237                memory_limit: self.config.memory_limit,
238                cpu_quota: self.config.cpu_quota,
239                cpu_period: self.config.cpu_period,
240                max_pids: self.config.max_pids,
241                cpu_weight: None,
242            };
243            cgroup.apply_config(&cgroup_config)?;
244
245            self.cgroup = Some(cgroup);
246        } else {
247            warn!(
248                "Skipping cgroup configuration for sandbox {} (not running as root)",
249                self.config.id
250            );
251        }
252
253        // Create process configuration with namespace and seccomp settings
254        let process_config = ProcessConfig {
255            program: program.to_string(),
256            args: args.iter().map(|s| s.to_string()).collect(),
257            env: Vec::new(), // Inherit parent environment
258            cwd: None,
259            chroot_dir: None,
260            uid: None,
261            gid: None,
262            seccomp: Some(crate::isolation::seccomp::SeccompFilter::from_profile(
263                self.config.seccomp_profile.clone(),
264            )),
265        };
266
267        // Execute with namespace isolation
268        if utils::is_root() {
269            // Real isolation with namespaces
270            let process_result =
271                ProcessExecutor::execute(process_config, self.config.namespace_config.clone())?;
272
273            self.pid = Some(process_result.pid);
274
275            let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
276
277            // Get peak memory from cgroup if available
278            let (memory_peak, _) = self.get_resource_usage().unwrap_or((0, 0));
279
280            Ok(SandboxResult {
281                exit_code: process_result.exit_status,
282                signal: process_result.signal,
283                timed_out: false,
284                memory_peak,
285                cpu_time_us: process_result.exec_time_ms * 1000, // Convert ms to us
286                wall_time_ms,
287            })
288        } else {
289            // Fallback: run without full namespace isolation (for testing)
290            warn!("Running without full isolation (not root). Use sudo for production sandboxes.");
291            let output = Command::new(program)
292                .args(args)
293                .output()
294                .map_err(SandboxError::Io)?;
295
296            let exit_code = output.status.code().unwrap_or(-1);
297            let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
298
299            Ok(SandboxResult {
300                exit_code,
301                signal: None,
302                timed_out: false,
303                memory_peak: 0,
304                cpu_time_us: 0,
305                wall_time_ms,
306            })
307        }
308    }
309
310    /// Kill sandbox
311    pub fn kill(&mut self) -> Result<()> {
312        if let Some(pid) = self.pid {
313            kill(pid, Signal::SIGKILL)
314                .map_err(|e| SandboxError::Syscall(format!("Failed to kill process: {}", e)))?;
315            self.pid = None;
316        }
317        Ok(())
318    }
319
320    /// Get resource usage
321    pub fn get_resource_usage(&self) -> Result<(u64, u64)> {
322        if let Some(ref cgroup) = self.cgroup {
323            let memory = cgroup.get_memory_usage()?;
324            let cpu = cgroup.get_cpu_usage()?;
325            Ok((memory, cpu))
326        } else {
327            Ok((0, 0))
328        }
329    }
330}
331
332impl Drop for Sandbox {
333    fn drop(&mut self) {
334        // Clean up on drop
335        let _ = self.kill();
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::resources::cgroup::Cgroup;
343    use crate::test_support::serial_guard;
344    use crate::utils;
345    use std::env;
346    use std::time::Duration;
347    use tempfile::tempdir;
348
349    fn config_with_temp_root(id: &str) -> (tempfile::TempDir, SandboxConfig) {
350        let tmp = tempdir().unwrap();
351        let config = SandboxConfig {
352            id: id.to_string(),
353            root: tmp.path().join("root"),
354            namespace_config: NamespaceConfig::minimal(),
355            ..Default::default()
356        };
357        (tmp, config)
358    }
359
360    struct RootOverrideGuard;
361
362    impl RootOverrideGuard {
363        fn enable() -> Self {
364            utils::set_root_override(Some(true));
365            Self
366        }
367    }
368
369    impl Drop for RootOverrideGuard {
370        fn drop(&mut self) {
371            utils::set_root_override(None);
372        }
373    }
374
375    struct EnvVarGuard {
376        key: &'static str,
377        prev: Option<String>,
378    }
379
380    impl EnvVarGuard {
381        fn new(key: &'static str, value: &str) -> Self {
382            let prev = env::var(key).ok();
383            unsafe {
384                env::set_var(key, value);
385            }
386            Self { key, prev }
387        }
388    }
389
390    impl Drop for EnvVarGuard {
391        fn drop(&mut self) {
392            if let Some(ref value) = self.prev {
393                unsafe {
394                    env::set_var(self.key, value);
395                }
396            } else {
397                unsafe {
398                    env::remove_var(self.key);
399                }
400            }
401        }
402    }
403
404    #[test]
405    fn test_sandbox_config_default() {
406        let config = SandboxConfig::default();
407        assert_eq!(config.id, "default");
408        assert!(config.memory_limit.is_none());
409    }
410
411    #[test]
412    fn test_sandbox_config_validate() {
413        let config = SandboxConfig {
414            id: String::new(),
415            ..Default::default()
416        };
417
418        // Validation might fail due to root requirement, but we can test ID validation
419        // by checking the error message
420        if let Err(e) = config.validate() {
421            // Expected to fail, either due to root or empty ID
422            assert!(e.to_string().contains("ID") || e.to_string().contains("root"));
423        }
424    }
425
426    #[test]
427    fn test_sandbox_builder_new() {
428        let builder = SandboxBuilder::new("test");
429        assert_eq!(builder.config.id, "test");
430    }
431
432    #[test]
433    fn test_sandbox_builder_memory_limit() {
434        let builder = SandboxBuilder::new("test").memory_limit(100 * 1024 * 1024);
435        assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
436    }
437
438    #[test]
439    fn test_sandbox_builder_memory_limit_str() -> Result<()> {
440        let builder = SandboxBuilder::new("test").memory_limit_str("100M")?;
441        assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
442        Ok(())
443    }
444
445    #[test]
446    fn test_sandbox_builder_cpu_limit() {
447        let builder = SandboxBuilder::new("test").cpu_limit_percent(50);
448        assert!(builder.config.cpu_quota.is_some());
449    }
450
451    #[test]
452    fn test_sandbox_builder_cpu_quota() {
453        let builder = SandboxBuilder::new("test").cpu_quota(50000, 100000);
454        assert_eq!(builder.config.cpu_quota, Some(50000));
455        assert_eq!(builder.config.cpu_period, Some(100000));
456    }
457
458    #[test]
459    fn test_sandbox_result() {
460        let result = SandboxResult {
461            exit_code: 0,
462            signal: None,
463            timed_out: false,
464            memory_peak: 1024,
465            cpu_time_us: 5000,
466            wall_time_ms: 100,
467        };
468        assert_eq!(result.exit_code, 0);
469        assert!(!result.timed_out);
470    }
471
472    #[test]
473    fn sandbox_config_invariants_detect_empty_id() {
474        let config = SandboxConfig {
475            id: String::new(),
476            ..Default::default()
477        };
478        assert!(config.validate_invariants().is_err());
479    }
480
481    #[test]
482    fn sandbox_config_invariants_detect_disabled_namespaces() {
483        let config = SandboxConfig {
484            namespace_config: NamespaceConfig {
485                pid: false,
486                ipc: false,
487                net: false,
488                mount: false,
489                uts: false,
490                user: false,
491            },
492            ..Default::default()
493        };
494        assert!(config.validate_invariants().is_err());
495    }
496
497    #[test]
498    fn sandbox_provides_id_and_root() {
499        let (_tmp, config) = config_with_temp_root("sand-id");
500        let sandbox = Sandbox::new(config.clone()).unwrap();
501        assert_eq!(sandbox.id(), "sand-id");
502        assert!(sandbox.root().ends_with("root"));
503        assert!(!sandbox.is_running());
504    }
505
506    #[test]
507    fn sandbox_run_executes_command_without_root() {
508        let _guard = serial_guard();
509        let (_tmp, config) = config_with_temp_root("run-test");
510        let mut sandbox = Sandbox::new(config).unwrap();
511        let args: [&str; 1] = ["hello"];
512        let result = sandbox.run("/bin/echo", &args).unwrap();
513        assert_eq!(result.exit_code, 0);
514        assert!(!sandbox.is_running());
515    }
516
517    #[test]
518    fn sandbox_reports_resource_usage_from_cgroup() {
519        let (tmp, mut config) = config_with_temp_root("resource-test");
520        config.root = tmp.path().join("root");
521        let mut sandbox = Sandbox::new(config).unwrap();
522
523        let cg_path = tmp.path().join("cgroup");
524        std::fs::create_dir_all(&cg_path).unwrap();
525        std::fs::write(cg_path.join("memory.current"), "1234").unwrap();
526        std::fs::write(cg_path.join("cpu.stat"), "usage_usec 77\n").unwrap();
527
528        sandbox.cgroup = Some(Cgroup::for_testing(cg_path.clone()));
529        let (mem, cpu) = sandbox.get_resource_usage().unwrap();
530        assert_eq!(mem, 1234);
531        assert_eq!(cpu, 77);
532    }
533
534    #[test]
535    #[ignore]
536    fn sandbox_builder_builds_when_root_override() {
537        let _guard = serial_guard();
538        let _root_guard = RootOverrideGuard::enable();
539        let tmp = tempdir().unwrap();
540        let _env_guard = EnvVarGuard::new("SANDBOX_CGROUP_ROOT", tmp.path().to_str().unwrap());
541
542        let mut sandbox = SandboxBuilder::new("integration")
543            .memory_limit(1024)
544            .cpu_limit_percent(10)
545            .max_pids(4)
546            .seccomp_profile(SeccompProfile::Minimal)
547            .root(tmp.path())
548            .timeout(Duration::from_secs(1))
549            .namespaces(NamespaceConfig::minimal())
550            .build()
551            .unwrap();
552
553        let args: [&str; 0] = [];
554        let result = sandbox.run("/bin/true", &args).unwrap();
555        assert_eq!(result.exit_code, 0);
556    }
557
558    #[test]
559    fn sandbox_kill_handles_missing_pid() {
560        let (_tmp, config) = config_with_temp_root("kill-test");
561        let mut sandbox = Sandbox::new(config).unwrap();
562        sandbox.kill().unwrap();
563    }
564
565    #[test]
566    fn sandbox_kill_terminates_real_process() {
567        let (_tmp, config) = config_with_temp_root("kill-proc");
568        let mut sandbox = Sandbox::new(config).unwrap();
569        let mut child = std::process::Command::new("sleep")
570            .arg("1")
571            .spawn()
572            .unwrap();
573        sandbox.pid = Some(Pid::from_raw(child.id() as i32));
574        sandbox.kill().unwrap();
575        let _ = child.wait();
576    }
577
578    #[test]
579    fn sandbox_get_resource_usage_without_cgroup() {
580        let (_tmp, config) = config_with_temp_root("no-cgroup");
581        let sandbox = Sandbox::new(config).unwrap();
582        let (mem, cpu) = sandbox.get_resource_usage().unwrap();
583        assert_eq!(mem, 0);
584        assert_eq!(cpu, 0);
585    }
586}