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 {
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 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,
317 wall_time_ms,
318 })
319 } else {
320 warn!("Running without full isolation (not root). Use sudo for production sandboxes.");
321 let output = Command::new(program)
322 .args(args)
323 .output()
324 .map_err(SandboxError::Io)?;
325
326 let exit_code = output.status.code().unwrap_or(-1);
327 let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
328
329 Ok(SandboxResult {
330 exit_code,
331 signal: None,
332 timed_out: false,
333 memory_peak: 0,
334 cpu_time_us: 0,
335 wall_time_ms,
336 })
337 }
338 }
339
340 pub fn run_with_stream(
342 &mut self,
343 program: &str,
344 args: &[&str],
345 ) -> Result<(SandboxResult, ProcessStream)> {
346 if self.is_running() {
347 return Err(SandboxError::AlreadyRunning);
348 }
349
350 self.start_time = Some(Instant::now());
351
352 let cgroup_name = format!("sandbox-{}", self.config.id);
353 let cgroup = Cgroup::new(&cgroup_name, Pid::from_raw(std::process::id() as i32))?;
354
355 let cgroup_config = CgroupConfig {
356 memory_limit: self.config.memory_limit,
357 cpu_quota: self.config.cpu_quota,
358 cpu_period: self.config.cpu_period,
359 max_pids: self.config.max_pids,
360 cpu_weight: None,
361 };
362 cgroup.apply_config(&cgroup_config)?;
363
364 self.cgroup = Some(cgroup);
365
366 let process_config = ProcessConfig {
367 program: program.to_string(),
368 args: args.iter().map(|s| s.to_string()).collect(),
369 env: Vec::new(),
370 cwd: None,
371 chroot_dir: None,
372 uid: None,
373 gid: None,
374 seccomp: Some(crate::isolation::seccomp::SeccompFilter::from_profile(
375 self.config.seccomp_profile.clone(),
376 )),
377 inherit_env: true,
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();
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use crate::resources::cgroup::Cgroup;
437 use crate::test_support::serial_guard;
438 use crate::utils;
439 use std::env;
440 use std::time::Duration;
441 use tempfile::tempdir;
442
443 fn config_with_temp_root(id: &str) -> (tempfile::TempDir, SandboxConfig) {
444 let tmp = tempdir().unwrap();
445 let config = SandboxConfig {
446 id: id.to_string(),
447 root: tmp.path().join("root"),
448 namespace_config: NamespaceConfig::minimal(),
449 ..Default::default()
450 };
451 (tmp, config)
452 }
453
454 struct RootOverrideGuard;
455
456 impl RootOverrideGuard {
457 fn enable() -> Self {
458 utils::set_root_override(Some(true));
459 Self
460 }
461 }
462
463 impl Drop for RootOverrideGuard {
464 fn drop(&mut self) {
465 utils::set_root_override(None);
466 }
467 }
468
469 struct EnvVarGuard {
470 key: &'static str,
471 prev: Option<String>,
472 }
473
474 impl EnvVarGuard {
475 fn new(key: &'static str, value: &str) -> Self {
476 let prev = env::var(key).ok();
477 unsafe {
478 env::set_var(key, value);
479 }
480 Self { key, prev }
481 }
482 }
483
484 impl Drop for EnvVarGuard {
485 fn drop(&mut self) {
486 if let Some(ref value) = self.prev {
487 unsafe {
488 env::set_var(self.key, value);
489 }
490 } else {
491 unsafe {
492 env::remove_var(self.key);
493 }
494 }
495 }
496 }
497
498 #[test]
499 fn test_sandbox_config_default() {
500 let config = SandboxConfig::default();
501 assert_eq!(config.id, "default");
502 assert!(config.memory_limit.is_none());
503 }
504
505 #[test]
506 fn test_sandbox_config_validate() {
507 let config = SandboxConfig {
508 id: String::new(),
509 ..Default::default()
510 };
511
512 if let Err(e) = config.validate() {
515 assert!(e.to_string().contains("ID") || e.to_string().contains("root"));
517 }
518 }
519
520 #[test]
521 fn test_sandbox_builder_new() {
522 let builder = SandboxBuilder::new("test");
523 assert_eq!(builder.config.id, "test");
524 }
525
526 #[test]
527 fn test_sandbox_builder_memory_limit() {
528 let builder = SandboxBuilder::new("test").memory_limit(100 * 1024 * 1024);
529 assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
530 }
531
532 #[test]
533 fn test_sandbox_builder_memory_limit_str() -> Result<()> {
534 let builder = SandboxBuilder::new("test").memory_limit_str("100M")?;
535 assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
536 Ok(())
537 }
538
539 #[test]
540 fn test_sandbox_builder_cpu_limit() {
541 let builder = SandboxBuilder::new("test").cpu_limit_percent(50);
542 assert!(builder.config.cpu_quota.is_some());
543 }
544
545 #[test]
546 fn test_sandbox_builder_cpu_limit_zero() {
547 let builder = SandboxBuilder::new("test").cpu_limit_percent(0);
548 assert!(builder.config.cpu_quota.is_none());
549 }
550
551 #[test]
552 fn test_sandbox_builder_cpu_limit_over_100() {
553 let builder = SandboxBuilder::new("test").cpu_limit_percent(150);
554 assert!(builder.config.cpu_quota.is_none());
555 }
556
557 #[test]
558 fn test_sandbox_builder_cpu_quota() {
559 let builder = SandboxBuilder::new("test").cpu_quota(50000, 100000);
560 assert_eq!(builder.config.cpu_quota, Some(50000));
561 assert_eq!(builder.config.cpu_period, Some(100000));
562 }
563
564 #[test]
565 fn test_sandbox_builder_max_pids() {
566 let builder = SandboxBuilder::new("test").max_pids(10);
567 assert_eq!(builder.config.max_pids, Some(10));
568 }
569
570 #[test]
571 fn test_sandbox_builder_seccomp_profile() {
572 let builder = SandboxBuilder::new("test").seccomp_profile(SeccompProfile::IoHeavy);
573 assert_eq!(builder.config.seccomp_profile, SeccompProfile::IoHeavy);
574 }
575
576 #[test]
577 fn test_sandbox_builder_root() {
578 let tmp = tempdir().unwrap();
579 let builder = SandboxBuilder::new("test").root(tmp.path());
580 assert_eq!(builder.config.root, tmp.path());
581 }
582
583 #[test]
584 fn test_sandbox_builder_timeout() {
585 let builder = SandboxBuilder::new("test").timeout(Duration::from_secs(30));
586 assert_eq!(builder.config.timeout, Some(Duration::from_secs(30)));
587 }
588
589 #[test]
590 fn test_sandbox_builder_namespaces() {
591 let ns_config = NamespaceConfig::minimal();
592 let builder = SandboxBuilder::new("test").namespaces(ns_config.clone());
593 assert_eq!(builder.config.namespace_config, ns_config);
594 }
595
596 #[test]
597 fn test_sandbox_result() {
598 let result = SandboxResult {
599 exit_code: 0,
600 signal: None,
601 timed_out: false,
602 memory_peak: 1024,
603 cpu_time_us: 5000,
604 wall_time_ms: 100,
605 };
606 assert_eq!(result.exit_code, 0);
607 assert!(!result.timed_out);
608 }
609
610 #[test]
611 fn sandbox_config_invariants_detect_empty_id() {
612 let config = SandboxConfig {
613 id: String::new(),
614 ..Default::default()
615 };
616 assert!(config.validate_invariants().is_err());
617 }
618
619 #[test]
620 fn sandbox_config_invariants_detect_disabled_namespaces() {
621 let config = SandboxConfig {
622 namespace_config: NamespaceConfig {
623 pid: false,
624 ipc: false,
625 net: false,
626 mount: false,
627 uts: false,
628 user: false,
629 },
630 ..Default::default()
631 };
632 assert!(config.validate_invariants().is_err());
633 }
634
635 #[test]
636 fn sandbox_provides_id_and_root() {
637 let (_tmp, config) = config_with_temp_root("sand-id");
638 let sandbox = Sandbox::new(config.clone()).unwrap();
639 assert_eq!(sandbox.id(), "sand-id");
640 assert!(sandbox.root().ends_with("root"));
641 assert!(!sandbox.is_running());
642 }
643
644 #[test]
645 fn sandbox_run_executes_command_without_root() {
646 let _guard = serial_guard();
647 let (_tmp, config) = config_with_temp_root("run-test");
648 let mut sandbox = Sandbox::new(config).unwrap();
649 let args: [&str; 1] = ["hello"];
650 let result = sandbox.run("/bin/echo", &args).unwrap();
651 assert_eq!(result.exit_code, 0);
652 assert!(!sandbox.is_running());
653 }
654
655 #[test]
656 fn sandbox_run_returns_error_if_already_running() {
657 let _guard = serial_guard();
658 let (_tmp, config) = config_with_temp_root("already-running");
659 let mut sandbox = Sandbox::new(config).unwrap();
660
661 sandbox.pid = Some(Pid::from_raw(1));
663
664 let args: [&str; 1] = ["test"];
665 let result = sandbox.run("/bin/echo", &args);
666
667 assert!(result.is_err());
668 assert!(result.unwrap_err().to_string().contains("already running"));
669 }
670
671 #[test]
672 fn test_sandbox_builder_build_creates_sandbox() {
673 let _guard = serial_guard();
674 let _root_guard = RootOverrideGuard::enable();
675 let tmp = tempdir().unwrap();
676 let sandbox = SandboxBuilder::new("build-test").root(tmp.path()).build();
677
678 assert!(sandbox.is_ok());
679 }
680
681 #[test]
682 fn test_sandbox_builder_build_validates_config() {
683 let _guard = serial_guard();
684 let tmp = tempdir().unwrap();
685 let result = SandboxBuilder::new("").root(tmp.path()).build();
686
687 assert!(result.is_err());
688 }
689
690 #[test]
691 fn sandbox_reports_resource_usage_from_cgroup() {
692 let (tmp, mut config) = config_with_temp_root("resource-test");
693 config.root = tmp.path().join("root");
694 let mut sandbox = Sandbox::new(config).unwrap();
695
696 let cg_path = tmp.path().join("cgroup");
697 std::fs::create_dir_all(&cg_path).unwrap();
698 std::fs::write(cg_path.join("memory.current"), "1234").unwrap();
699 std::fs::write(cg_path.join("cpu.stat"), "usage_usec 77\n").unwrap();
700
701 sandbox.cgroup = Some(Cgroup::for_testing(cg_path.clone()));
702 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
703 assert_eq!(mem, 1234);
704 assert_eq!(cpu, 77);
705 }
706
707 #[test]
708 #[ignore]
709 fn sandbox_builder_builds_when_root_override() {
710 let _guard = serial_guard();
711 let _root_guard = RootOverrideGuard::enable();
712 let tmp = tempdir().unwrap();
713 let _env_guard = EnvVarGuard::new("SANDBOX_CGROUP_ROOT", tmp.path().to_str().unwrap());
714
715 let mut sandbox = SandboxBuilder::new("integration")
716 .memory_limit(1024)
717 .cpu_limit_percent(10)
718 .max_pids(4)
719 .seccomp_profile(SeccompProfile::Minimal)
720 .root(tmp.path())
721 .timeout(Duration::from_secs(1))
722 .namespaces(NamespaceConfig::minimal())
723 .build()
724 .unwrap();
725
726 let args: [&str; 0] = [];
727 let result = sandbox.run("/bin/true", &args).unwrap();
728 assert_eq!(result.exit_code, 0);
729 }
730
731 #[test]
732 fn sandbox_kill_handles_missing_pid() {
733 let (_tmp, config) = config_with_temp_root("kill-test");
734 let mut sandbox = Sandbox::new(config).unwrap();
735 sandbox.kill().unwrap();
736 }
737
738 #[test]
739 fn sandbox_kill_terminates_real_process() {
740 let (_tmp, config) = config_with_temp_root("kill-proc");
741 let mut sandbox = Sandbox::new(config).unwrap();
742 let mut child = std::process::Command::new("sleep")
743 .arg("1")
744 .spawn()
745 .unwrap();
746 sandbox.pid = Some(Pid::from_raw(child.id() as i32));
747 sandbox.kill().unwrap();
748 let _ = child.wait();
749 }
750
751 #[test]
752 fn sandbox_get_resource_usage_without_cgroup() {
753 let (_tmp, config) = config_with_temp_root("no-cgroup");
754 let sandbox = Sandbox::new(config).unwrap();
755 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
756 assert_eq!(mem, 0);
757 assert_eq!(cpu, 0);
758 }
759
760 #[test]
761 #[ignore]
762 fn sandbox_run_with_stream_captures_output() {
763 let _guard = serial_guard();
764 let _root_guard = RootOverrideGuard::enable();
765 let (_tmp, config) = config_with_temp_root("stream-test");
766 let mut sandbox = Sandbox::new(config).unwrap();
767
768 let (result, stream) = sandbox
769 .run_with_stream("/bin/echo", &["hello world"])
770 .unwrap();
771
772 let chunks: Vec<_> = stream.into_iter().collect();
773
774 assert!(!chunks.is_empty());
775 assert_eq!(result.exit_code, 0);
776
777 let has_stdout = chunks
778 .iter()
779 .any(|chunk| matches!(chunk, crate::StreamChunk::Stdout(_)));
780 let has_exit = chunks
781 .iter()
782 .any(|chunk| matches!(chunk, crate::StreamChunk::Exit { .. }));
783
784 assert!(has_stdout, "Should have captured stdout");
785 assert!(has_exit, "Should have exit chunk");
786 }
787
788 #[test]
789 fn test_sandbox_result_killed_by_seccomp() {
790 let result = SandboxResult {
791 exit_code: 159,
792 signal: None,
793 timed_out: false,
794 memory_peak: 0,
795 cpu_time_us: 0,
796 wall_time_ms: 0,
797 };
798 assert!(result.killed_by_seccomp());
799 }
800
801 #[test]
802 fn test_sandbox_result_not_killed_by_seccomp() {
803 let result = SandboxResult {
804 exit_code: 0,
805 signal: None,
806 timed_out: false,
807 memory_peak: 0,
808 cpu_time_us: 0,
809 wall_time_ms: 0,
810 };
811 assert!(!result.killed_by_seccomp());
812 }
813
814 #[test]
815 fn test_sandbox_result_seccomp_error_message() {
816 let result = SandboxResult {
817 exit_code: 159,
818 signal: None,
819 timed_out: false,
820 memory_peak: 0,
821 cpu_time_us: 0,
822 wall_time_ms: 0,
823 };
824 let msg = result.seccomp_error();
825 assert!(msg.is_some());
826 assert!(msg.unwrap().contains("permissions"));
827 }
828
829 #[test]
830 fn test_sandbox_result_check_seccomp_error_when_killed() {
831 let result = SandboxResult {
832 exit_code: 159,
833 signal: None,
834 timed_out: false,
835 memory_peak: 0,
836 cpu_time_us: 0,
837 wall_time_ms: 0,
838 };
839 let check_result = result.check_seccomp_error();
840 assert!(check_result.is_err());
841 let err = check_result.unwrap_err();
842 assert!(err.to_string().contains("restrictive"));
843 }
844
845 #[test]
846 fn test_sandbox_result_check_seccomp_error_when_success() {
847 let result = SandboxResult {
848 exit_code: 0,
849 signal: None,
850 timed_out: false,
851 memory_peak: 0,
852 cpu_time_us: 0,
853 wall_time_ms: 0,
854 };
855 let check_result = result.check_seccomp_error();
856 assert!(check_result.is_ok());
857 }
858}