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::process::{ProcessConfig, ProcessExecutor};
14use crate::isolation::namespace::NamespaceConfig;
15use crate::isolation::seccomp::SeccompProfile;
16use crate::resources::cgroup::{Cgroup, CgroupConfig};
17use crate::utils;
18
19#[derive(Debug, Clone)]
21pub struct SandboxConfig {
22 pub root: PathBuf,
24 pub memory_limit: Option<u64>,
26 pub cpu_quota: Option<u64>,
28 pub cpu_period: Option<u64>,
30 pub max_pids: Option<u32>,
32 pub seccomp_profile: SeccompProfile,
34 pub namespace_config: NamespaceConfig,
36 pub timeout: Option<Duration>,
38 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 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
83pub struct SandboxBuilder {
85 config: SandboxConfig,
86}
87
88impl SandboxBuilder {
89 pub fn new(id: &str) -> Self {
91 Self {
92 config: SandboxConfig {
93 id: id.to_string(),
94 ..Default::default()
95 },
96 }
97 }
98
99 pub fn memory_limit(mut self, bytes: u64) -> Self {
101 self.config.memory_limit = Some(bytes);
102 self
103 }
104
105 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 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 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; let period = 100000;
125 self.cpu_quota(quota, period)
126 }
127
128 pub fn max_pids(mut self, max: u32) -> Self {
130 self.config.max_pids = Some(max);
131 self
132 }
133
134 pub fn seccomp_profile(mut self, profile: SeccompProfile) -> Self {
136 self.config.seccomp_profile = profile;
137 self
138 }
139
140 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 pub fn timeout(mut self, duration: Duration) -> Self {
148 self.config.timeout = Some(duration);
149 self
150 }
151
152 pub fn namespaces(mut self, config: NamespaceConfig) -> Self {
154 self.config.namespace_config = config;
155 self
156 }
157
158 pub fn build(self) -> Result<Sandbox> {
160 self.config.validate()?;
161 Sandbox::new(self.config)
162 }
163}
164
165#[derive(Debug, Clone)]
167pub struct SandboxResult {
168 pub exit_code: i32,
170 pub signal: Option<i32>,
172 pub timed_out: bool,
174 pub memory_peak: u64,
176 pub cpu_time_us: u64,
178 pub wall_time_ms: u64,
180}
181
182pub struct Sandbox {
184 config: SandboxConfig,
185 pid: Option<Pid>,
186 cgroup: Option<Cgroup>,
187 start_time: Option<Instant>,
188}
189
190impl Sandbox {
191 fn new(config: SandboxConfig) -> Result<Self> {
193 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 pub fn id(&self) -> &str {
211 &self.config.id
212 }
213
214 pub fn root(&self) -> &Path {
216 &self.config.root
217 }
218
219 pub fn is_running(&self) -> bool {
221 self.pid.is_some()
222 }
223
224 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 let process_config = ProcessConfig {
255 program: program.to_string(),
256 args: args.iter().map(|s| s.to_string()).collect(),
257 env: Vec::new(), 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 if utils::is_root() {
269 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 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, wall_time_ms,
287 })
288 } else {
289 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 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 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 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 if let Err(e) = config.validate() {
421 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_limit_zero() {
453 let builder = SandboxBuilder::new("test").cpu_limit_percent(0);
454 assert!(builder.config.cpu_quota.is_none());
455 }
456
457 #[test]
458 fn test_sandbox_builder_cpu_limit_over_100() {
459 let builder = SandboxBuilder::new("test").cpu_limit_percent(150);
460 assert!(builder.config.cpu_quota.is_none());
461 }
462
463 #[test]
464 fn test_sandbox_builder_cpu_quota() {
465 let builder = SandboxBuilder::new("test").cpu_quota(50000, 100000);
466 assert_eq!(builder.config.cpu_quota, Some(50000));
467 assert_eq!(builder.config.cpu_period, Some(100000));
468 }
469
470 #[test]
471 fn test_sandbox_builder_max_pids() {
472 let builder = SandboxBuilder::new("test").max_pids(10);
473 assert_eq!(builder.config.max_pids, Some(10));
474 }
475
476 #[test]
477 fn test_sandbox_builder_seccomp_profile() {
478 let builder = SandboxBuilder::new("test").seccomp_profile(SeccompProfile::IoHeavy);
479 assert_eq!(builder.config.seccomp_profile, SeccompProfile::IoHeavy);
480 }
481
482 #[test]
483 fn test_sandbox_builder_root() {
484 let tmp = tempdir().unwrap();
485 let builder = SandboxBuilder::new("test").root(tmp.path());
486 assert_eq!(builder.config.root, tmp.path());
487 }
488
489 #[test]
490 fn test_sandbox_builder_timeout() {
491 let builder = SandboxBuilder::new("test").timeout(Duration::from_secs(30));
492 assert_eq!(builder.config.timeout, Some(Duration::from_secs(30)));
493 }
494
495 #[test]
496 fn test_sandbox_builder_namespaces() {
497 let ns_config = NamespaceConfig::minimal();
498 let builder = SandboxBuilder::new("test").namespaces(ns_config.clone());
499 assert_eq!(builder.config.namespace_config, ns_config);
500 }
501
502 #[test]
503 fn test_sandbox_result() {
504 let result = SandboxResult {
505 exit_code: 0,
506 signal: None,
507 timed_out: false,
508 memory_peak: 1024,
509 cpu_time_us: 5000,
510 wall_time_ms: 100,
511 };
512 assert_eq!(result.exit_code, 0);
513 assert!(!result.timed_out);
514 }
515
516 #[test]
517 fn sandbox_config_invariants_detect_empty_id() {
518 let config = SandboxConfig {
519 id: String::new(),
520 ..Default::default()
521 };
522 assert!(config.validate_invariants().is_err());
523 }
524
525 #[test]
526 fn sandbox_config_invariants_detect_disabled_namespaces() {
527 let config = SandboxConfig {
528 namespace_config: NamespaceConfig {
529 pid: false,
530 ipc: false,
531 net: false,
532 mount: false,
533 uts: false,
534 user: false,
535 },
536 ..Default::default()
537 };
538 assert!(config.validate_invariants().is_err());
539 }
540
541 #[test]
542 fn sandbox_provides_id_and_root() {
543 let (_tmp, config) = config_with_temp_root("sand-id");
544 let sandbox = Sandbox::new(config.clone()).unwrap();
545 assert_eq!(sandbox.id(), "sand-id");
546 assert!(sandbox.root().ends_with("root"));
547 assert!(!sandbox.is_running());
548 }
549
550 #[test]
551 fn sandbox_run_executes_command_without_root() {
552 let _guard = serial_guard();
553 let (_tmp, config) = config_with_temp_root("run-test");
554 let mut sandbox = Sandbox::new(config).unwrap();
555 let args: [&str; 1] = ["hello"];
556 let result = sandbox.run("/bin/echo", &args).unwrap();
557 assert_eq!(result.exit_code, 0);
558 assert!(!sandbox.is_running());
559 }
560
561 #[test]
562 fn sandbox_run_returns_error_if_already_running() {
563 let _guard = serial_guard();
564 let (_tmp, config) = config_with_temp_root("already-running");
565 let mut sandbox = Sandbox::new(config).unwrap();
566
567 sandbox.pid = Some(Pid::from_raw(1));
569
570 let args: [&str; 1] = ["test"];
571 let result = sandbox.run("/bin/echo", &args);
572
573 assert!(result.is_err());
574 assert!(result.unwrap_err().to_string().contains("already running"));
575 }
576
577 #[test]
578 fn test_sandbox_builder_build_creates_sandbox() {
579 let _guard = serial_guard();
580 let _root_guard = RootOverrideGuard::enable();
581 let tmp = tempdir().unwrap();
582 let sandbox = SandboxBuilder::new("build-test").root(tmp.path()).build();
583
584 assert!(sandbox.is_ok());
585 }
586
587 #[test]
588 fn test_sandbox_builder_build_validates_config() {
589 let _guard = serial_guard();
590 let tmp = tempdir().unwrap();
591 let result = SandboxBuilder::new("").root(tmp.path()).build();
592
593 assert!(result.is_err());
594 }
595
596 #[test]
597 fn sandbox_reports_resource_usage_from_cgroup() {
598 let (tmp, mut config) = config_with_temp_root("resource-test");
599 config.root = tmp.path().join("root");
600 let mut sandbox = Sandbox::new(config).unwrap();
601
602 let cg_path = tmp.path().join("cgroup");
603 std::fs::create_dir_all(&cg_path).unwrap();
604 std::fs::write(cg_path.join("memory.current"), "1234").unwrap();
605 std::fs::write(cg_path.join("cpu.stat"), "usage_usec 77\n").unwrap();
606
607 sandbox.cgroup = Some(Cgroup::for_testing(cg_path.clone()));
608 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
609 assert_eq!(mem, 1234);
610 assert_eq!(cpu, 77);
611 }
612
613 #[test]
614 #[ignore]
615 fn sandbox_builder_builds_when_root_override() {
616 let _guard = serial_guard();
617 let _root_guard = RootOverrideGuard::enable();
618 let tmp = tempdir().unwrap();
619 let _env_guard = EnvVarGuard::new("SANDBOX_CGROUP_ROOT", tmp.path().to_str().unwrap());
620
621 let mut sandbox = SandboxBuilder::new("integration")
622 .memory_limit(1024)
623 .cpu_limit_percent(10)
624 .max_pids(4)
625 .seccomp_profile(SeccompProfile::Minimal)
626 .root(tmp.path())
627 .timeout(Duration::from_secs(1))
628 .namespaces(NamespaceConfig::minimal())
629 .build()
630 .unwrap();
631
632 let args: [&str; 0] = [];
633 let result = sandbox.run("/bin/true", &args).unwrap();
634 assert_eq!(result.exit_code, 0);
635 }
636
637 #[test]
638 fn sandbox_kill_handles_missing_pid() {
639 let (_tmp, config) = config_with_temp_root("kill-test");
640 let mut sandbox = Sandbox::new(config).unwrap();
641 sandbox.kill().unwrap();
642 }
643
644 #[test]
645 fn sandbox_kill_terminates_real_process() {
646 let (_tmp, config) = config_with_temp_root("kill-proc");
647 let mut sandbox = Sandbox::new(config).unwrap();
648 let mut child = std::process::Command::new("sleep")
649 .arg("1")
650 .spawn()
651 .unwrap();
652 sandbox.pid = Some(Pid::from_raw(child.id() as i32));
653 sandbox.kill().unwrap();
654 let _ = child.wait();
655 }
656
657 #[test]
658 fn sandbox_get_resource_usage_without_cgroup() {
659 let (_tmp, config) = config_with_temp_root("no-cgroup");
660 let sandbox = Sandbox::new(config).unwrap();
661 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
662 assert_eq!(mem, 0);
663 assert_eq!(cpu, 0);
664 }
665}