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
183pub struct Sandbox {
185 config: SandboxConfig,
186 pid: Option<Pid>,
187 cgroup: Option<Cgroup>,
188 start_time: Option<Instant>,
189}
190
191impl Sandbox {
192 fn new(config: SandboxConfig) -> Result<Self> {
194 fs::create_dir_all(&config.root).map_err(|e| {
196 SandboxError::Io(std::io::Error::other(format!(
197 "Failed to create root directory: {}",
198 e
199 )))
200 })?;
201
202 Ok(Self {
203 config,
204 pid: None,
205 cgroup: None,
206 start_time: None,
207 })
208 }
209
210 pub fn id(&self) -> &str {
212 &self.config.id
213 }
214
215 pub fn root(&self) -> &Path {
217 &self.config.root
218 }
219
220 pub fn is_running(&self) -> bool {
222 self.pid.is_some()
223 }
224
225 pub fn run(&mut self, program: &str, args: &[&str]) -> Result<SandboxResult> {
227 if self.is_running() {
228 return Err(SandboxError::AlreadyRunning);
229 }
230
231 self.start_time = Some(Instant::now());
232
233 if utils::is_root() {
234 let cgroup_name = format!("sandbox-{}", self.config.id);
235 let cgroup = Cgroup::new(&cgroup_name, Pid::from_raw(std::process::id() as i32))?;
236
237 let cgroup_config = CgroupConfig {
238 memory_limit: self.config.memory_limit,
239 cpu_quota: self.config.cpu_quota,
240 cpu_period: self.config.cpu_period,
241 max_pids: self.config.max_pids,
242 cpu_weight: None,
243 };
244 cgroup.apply_config(&cgroup_config)?;
245
246 self.cgroup = Some(cgroup);
247 } else {
248 warn!(
249 "Skipping cgroup configuration for sandbox {} (not running as root)",
250 self.config.id
251 );
252 }
253
254 let process_config = ProcessConfig {
256 program: program.to_string(),
257 args: args.iter().map(|s| s.to_string()).collect(),
258 env: Vec::new(), cwd: None,
260 chroot_dir: None,
261 uid: None,
262 gid: None,
263 seccomp: Some(crate::isolation::seccomp::SeccompFilter::from_profile(
264 self.config.seccomp_profile.clone(),
265 )),
266 };
267
268 if utils::is_root() {
270 let process_result =
272 ProcessExecutor::execute(process_config, self.config.namespace_config.clone())?;
273
274 self.pid = Some(process_result.pid);
275
276 let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
277
278 let (memory_peak, _) = self.get_resource_usage().unwrap_or((0, 0));
280
281 Ok(SandboxResult {
282 exit_code: process_result.exit_status,
283 signal: process_result.signal,
284 timed_out: false,
285 memory_peak,
286 cpu_time_us: process_result.exec_time_ms * 1000, wall_time_ms,
288 })
289 } else {
290 warn!("Running without full isolation (not root). Use sudo for production sandboxes.");
292 let output = Command::new(program)
293 .args(args)
294 .output()
295 .map_err(SandboxError::Io)?;
296
297 let exit_code = output.status.code().unwrap_or(-1);
298 let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
299
300 Ok(SandboxResult {
301 exit_code,
302 signal: None,
303 timed_out: false,
304 memory_peak: 0,
305 cpu_time_us: 0,
306 wall_time_ms,
307 })
308 }
309 }
310
311 pub fn run_with_stream(
313 &mut self,
314 program: &str,
315 args: &[&str],
316 ) -> Result<(SandboxResult, ProcessStream)> {
317 if self.is_running() {
318 return Err(SandboxError::AlreadyRunning);
319 }
320
321 self.start_time = Some(Instant::now());
322
323 let cgroup_name = format!("sandbox-{}", self.config.id);
324 let cgroup = Cgroup::new(&cgroup_name, Pid::from_raw(std::process::id() as i32))?;
325
326 let cgroup_config = CgroupConfig {
327 memory_limit: self.config.memory_limit,
328 cpu_quota: self.config.cpu_quota,
329 cpu_period: self.config.cpu_period,
330 max_pids: self.config.max_pids,
331 cpu_weight: None,
332 };
333 cgroup.apply_config(&cgroup_config)?;
334
335 self.cgroup = Some(cgroup);
336
337 let process_config = ProcessConfig {
338 program: program.to_string(),
339 args: args.iter().map(|s| s.to_string()).collect(),
340 env: Vec::new(),
341 cwd: None,
342 chroot_dir: None,
343 uid: None,
344 gid: None,
345 seccomp: Some(crate::isolation::seccomp::SeccompFilter::from_profile(
346 self.config.seccomp_profile.clone(),
347 )),
348 };
349
350 let (process_result, stream) = ProcessExecutor::execute_with_stream(
351 process_config,
352 self.config.namespace_config.clone(),
353 true,
354 )?;
355
356 self.pid = Some(process_result.pid);
357
358 let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
359 let (memory_peak, _) = self.get_resource_usage().unwrap_or((0, 0));
360
361 let sandbox_result = SandboxResult {
362 exit_code: process_result.exit_status,
363 signal: process_result.signal,
364 timed_out: false,
365 memory_peak,
366 cpu_time_us: process_result.exec_time_ms * 1000,
367 wall_time_ms,
368 };
369
370 let stream =
371 stream.ok_or_else(|| SandboxError::Io(std::io::Error::other("stream unavailable")))?;
372
373 Ok((sandbox_result, stream))
374 }
375
376 pub fn kill(&mut self) -> Result<()> {
377 if let Some(pid) = self.pid {
378 kill(pid, Signal::SIGKILL)
379 .map_err(|e| SandboxError::Syscall(format!("Failed to kill process: {}", e)))?;
380 self.pid = None;
381 }
382 Ok(())
383 }
384
385 pub fn get_resource_usage(&self) -> Result<(u64, u64)> {
387 if let Some(ref cgroup) = self.cgroup {
388 let memory = cgroup.get_memory_usage()?;
389 let cpu = cgroup.get_cpu_usage()?;
390 Ok((memory, cpu))
391 } else {
392 Ok((0, 0))
393 }
394 }
395}
396
397impl Drop for Sandbox {
398 fn drop(&mut self) {
399 let _ = self.kill();
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::resources::cgroup::Cgroup;
408 use crate::test_support::serial_guard;
409 use crate::utils;
410 use std::env;
411 use std::time::Duration;
412 use tempfile::tempdir;
413
414 fn config_with_temp_root(id: &str) -> (tempfile::TempDir, SandboxConfig) {
415 let tmp = tempdir().unwrap();
416 let config = SandboxConfig {
417 id: id.to_string(),
418 root: tmp.path().join("root"),
419 namespace_config: NamespaceConfig::minimal(),
420 ..Default::default()
421 };
422 (tmp, config)
423 }
424
425 struct RootOverrideGuard;
426
427 impl RootOverrideGuard {
428 fn enable() -> Self {
429 utils::set_root_override(Some(true));
430 Self
431 }
432 }
433
434 impl Drop for RootOverrideGuard {
435 fn drop(&mut self) {
436 utils::set_root_override(None);
437 }
438 }
439
440 struct EnvVarGuard {
441 key: &'static str,
442 prev: Option<String>,
443 }
444
445 impl EnvVarGuard {
446 fn new(key: &'static str, value: &str) -> Self {
447 let prev = env::var(key).ok();
448 unsafe {
449 env::set_var(key, value);
450 }
451 Self { key, prev }
452 }
453 }
454
455 impl Drop for EnvVarGuard {
456 fn drop(&mut self) {
457 if let Some(ref value) = self.prev {
458 unsafe {
459 env::set_var(self.key, value);
460 }
461 } else {
462 unsafe {
463 env::remove_var(self.key);
464 }
465 }
466 }
467 }
468
469 #[test]
470 fn test_sandbox_config_default() {
471 let config = SandboxConfig::default();
472 assert_eq!(config.id, "default");
473 assert!(config.memory_limit.is_none());
474 }
475
476 #[test]
477 fn test_sandbox_config_validate() {
478 let config = SandboxConfig {
479 id: String::new(),
480 ..Default::default()
481 };
482
483 if let Err(e) = config.validate() {
486 assert!(e.to_string().contains("ID") || e.to_string().contains("root"));
488 }
489 }
490
491 #[test]
492 fn test_sandbox_builder_new() {
493 let builder = SandboxBuilder::new("test");
494 assert_eq!(builder.config.id, "test");
495 }
496
497 #[test]
498 fn test_sandbox_builder_memory_limit() {
499 let builder = SandboxBuilder::new("test").memory_limit(100 * 1024 * 1024);
500 assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
501 }
502
503 #[test]
504 fn test_sandbox_builder_memory_limit_str() -> Result<()> {
505 let builder = SandboxBuilder::new("test").memory_limit_str("100M")?;
506 assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
507 Ok(())
508 }
509
510 #[test]
511 fn test_sandbox_builder_cpu_limit() {
512 let builder = SandboxBuilder::new("test").cpu_limit_percent(50);
513 assert!(builder.config.cpu_quota.is_some());
514 }
515
516 #[test]
517 fn test_sandbox_builder_cpu_limit_zero() {
518 let builder = SandboxBuilder::new("test").cpu_limit_percent(0);
519 assert!(builder.config.cpu_quota.is_none());
520 }
521
522 #[test]
523 fn test_sandbox_builder_cpu_limit_over_100() {
524 let builder = SandboxBuilder::new("test").cpu_limit_percent(150);
525 assert!(builder.config.cpu_quota.is_none());
526 }
527
528 #[test]
529 fn test_sandbox_builder_cpu_quota() {
530 let builder = SandboxBuilder::new("test").cpu_quota(50000, 100000);
531 assert_eq!(builder.config.cpu_quota, Some(50000));
532 assert_eq!(builder.config.cpu_period, Some(100000));
533 }
534
535 #[test]
536 fn test_sandbox_builder_max_pids() {
537 let builder = SandboxBuilder::new("test").max_pids(10);
538 assert_eq!(builder.config.max_pids, Some(10));
539 }
540
541 #[test]
542 fn test_sandbox_builder_seccomp_profile() {
543 let builder = SandboxBuilder::new("test").seccomp_profile(SeccompProfile::IoHeavy);
544 assert_eq!(builder.config.seccomp_profile, SeccompProfile::IoHeavy);
545 }
546
547 #[test]
548 fn test_sandbox_builder_root() {
549 let tmp = tempdir().unwrap();
550 let builder = SandboxBuilder::new("test").root(tmp.path());
551 assert_eq!(builder.config.root, tmp.path());
552 }
553
554 #[test]
555 fn test_sandbox_builder_timeout() {
556 let builder = SandboxBuilder::new("test").timeout(Duration::from_secs(30));
557 assert_eq!(builder.config.timeout, Some(Duration::from_secs(30)));
558 }
559
560 #[test]
561 fn test_sandbox_builder_namespaces() {
562 let ns_config = NamespaceConfig::minimal();
563 let builder = SandboxBuilder::new("test").namespaces(ns_config.clone());
564 assert_eq!(builder.config.namespace_config, ns_config);
565 }
566
567 #[test]
568 fn test_sandbox_result() {
569 let result = SandboxResult {
570 exit_code: 0,
571 signal: None,
572 timed_out: false,
573 memory_peak: 1024,
574 cpu_time_us: 5000,
575 wall_time_ms: 100,
576 };
577 assert_eq!(result.exit_code, 0);
578 assert!(!result.timed_out);
579 }
580
581 #[test]
582 fn sandbox_config_invariants_detect_empty_id() {
583 let config = SandboxConfig {
584 id: String::new(),
585 ..Default::default()
586 };
587 assert!(config.validate_invariants().is_err());
588 }
589
590 #[test]
591 fn sandbox_config_invariants_detect_disabled_namespaces() {
592 let config = SandboxConfig {
593 namespace_config: NamespaceConfig {
594 pid: false,
595 ipc: false,
596 net: false,
597 mount: false,
598 uts: false,
599 user: false,
600 },
601 ..Default::default()
602 };
603 assert!(config.validate_invariants().is_err());
604 }
605
606 #[test]
607 fn sandbox_provides_id_and_root() {
608 let (_tmp, config) = config_with_temp_root("sand-id");
609 let sandbox = Sandbox::new(config.clone()).unwrap();
610 assert_eq!(sandbox.id(), "sand-id");
611 assert!(sandbox.root().ends_with("root"));
612 assert!(!sandbox.is_running());
613 }
614
615 #[test]
616 fn sandbox_run_executes_command_without_root() {
617 let _guard = serial_guard();
618 let (_tmp, config) = config_with_temp_root("run-test");
619 let mut sandbox = Sandbox::new(config).unwrap();
620 let args: [&str; 1] = ["hello"];
621 let result = sandbox.run("/bin/echo", &args).unwrap();
622 assert_eq!(result.exit_code, 0);
623 assert!(!sandbox.is_running());
624 }
625
626 #[test]
627 fn sandbox_run_returns_error_if_already_running() {
628 let _guard = serial_guard();
629 let (_tmp, config) = config_with_temp_root("already-running");
630 let mut sandbox = Sandbox::new(config).unwrap();
631
632 sandbox.pid = Some(Pid::from_raw(1));
634
635 let args: [&str; 1] = ["test"];
636 let result = sandbox.run("/bin/echo", &args);
637
638 assert!(result.is_err());
639 assert!(result.unwrap_err().to_string().contains("already running"));
640 }
641
642 #[test]
643 fn test_sandbox_builder_build_creates_sandbox() {
644 let _guard = serial_guard();
645 let _root_guard = RootOverrideGuard::enable();
646 let tmp = tempdir().unwrap();
647 let sandbox = SandboxBuilder::new("build-test").root(tmp.path()).build();
648
649 assert!(sandbox.is_ok());
650 }
651
652 #[test]
653 fn test_sandbox_builder_build_validates_config() {
654 let _guard = serial_guard();
655 let tmp = tempdir().unwrap();
656 let result = SandboxBuilder::new("").root(tmp.path()).build();
657
658 assert!(result.is_err());
659 }
660
661 #[test]
662 fn sandbox_reports_resource_usage_from_cgroup() {
663 let (tmp, mut config) = config_with_temp_root("resource-test");
664 config.root = tmp.path().join("root");
665 let mut sandbox = Sandbox::new(config).unwrap();
666
667 let cg_path = tmp.path().join("cgroup");
668 std::fs::create_dir_all(&cg_path).unwrap();
669 std::fs::write(cg_path.join("memory.current"), "1234").unwrap();
670 std::fs::write(cg_path.join("cpu.stat"), "usage_usec 77\n").unwrap();
671
672 sandbox.cgroup = Some(Cgroup::for_testing(cg_path.clone()));
673 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
674 assert_eq!(mem, 1234);
675 assert_eq!(cpu, 77);
676 }
677
678 #[test]
679 #[ignore]
680 fn sandbox_builder_builds_when_root_override() {
681 let _guard = serial_guard();
682 let _root_guard = RootOverrideGuard::enable();
683 let tmp = tempdir().unwrap();
684 let _env_guard = EnvVarGuard::new("SANDBOX_CGROUP_ROOT", tmp.path().to_str().unwrap());
685
686 let mut sandbox = SandboxBuilder::new("integration")
687 .memory_limit(1024)
688 .cpu_limit_percent(10)
689 .max_pids(4)
690 .seccomp_profile(SeccompProfile::Minimal)
691 .root(tmp.path())
692 .timeout(Duration::from_secs(1))
693 .namespaces(NamespaceConfig::minimal())
694 .build()
695 .unwrap();
696
697 let args: [&str; 0] = [];
698 let result = sandbox.run("/bin/true", &args).unwrap();
699 assert_eq!(result.exit_code, 0);
700 }
701
702 #[test]
703 fn sandbox_kill_handles_missing_pid() {
704 let (_tmp, config) = config_with_temp_root("kill-test");
705 let mut sandbox = Sandbox::new(config).unwrap();
706 sandbox.kill().unwrap();
707 }
708
709 #[test]
710 fn sandbox_kill_terminates_real_process() {
711 let (_tmp, config) = config_with_temp_root("kill-proc");
712 let mut sandbox = Sandbox::new(config).unwrap();
713 let mut child = std::process::Command::new("sleep")
714 .arg("1")
715 .spawn()
716 .unwrap();
717 sandbox.pid = Some(Pid::from_raw(child.id() as i32));
718 sandbox.kill().unwrap();
719 let _ = child.wait();
720 }
721
722 #[test]
723 fn sandbox_get_resource_usage_without_cgroup() {
724 let (_tmp, config) = config_with_temp_root("no-cgroup");
725 let sandbox = Sandbox::new(config).unwrap();
726 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
727 assert_eq!(mem, 0);
728 assert_eq!(cpu, 0);
729 }
730
731 #[test]
732 #[ignore]
733 fn sandbox_run_with_stream_captures_output() {
734 let _guard = serial_guard();
735 let _root_guard = RootOverrideGuard::enable();
736 let (_tmp, config) = config_with_temp_root("stream-test");
737 let mut sandbox = Sandbox::new(config).unwrap();
738
739 let (result, stream) = sandbox
740 .run_with_stream("/bin/echo", &["hello world"])
741 .unwrap();
742
743 let chunks: Vec<_> = stream.into_iter().collect();
744
745 assert!(!chunks.is_empty());
746 assert_eq!(result.exit_code, 0);
747
748 let has_stdout = chunks
749 .iter()
750 .any(|chunk| matches!(chunk, crate::StreamChunk::Stdout(_)));
751 let has_exit = chunks
752 .iter()
753 .any(|chunk| matches!(chunk, crate::StreamChunk::Exit { .. }));
754
755 assert!(has_stdout, "Should have captured stdout");
756 assert!(has_exit, "Should have exit chunk");
757 }
758}