sandbox_rs/
controller.rs

1//! Main sandbox controller
2
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::time::{Duration, Instant};
6
7use nix::sys::signal::{Signal, kill};
8use nix::unistd::Pid;
9
10use crate::errors::{Result, SandboxError};
11use crate::execution::ProcessStream;
12use crate::execution::process::{ProcessConfig, ProcessExecutor};
13use crate::isolation::namespace::NamespaceConfig;
14use crate::isolation::seccomp::SeccompProfile;
15use crate::resources::cgroup::{Cgroup, CgroupConfig};
16use crate::utils;
17
18/// Sandbox configuration
19#[derive(Debug, Clone)]
20pub struct SandboxConfig {
21    /// Root directory for sandbox
22    pub root: PathBuf,
23    /// Memory limit in bytes
24    pub memory_limit: Option<u64>,
25    /// CPU quota (microseconds)
26    pub cpu_quota: Option<u64>,
27    /// CPU period (microseconds)
28    pub cpu_period: Option<u64>,
29    /// Maximum PIDs
30    pub max_pids: Option<u32>,
31    /// Seccomp profile
32    pub seccomp_profile: SeccompProfile,
33    /// Namespace configuration
34    pub namespace_config: NamespaceConfig,
35    /// Timeout
36    pub timeout: Option<Duration>,
37    /// Unique sandbox ID
38    pub id: String,
39}
40
41impl Default for SandboxConfig {
42    fn default() -> Self {
43        Self {
44            root: PathBuf::from("/var/lib/sandbox"),
45            memory_limit: None,
46            cpu_quota: None,
47            cpu_period: None,
48            max_pids: None,
49            seccomp_profile: SeccompProfile::Minimal,
50            namespace_config: NamespaceConfig::default(),
51            timeout: None,
52            id: "default".to_string(),
53        }
54    }
55}
56
57impl SandboxConfig {
58    /// Validate configuration
59    pub fn validate(&self) -> Result<()> {
60        utils::require_root()?;
61
62        self.validate_invariants()
63    }
64
65    fn validate_invariants(&self) -> Result<()> {
66        if self.id.is_empty() {
67            return Err(SandboxError::InvalidConfig(
68                "Sandbox ID cannot be empty".to_string(),
69            ));
70        }
71
72        if self.namespace_config.enabled_count() == 0 {
73            return Err(SandboxError::InvalidConfig(
74                "At least one namespace must be enabled".to_string(),
75            ));
76        }
77
78        Ok(())
79    }
80}
81
82/// Builder pattern for sandbox creation
83pub struct SandboxBuilder {
84    config: SandboxConfig,
85}
86
87impl SandboxBuilder {
88    /// Create new builder
89    pub fn new(id: &str) -> Self {
90        Self {
91            config: SandboxConfig {
92                id: id.to_string(),
93                ..Default::default()
94            },
95        }
96    }
97
98    /// Set memory limit
99    pub fn memory_limit(mut self, bytes: u64) -> Self {
100        self.config.memory_limit = Some(bytes);
101        self
102    }
103
104    /// Set memory limit from string (e.g., "100M")
105    pub fn memory_limit_str(self, s: &str) -> Result<Self> {
106        let bytes = utils::parse_memory_size(s)?;
107        Ok(self.memory_limit(bytes))
108    }
109
110    /// Set CPU quota
111    pub fn cpu_quota(mut self, quota: u64, period: u64) -> Self {
112        self.config.cpu_quota = Some(quota);
113        self.config.cpu_period = Some(period);
114        self
115    }
116
117    /// Set CPU limit by percentage (0-100)
118    pub fn cpu_limit_percent(self, percent: u32) -> Self {
119        if percent == 0 || percent > 100 {
120            return self;
121        }
122        let quota = (percent as u64) * 1000; // percent * period/100 with period=100000
123        let period = 100000;
124        self.cpu_quota(quota, period)
125    }
126
127    /// Set maximum PIDs
128    pub fn max_pids(mut self, max: u32) -> Self {
129        self.config.max_pids = Some(max);
130        self
131    }
132
133    /// Set seccomp profile
134    pub fn seccomp_profile(mut self, profile: SeccompProfile) -> Self {
135        self.config.seccomp_profile = profile;
136        self
137    }
138
139    /// Set root directory
140    pub fn root(mut self, path: impl AsRef<Path>) -> Self {
141        self.config.root = path.as_ref().to_path_buf();
142        self
143    }
144
145    /// Set timeout
146    pub fn timeout(mut self, duration: Duration) -> Self {
147        self.config.timeout = Some(duration);
148        self
149    }
150
151    /// Set namespace configuration
152    pub fn namespaces(mut self, config: NamespaceConfig) -> Self {
153        self.config.namespace_config = config;
154        self
155    }
156
157    /// Build sandbox
158    pub fn build(self) -> Result<Sandbox> {
159        self.config.validate()?;
160        Sandbox::new(self.config)
161    }
162}
163
164/// Sandbox execution result
165#[derive(Debug, Clone)]
166pub struct SandboxResult {
167    /// Exit code
168    pub exit_code: i32,
169    /// Signal that killed process (if any)
170    pub signal: Option<i32>,
171    /// Whether timeout occurred
172    pub timed_out: bool,
173    /// Memory usage in bytes
174    pub memory_peak: u64,
175    /// CPU time in microseconds
176    pub cpu_time_us: u64,
177    /// Wall clock time in seconds
178    pub wall_time_ms: u64,
179}
180
181impl SandboxResult {
182    /// Check if process was killed by seccomp (SIGSYS - signal 31)
183    /// Returns true if exit code is 159 (128 + 31)
184    pub fn killed_by_seccomp(&self) -> bool {
185        self.exit_code == 159
186    }
187
188    /// Get human-readable error message if process failed due to seccomp
189    pub fn seccomp_error(&self) -> Option<&'static str> {
190        if self.killed_by_seccomp() {
191            Some("The action requires more permissions than were granted.")
192        } else {
193            None
194        }
195    }
196
197    /// Convert to Result, returning error if process was killed by seccomp
198    pub fn check_seccomp_error(&self) -> crate::errors::Result<&SandboxResult> {
199        if self.killed_by_seccomp() {
200            Err(SandboxError::PermissionDenied(
201                "The seccomp profile is too restrictive for this operation. \
202                 Try using a less restrictive profile (e.g., SeccompProfile::Compute or SeccompProfile::Unrestricted)"
203                    .to_string(),
204            ))
205        } else {
206            Ok(self)
207        }
208    }
209}
210
211/// Active sandbox
212pub struct Sandbox {
213    config: SandboxConfig,
214    pid: Option<Pid>,
215    cgroup: Option<Cgroup>,
216    start_time: Option<Instant>,
217}
218
219impl Sandbox {
220    /// Create new sandbox
221    fn new(config: SandboxConfig) -> Result<Self> {
222        // Create root directory
223        fs::create_dir_all(&config.root).map_err(|e| {
224            SandboxError::Io(std::io::Error::other(format!(
225                "Failed to create root directory: {}",
226                e
227            )))
228        })?;
229
230        Ok(Self {
231            config,
232            pid: None,
233            cgroup: None,
234            start_time: None,
235        })
236    }
237
238    /// Get sandbox ID
239    pub fn id(&self) -> &str {
240        &self.config.id
241    }
242
243    /// Get sandbox root
244    pub fn root(&self) -> &Path {
245        &self.config.root
246    }
247
248    /// Check if sandbox is running
249    pub fn is_running(&self) -> bool {
250        self.pid.is_some()
251    }
252
253    /// Run program in sandbox
254    pub fn run(&mut self, program: &str, args: &[&str]) -> Result<SandboxResult> {
255        if self.is_running() {
256            return Err(SandboxError::AlreadyRunning);
257        }
258
259        if !utils::is_root() {
260            return Err(SandboxError::PermissionDenied(
261                "Sandbox requires root privileges for proper isolation (cgroups, namespaces). \
262                 Running without root would bypass all resource limits and namespace isolation. \
263                 Use sudo or run as root."
264                    .to_string(),
265            ));
266        }
267
268        self.start_time = Some(Instant::now());
269
270        let cgroup_name = format!("sandbox-{}", self.config.id);
271        let cgroup = Cgroup::new(&cgroup_name, Pid::from_raw(std::process::id() as i32))?;
272
273        let cgroup_config = CgroupConfig {
274            memory_limit: self.config.memory_limit,
275            cpu_quota: self.config.cpu_quota,
276            cpu_period: self.config.cpu_period,
277            max_pids: self.config.max_pids,
278            cpu_weight: None,
279        };
280        cgroup.apply_config(&cgroup_config)?;
281
282        self.cgroup = Some(cgroup);
283
284        let process_config = ProcessConfig {
285            program: program.to_string(),
286            args: args.iter().map(|s| s.to_string()).collect(),
287            env: Vec::new(),
288            cwd: None,
289            chroot_dir: None,
290            uid: None,
291            gid: None,
292            seccomp: Some(crate::isolation::seccomp::SeccompFilter::from_profile(
293                self.config.seccomp_profile.clone(),
294            )),
295            inherit_env: true,
296        };
297
298        let process_result =
299            ProcessExecutor::execute(process_config, self.config.namespace_config.clone())?;
300
301        self.pid = Some(process_result.pid);
302
303        let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
304        let (memory_peak, _) = self.get_resource_usage()?;
305
306        Ok(SandboxResult {
307            exit_code: process_result.exit_status,
308            signal: process_result.signal,
309            timed_out: false,
310            memory_peak,
311            cpu_time_us: process_result.exec_time_ms * 1000,
312            wall_time_ms,
313        })
314    }
315
316    /// Run program with streaming output
317    pub fn run_with_stream(
318        &mut self,
319        program: &str,
320        args: &[&str],
321    ) -> Result<(SandboxResult, ProcessStream)> {
322        if self.is_running() {
323            return Err(SandboxError::AlreadyRunning);
324        }
325
326        self.start_time = Some(Instant::now());
327
328        let cgroup_name = format!("sandbox-{}", self.config.id);
329        let cgroup = Cgroup::new(&cgroup_name, Pid::from_raw(std::process::id() as i32))?;
330
331        let cgroup_config = CgroupConfig {
332            memory_limit: self.config.memory_limit,
333            cpu_quota: self.config.cpu_quota,
334            cpu_period: self.config.cpu_period,
335            max_pids: self.config.max_pids,
336            cpu_weight: None,
337        };
338        cgroup.apply_config(&cgroup_config)?;
339
340        self.cgroup = Some(cgroup);
341
342        let process_config = ProcessConfig {
343            program: program.to_string(),
344            args: args.iter().map(|s| s.to_string()).collect(),
345            env: Vec::new(),
346            cwd: None,
347            chroot_dir: None,
348            uid: None,
349            gid: None,
350            seccomp: Some(crate::isolation::seccomp::SeccompFilter::from_profile(
351                self.config.seccomp_profile.clone(),
352            )),
353            inherit_env: true,
354        };
355
356        let (process_result, stream) = ProcessExecutor::execute_with_stream(
357            process_config,
358            self.config.namespace_config.clone(),
359            true,
360        )?;
361
362        self.pid = Some(process_result.pid);
363
364        let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
365        let (memory_peak, _) = self.get_resource_usage().unwrap_or((0, 0));
366
367        let sandbox_result = SandboxResult {
368            exit_code: process_result.exit_status,
369            signal: process_result.signal,
370            timed_out: false,
371            memory_peak,
372            cpu_time_us: process_result.exec_time_ms * 1000,
373            wall_time_ms,
374        };
375
376        let stream =
377            stream.ok_or_else(|| SandboxError::Io(std::io::Error::other("stream unavailable")))?;
378
379        Ok((sandbox_result, stream))
380    }
381
382    pub fn kill(&mut self) -> Result<()> {
383        if let Some(pid) = self.pid {
384            kill(pid, Signal::SIGKILL)
385                .map_err(|e| SandboxError::Syscall(format!("Failed to kill process: {}", e)))?;
386            self.pid = None;
387        }
388        Ok(())
389    }
390
391    /// Get resource usage
392    pub fn get_resource_usage(&self) -> Result<(u64, u64)> {
393        if let Some(ref cgroup) = self.cgroup {
394            let memory = cgroup.get_memory_usage()?;
395            let cpu = cgroup.get_cpu_usage()?;
396            Ok((memory, cpu))
397        } else {
398            Err(SandboxError::InvalidConfig(
399                "Cannot get resource usage: cgroup not initialized".to_string(),
400            ))
401        }
402    }
403}
404
405impl Drop for Sandbox {
406    fn drop(&mut self) {
407        let _ = self.kill();
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::resources::cgroup::Cgroup;
415    use crate::test_support::serial_guard;
416    use crate::utils;
417    use std::env;
418    use std::time::Duration;
419    use tempfile::tempdir;
420
421    fn config_with_temp_root(id: &str) -> (tempfile::TempDir, SandboxConfig) {
422        let tmp = tempdir().unwrap();
423        let config = SandboxConfig {
424            id: id.to_string(),
425            root: tmp.path().join("root"),
426            namespace_config: NamespaceConfig::minimal(),
427            ..Default::default()
428        };
429        (tmp, config)
430    }
431
432    struct RootOverrideGuard;
433
434    impl RootOverrideGuard {
435        fn enable() -> Self {
436            utils::set_root_override(Some(true));
437            Self
438        }
439    }
440
441    impl Drop for RootOverrideGuard {
442        fn drop(&mut self) {
443            utils::set_root_override(None);
444        }
445    }
446
447    struct EnvVarGuard {
448        key: &'static str,
449        prev: Option<String>,
450    }
451
452    impl EnvVarGuard {
453        fn new(key: &'static str, value: &str) -> Self {
454            let prev = env::var(key).ok();
455            unsafe {
456                env::set_var(key, value);
457            }
458            Self { key, prev }
459        }
460    }
461
462    impl Drop for EnvVarGuard {
463        fn drop(&mut self) {
464            if let Some(ref value) = self.prev {
465                unsafe {
466                    env::set_var(self.key, value);
467                }
468            } else {
469                unsafe {
470                    env::remove_var(self.key);
471                }
472            }
473        }
474    }
475
476    #[test]
477    fn test_sandbox_config_default() {
478        let config = SandboxConfig::default();
479        assert_eq!(config.id, "default");
480        assert!(config.memory_limit.is_none());
481    }
482
483    #[test]
484    fn test_sandbox_config_validate() {
485        let config = SandboxConfig {
486            id: String::new(),
487            ..Default::default()
488        };
489
490        // Validation might fail due to root requirement, but we can test ID validation
491        // by checking the error message
492        if let Err(e) = config.validate() {
493            // Expected to fail, either due to root or empty ID
494            assert!(e.to_string().contains("ID") || e.to_string().contains("root"));
495        }
496    }
497
498    #[test]
499    fn test_sandbox_builder_new() {
500        let builder = SandboxBuilder::new("test");
501        assert_eq!(builder.config.id, "test");
502    }
503
504    #[test]
505    fn test_sandbox_builder_memory_limit() {
506        let builder = SandboxBuilder::new("test").memory_limit(100 * 1024 * 1024);
507        assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
508    }
509
510    #[test]
511    fn test_sandbox_builder_memory_limit_str() -> Result<()> {
512        let builder = SandboxBuilder::new("test").memory_limit_str("100M")?;
513        assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
514        Ok(())
515    }
516
517    #[test]
518    fn test_sandbox_builder_cpu_limit() {
519        let builder = SandboxBuilder::new("test").cpu_limit_percent(50);
520        assert!(builder.config.cpu_quota.is_some());
521    }
522
523    #[test]
524    fn test_sandbox_builder_cpu_limit_zero() {
525        let builder = SandboxBuilder::new("test").cpu_limit_percent(0);
526        assert!(builder.config.cpu_quota.is_none());
527    }
528
529    #[test]
530    fn test_sandbox_builder_cpu_limit_over_100() {
531        let builder = SandboxBuilder::new("test").cpu_limit_percent(150);
532        assert!(builder.config.cpu_quota.is_none());
533    }
534
535    #[test]
536    fn test_sandbox_builder_cpu_quota() {
537        let builder = SandboxBuilder::new("test").cpu_quota(50000, 100000);
538        assert_eq!(builder.config.cpu_quota, Some(50000));
539        assert_eq!(builder.config.cpu_period, Some(100000));
540    }
541
542    #[test]
543    fn test_sandbox_builder_max_pids() {
544        let builder = SandboxBuilder::new("test").max_pids(10);
545        assert_eq!(builder.config.max_pids, Some(10));
546    }
547
548    #[test]
549    fn test_sandbox_builder_seccomp_profile() {
550        let builder = SandboxBuilder::new("test").seccomp_profile(SeccompProfile::IoHeavy);
551        assert_eq!(builder.config.seccomp_profile, SeccompProfile::IoHeavy);
552    }
553
554    #[test]
555    fn test_sandbox_builder_root() {
556        let tmp = tempdir().unwrap();
557        let builder = SandboxBuilder::new("test").root(tmp.path());
558        assert_eq!(builder.config.root, tmp.path());
559    }
560
561    #[test]
562    fn test_sandbox_builder_timeout() {
563        let builder = SandboxBuilder::new("test").timeout(Duration::from_secs(30));
564        assert_eq!(builder.config.timeout, Some(Duration::from_secs(30)));
565    }
566
567    #[test]
568    fn test_sandbox_builder_namespaces() {
569        let ns_config = NamespaceConfig::minimal();
570        let builder = SandboxBuilder::new("test").namespaces(ns_config.clone());
571        assert_eq!(builder.config.namespace_config, ns_config);
572    }
573
574    #[test]
575    fn test_sandbox_result() {
576        let result = SandboxResult {
577            exit_code: 0,
578            signal: None,
579            timed_out: false,
580            memory_peak: 1024,
581            cpu_time_us: 5000,
582            wall_time_ms: 100,
583        };
584        assert_eq!(result.exit_code, 0);
585        assert!(!result.timed_out);
586    }
587
588    #[test]
589    fn sandbox_config_invariants_detect_empty_id() {
590        let config = SandboxConfig {
591            id: String::new(),
592            ..Default::default()
593        };
594        assert!(config.validate_invariants().is_err());
595    }
596
597    #[test]
598    fn sandbox_config_invariants_detect_disabled_namespaces() {
599        let config = SandboxConfig {
600            namespace_config: NamespaceConfig {
601                pid: false,
602                ipc: false,
603                net: false,
604                mount: false,
605                uts: false,
606                user: false,
607            },
608            ..Default::default()
609        };
610        assert!(config.validate_invariants().is_err());
611    }
612
613    #[test]
614    fn sandbox_provides_id_and_root() {
615        let (_tmp, config) = config_with_temp_root("sand-id");
616        let sandbox = Sandbox::new(config.clone()).unwrap();
617        assert_eq!(sandbox.id(), "sand-id");
618        assert!(sandbox.root().ends_with("root"));
619        assert!(!sandbox.is_running());
620    }
621
622    #[test]
623    fn sandbox_run_requires_root() {
624        let _guard = serial_guard();
625        let (_tmp, config) = config_with_temp_root("run-test");
626        let mut sandbox = Sandbox::new(config).unwrap();
627        let args: [&str; 1] = ["hello"];
628        let result = sandbox.run("/bin/echo", &args);
629        assert!(result.is_err());
630        assert!(result.unwrap_err().to_string().contains("root"));
631    }
632
633    #[test]
634    fn sandbox_run_returns_error_if_already_running() {
635        let _guard = serial_guard();
636        let (_tmp, config) = config_with_temp_root("already-running");
637        let mut sandbox = Sandbox::new(config).unwrap();
638
639        // Set PID to simulate already running
640        sandbox.pid = Some(Pid::from_raw(1));
641
642        let args: [&str; 1] = ["test"];
643        let result = sandbox.run("/bin/echo", &args);
644
645        assert!(result.is_err());
646        assert!(result.unwrap_err().to_string().contains("already running"));
647    }
648
649    #[test]
650    fn test_sandbox_builder_build_creates_sandbox() {
651        let _guard = serial_guard();
652        let _root_guard = RootOverrideGuard::enable();
653        let tmp = tempdir().unwrap();
654        let sandbox = SandboxBuilder::new("build-test").root(tmp.path()).build();
655
656        assert!(sandbox.is_ok());
657    }
658
659    #[test]
660    fn test_sandbox_builder_build_validates_config() {
661        let _guard = serial_guard();
662        let tmp = tempdir().unwrap();
663        let result = SandboxBuilder::new("").root(tmp.path()).build();
664
665        assert!(result.is_err());
666    }
667
668    #[test]
669    fn sandbox_reports_resource_usage_from_cgroup() {
670        let (tmp, mut config) = config_with_temp_root("resource-test");
671        config.root = tmp.path().join("root");
672        let mut sandbox = Sandbox::new(config).unwrap();
673
674        let cg_path = tmp.path().join("cgroup");
675        std::fs::create_dir_all(&cg_path).unwrap();
676        std::fs::write(cg_path.join("memory.current"), "1234").unwrap();
677        std::fs::write(cg_path.join("cpu.stat"), "usage_usec 77\n").unwrap();
678
679        sandbox.cgroup = Some(Cgroup::for_testing(cg_path.clone()));
680        let (mem, cpu) = sandbox.get_resource_usage().unwrap();
681        assert_eq!(mem, 1234);
682        assert_eq!(cpu, 77);
683    }
684
685    #[test]
686    #[ignore]
687    fn sandbox_builder_builds_when_root_override() {
688        let _guard = serial_guard();
689        let _root_guard = RootOverrideGuard::enable();
690        let tmp = tempdir().unwrap();
691        let _env_guard = EnvVarGuard::new("SANDBOX_CGROUP_ROOT", tmp.path().to_str().unwrap());
692
693        let mut sandbox = SandboxBuilder::new("integration")
694            .memory_limit(1024)
695            .cpu_limit_percent(10)
696            .max_pids(4)
697            .seccomp_profile(SeccompProfile::Minimal)
698            .root(tmp.path())
699            .timeout(Duration::from_secs(1))
700            .namespaces(NamespaceConfig::minimal())
701            .build()
702            .unwrap();
703
704        let args: [&str; 0] = [];
705        let result = sandbox.run("/bin/true", &args).unwrap();
706        assert_eq!(result.exit_code, 0);
707    }
708
709    #[test]
710    fn sandbox_kill_handles_missing_pid() {
711        let (_tmp, config) = config_with_temp_root("kill-test");
712        let mut sandbox = Sandbox::new(config).unwrap();
713        sandbox.kill().unwrap();
714    }
715
716    #[test]
717    fn sandbox_kill_terminates_real_process() {
718        let (_tmp, config) = config_with_temp_root("kill-proc");
719        let mut sandbox = Sandbox::new(config).unwrap();
720        let mut child = std::process::Command::new("sleep")
721            .arg("1")
722            .spawn()
723            .unwrap();
724        sandbox.pid = Some(Pid::from_raw(child.id() as i32));
725        sandbox.kill().unwrap();
726        let _ = child.wait();
727    }
728
729    #[test]
730    fn sandbox_get_resource_usage_without_cgroup_fails() {
731        let (_tmp, config) = config_with_temp_root("no-cgroup");
732        let sandbox = Sandbox::new(config).unwrap();
733        let result = sandbox.get_resource_usage();
734        assert!(result.is_err());
735    }
736
737    #[test]
738    #[ignore]
739    fn sandbox_run_with_stream_captures_output() {
740        let _guard = serial_guard();
741        let _root_guard = RootOverrideGuard::enable();
742        let (_tmp, config) = config_with_temp_root("stream-test");
743        let mut sandbox = Sandbox::new(config).unwrap();
744
745        let (result, stream) = sandbox
746            .run_with_stream("/bin/echo", &["hello world"])
747            .unwrap();
748
749        let chunks: Vec<_> = stream.into_iter().collect();
750
751        assert!(!chunks.is_empty());
752        assert_eq!(result.exit_code, 0);
753
754        let has_stdout = chunks
755            .iter()
756            .any(|chunk| matches!(chunk, crate::StreamChunk::Stdout(_)));
757        let has_exit = chunks
758            .iter()
759            .any(|chunk| matches!(chunk, crate::StreamChunk::Exit { .. }));
760
761        assert!(has_stdout, "Should have captured stdout");
762        assert!(has_exit, "Should have exit chunk");
763    }
764
765    #[test]
766    fn test_sandbox_result_killed_by_seccomp() {
767        let result = SandboxResult {
768            exit_code: 159,
769            signal: None,
770            timed_out: false,
771            memory_peak: 0,
772            cpu_time_us: 0,
773            wall_time_ms: 0,
774        };
775        assert!(result.killed_by_seccomp());
776    }
777
778    #[test]
779    fn test_sandbox_result_not_killed_by_seccomp() {
780        let result = SandboxResult {
781            exit_code: 0,
782            signal: None,
783            timed_out: false,
784            memory_peak: 0,
785            cpu_time_us: 0,
786            wall_time_ms: 0,
787        };
788        assert!(!result.killed_by_seccomp());
789    }
790
791    #[test]
792    fn test_sandbox_result_seccomp_error_message() {
793        let result = SandboxResult {
794            exit_code: 159,
795            signal: None,
796            timed_out: false,
797            memory_peak: 0,
798            cpu_time_us: 0,
799            wall_time_ms: 0,
800        };
801        let msg = result.seccomp_error();
802        assert!(msg.is_some());
803        assert!(msg.unwrap().contains("permissions"));
804    }
805
806    #[test]
807    fn test_sandbox_result_check_seccomp_error_when_killed() {
808        let result = SandboxResult {
809            exit_code: 159,
810            signal: None,
811            timed_out: false,
812            memory_peak: 0,
813            cpu_time_us: 0,
814            wall_time_ms: 0,
815        };
816        let check_result = result.check_seccomp_error();
817        assert!(check_result.is_err());
818        let err = check_result.unwrap_err();
819        assert!(err.to_string().contains("restrictive"));
820    }
821
822    #[test]
823    fn test_sandbox_result_check_seccomp_error_when_success() {
824        let result = SandboxResult {
825            exit_code: 0,
826            signal: None,
827            timed_out: false,
828            memory_peak: 0,
829            cpu_time_us: 0,
830            wall_time_ms: 0,
831        };
832        let check_result = result.check_seccomp_error();
833        assert!(check_result.is_ok());
834    }
835}