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