1use 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 sandbox_cgroup::cgroup::{Cgroup, CgroupConfig};
11use sandbox_cgroup::rlimit::RlimitConfig;
12use sandbox_core::capabilities::SystemCapabilities;
13use sandbox_core::privilege::{PrivilegeMode, ResolvedMode};
14use sandbox_core::{Result, SandboxError};
15use sandbox_namespace::NamespaceConfig;
16use sandbox_seccomp::{SeccompFilter, SeccompProfile};
17
18use crate::execution::ProcessStream;
19use crate::execution::process::{ProcessConfig, ProcessExecutor};
20
21#[derive(Debug, Clone)]
23pub struct SandboxConfig {
24 pub root: PathBuf,
26 pub memory_limit: Option<u64>,
28 pub cpu_quota: Option<u64>,
30 pub cpu_period: Option<u64>,
32 pub max_pids: Option<u32>,
34 pub seccomp_profile: SeccompProfile,
36 pub namespace_config: NamespaceConfig,
38 pub timeout: Option<Duration>,
40 pub id: String,
42 pub privilege_mode: PrivilegeMode,
44}
45
46impl Default for SandboxConfig {
47 fn default() -> Self {
48 Self {
49 root: PathBuf::from("/tmp/sandbox"),
50 memory_limit: None,
51 cpu_quota: None,
52 cpu_period: None,
53 max_pids: None,
54 seccomp_profile: SeccompProfile::Minimal,
55 namespace_config: NamespaceConfig::default(),
56 timeout: None,
57 id: "default".to_string(),
58 privilege_mode: PrivilegeMode::Auto,
59 }
60 }
61}
62
63impl SandboxConfig {
64 pub fn validate(&self) -> Result<()> {
66 self.validate_invariants()?;
67
68 let caps = SystemCapabilities::detect();
69 let mode = self.privilege_mode.resolve(&caps);
70
71 match mode {
72 ResolvedMode::Privileged => {
73 if !caps.has_root {
74 return Err(SandboxError::PermissionDenied(
75 "Privileged mode requires root privileges".to_string(),
76 ));
77 }
78 }
79 ResolvedMode::Unprivileged => {
80 if !caps.has_seccomp {
81 return Err(SandboxError::FeatureNotAvailable(
82 "Seccomp is required for unprivileged sandboxing".to_string(),
83 ));
84 }
85 }
86 }
87
88 Ok(())
89 }
90
91 fn validate_invariants(&self) -> Result<()> {
92 if self.id.is_empty() {
93 return Err(SandboxError::InvalidConfig(
94 "Sandbox ID cannot be empty".to_string(),
95 ));
96 }
97
98 if self.namespace_config.enabled_count() == 0 {
99 return Err(SandboxError::InvalidConfig(
100 "At least one namespace must be enabled".to_string(),
101 ));
102 }
103
104 Ok(())
105 }
106}
107
108pub struct SandboxBuilder {
110 config: SandboxConfig,
111}
112
113impl SandboxBuilder {
114 pub fn new(id: &str) -> Self {
116 Self {
117 config: SandboxConfig {
118 id: id.to_string(),
119 ..Default::default()
120 },
121 }
122 }
123
124 pub fn memory_limit(mut self, bytes: u64) -> Self {
126 self.config.memory_limit = Some(bytes);
127 self
128 }
129
130 pub fn memory_limit_str(self, s: &str) -> Result<Self> {
132 let bytes = sandbox_core::util::parse_memory_size(s)?;
133 Ok(self.memory_limit(bytes))
134 }
135
136 pub fn cpu_quota(mut self, quota: u64, period: u64) -> Self {
138 self.config.cpu_quota = Some(quota);
139 self.config.cpu_period = Some(period);
140 self
141 }
142
143 pub fn cpu_limit_percent(self, percent: u32) -> Self {
145 if percent == 0 || percent > 100 {
146 return self;
147 }
148 let quota = (percent as u64) * 1000;
149 let period = 100000;
150 self.cpu_quota(quota, period)
151 }
152
153 pub fn max_pids(mut self, max: u32) -> Self {
155 self.config.max_pids = Some(max);
156 self
157 }
158
159 pub fn seccomp_profile(mut self, profile: SeccompProfile) -> Self {
161 self.config.seccomp_profile = profile;
162 self
163 }
164
165 pub fn root(mut self, path: impl AsRef<Path>) -> Self {
167 self.config.root = path.as_ref().to_path_buf();
168 self
169 }
170
171 pub fn timeout(mut self, duration: Duration) -> Self {
173 self.config.timeout = Some(duration);
174 self
175 }
176
177 pub fn namespaces(mut self, config: NamespaceConfig) -> Self {
179 self.config.namespace_config = config;
180 self
181 }
182
183 pub fn privilege_mode(mut self, mode: PrivilegeMode) -> Self {
185 self.config.privilege_mode = mode;
186 self
187 }
188
189 pub fn build(self) -> Result<Sandbox> {
191 self.config.validate()?;
192 Sandbox::new(self.config)
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct SandboxResult {
199 pub exit_code: i32,
201 pub signal: Option<i32>,
203 pub timed_out: bool,
205 pub memory_peak: u64,
208 pub cpu_time_us: u64,
211 pub wall_time_ms: u64,
213}
214
215impl SandboxResult {
216 pub fn killed_by_seccomp(&self) -> bool {
218 self.exit_code == 159
219 }
220
221 pub fn seccomp_error(&self) -> Option<&'static str> {
223 if self.killed_by_seccomp() {
224 Some("The action requires more permissions than were granted.")
225 } else {
226 None
227 }
228 }
229
230 pub fn check_seccomp_error(&self) -> Result<&SandboxResult> {
232 if self.killed_by_seccomp() {
233 Err(SandboxError::PermissionDenied(
234 "The seccomp profile is too restrictive for this operation. \
235 Try using a less restrictive profile (e.g., SeccompProfile::Compute or SeccompProfile::Unrestricted)"
236 .to_string(),
237 ))
238 } else {
239 Ok(self)
240 }
241 }
242}
243
244pub struct Sandbox {
246 config: SandboxConfig,
247 resolved_mode: ResolvedMode,
248 pid: Option<Pid>,
249 cgroup: Option<Cgroup>,
250 start_time: Option<Instant>,
251}
252
253impl Sandbox {
254 fn new(config: SandboxConfig) -> Result<Self> {
256 let caps = SystemCapabilities::detect();
257 let resolved_mode = config.privilege_mode.resolve(&caps);
258
259 let mut config = config;
261 if resolved_mode.is_unprivileged() && !config.namespace_config.user {
262 config.namespace_config.user = true;
264 }
265
266 fs::create_dir_all(&config.root).map_err(|e| {
268 SandboxError::Io(std::io::Error::other(format!(
269 "Failed to create root directory: {}",
270 e
271 )))
272 })?;
273
274 Ok(Self {
275 config,
276 resolved_mode,
277 pid: None,
278 cgroup: None,
279 start_time: None,
280 })
281 }
282
283 pub fn id(&self) -> &str {
285 &self.config.id
286 }
287
288 pub fn root(&self) -> &Path {
290 &self.config.root
291 }
292
293 pub fn is_running(&self) -> bool {
295 self.pid.is_some()
296 }
297
298 pub fn privilege_mode(&self) -> ResolvedMode {
300 self.resolved_mode
301 }
302
303 fn build_process_config(&self) -> ProcessConfig {
305 ProcessConfig {
306 program: String::new(), args: Vec::new(), env: Vec::new(),
309 cwd: None,
310 chroot_dir: None,
311 uid: None,
312 gid: None,
313 seccomp: Some(SeccompFilter::from_profile(
314 self.config.seccomp_profile.clone(),
315 )),
316 rlimits: if self.resolved_mode.is_unprivileged() {
317 Some(self.build_rlimit_config())
318 } else {
319 None
320 },
321 inherit_env: true,
322 use_user_namespace: self.config.namespace_config.user,
323 }
324 }
325
326 fn build_rlimit_config(&self) -> RlimitConfig {
332 RlimitConfig {
333 max_memory: self.config.memory_limit,
334 max_cpu_seconds: self.config.timeout.map(|t| t.as_secs()),
335 max_processes: self.config.max_pids.map(|p| p as u64),
336 ..Default::default()
337 }
338 }
339
340 fn setup_cgroup(&mut self) -> Result<()> {
342 if self.resolved_mode.is_unprivileged() {
343 return Ok(());
344 }
345
346 let cgroup_name = format!("sandbox-{}", self.config.id);
347 let cgroup = Cgroup::new(&cgroup_name, Pid::from_raw(std::process::id() as i32))?;
348
349 let cgroup_config = CgroupConfig {
350 memory_limit: self.config.memory_limit,
351 cpu_quota: self.config.cpu_quota,
352 cpu_period: self.config.cpu_period,
353 max_pids: self.config.max_pids,
354 cpu_weight: None,
355 };
356 cgroup.apply_config(&cgroup_config)?;
357 self.cgroup = Some(cgroup);
358 Ok(())
359 }
360
361 pub fn run(&mut self, program: &str, args: &[&str]) -> Result<SandboxResult> {
363 if self.is_running() {
364 return Err(SandboxError::AlreadyRunning);
365 }
366
367 self.start_time = Some(Instant::now());
368 self.setup_cgroup()?;
369
370 let mut process_config = self.build_process_config();
371 process_config.program = program.to_string();
372 process_config.args = args.iter().map(|s| s.to_string()).collect();
373
374 let process_result =
375 ProcessExecutor::execute(process_config, self.config.namespace_config.clone())?;
376
377 self.pid = Some(process_result.pid);
378
379 let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
380 let (memory_peak, cpu_time_us) = self.get_resource_usage().unwrap_or((0, 0));
381
382 Ok(SandboxResult {
383 exit_code: process_result.exit_status,
384 signal: process_result.signal,
385 timed_out: false,
386 memory_peak,
387 cpu_time_us,
388 wall_time_ms,
389 })
390 }
391
392 pub fn run_with_stream(
395 &mut self,
396 program: &str,
397 args: &[&str],
398 ) -> Result<(SandboxResult, ProcessStream)> {
399 if self.is_running() {
400 return Err(SandboxError::AlreadyRunning);
401 }
402
403 self.start_time = Some(Instant::now());
404 self.setup_cgroup()?;
405
406 let mut process_config = self.build_process_config();
407 process_config.program = program.to_string();
408 process_config.args = args.iter().map(|s| s.to_string()).collect();
409
410 let (process_result, stream) = ProcessExecutor::execute_with_stream(
411 process_config,
412 self.config.namespace_config.clone(),
413 true,
414 )?;
415
416 self.pid = Some(process_result.pid);
417
418 let wall_time_ms = self.start_time.unwrap().elapsed().as_millis() as u64;
419 let (memory_peak, cpu_time_us) = self.get_resource_usage().unwrap_or((0, 0));
420
421 let sandbox_result = SandboxResult {
422 exit_code: process_result.exit_status,
423 signal: process_result.signal,
424 timed_out: false,
425 memory_peak,
426 cpu_time_us,
427 wall_time_ms,
428 };
429
430 let stream =
431 stream.ok_or_else(|| SandboxError::Io(std::io::Error::other("stream unavailable")))?;
432
433 Ok((sandbox_result, stream))
434 }
435
436 pub fn kill(&mut self) -> Result<()> {
437 if let Some(pid) = self.pid {
438 kill(pid, Signal::SIGKILL)
439 .map_err(|e| SandboxError::Syscall(format!("Failed to kill process: {}", e)))?;
440 self.pid = None;
441 }
442 Ok(())
443 }
444
445 pub fn get_resource_usage(&self) -> Result<(u64, u64)> {
447 if let Some(ref cgroup) = self.cgroup {
448 let memory = cgroup.get_memory_usage()?;
449 let cpu = cgroup.get_cpu_usage()?;
450 Ok((memory, cpu))
451 } else {
452 Ok((0, 0))
454 }
455 }
456}
457
458impl Drop for Sandbox {
459 fn drop(&mut self) {
460 let _ = self.kill();
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use tempfile::tempdir;
468
469 fn config_with_temp_root(id: &str) -> (tempfile::TempDir, SandboxConfig) {
470 let tmp = tempdir().unwrap();
471 let config = SandboxConfig {
472 id: id.to_string(),
473 root: tmp.path().join("root"),
474 namespace_config: NamespaceConfig::minimal(),
475 ..Default::default()
476 };
477 (tmp, config)
478 }
479
480 #[test]
481 fn test_sandbox_config_default() {
482 let config = SandboxConfig::default();
483 assert_eq!(config.id, "default");
484 assert!(config.memory_limit.is_none());
485 assert_eq!(config.privilege_mode, PrivilegeMode::Auto);
486 }
487
488 #[test]
489 fn test_sandbox_config_validate_empty_id() {
490 let config = SandboxConfig {
491 id: String::new(),
492 ..Default::default()
493 };
494 assert!(config.validate_invariants().is_err());
495 }
496
497 #[test]
498 fn test_sandbox_config_validate_no_namespaces() {
499 let config = SandboxConfig {
500 namespace_config: NamespaceConfig {
501 pid: false,
502 ipc: false,
503 net: false,
504 mount: false,
505 uts: false,
506 user: false,
507 },
508 ..Default::default()
509 };
510 assert!(config.validate_invariants().is_err());
511 }
512
513 #[test]
514 fn test_sandbox_builder_new() {
515 let builder = SandboxBuilder::new("test");
516 assert_eq!(builder.config.id, "test");
517 }
518
519 #[test]
520 fn test_sandbox_builder_memory_limit() {
521 let builder = SandboxBuilder::new("test").memory_limit(100 * 1024 * 1024);
522 assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
523 }
524
525 #[test]
526 fn test_sandbox_builder_memory_limit_str() -> Result<()> {
527 let builder = SandboxBuilder::new("test").memory_limit_str("100M")?;
528 assert_eq!(builder.config.memory_limit, Some(100 * 1024 * 1024));
529 Ok(())
530 }
531
532 #[test]
533 fn test_sandbox_builder_cpu_limit() {
534 let builder = SandboxBuilder::new("test").cpu_limit_percent(50);
535 assert!(builder.config.cpu_quota.is_some());
536 }
537
538 #[test]
539 fn test_sandbox_builder_cpu_limit_zero() {
540 let builder = SandboxBuilder::new("test").cpu_limit_percent(0);
541 assert!(builder.config.cpu_quota.is_none());
542 }
543
544 #[test]
545 fn test_sandbox_builder_cpu_limit_over_100() {
546 let builder = SandboxBuilder::new("test").cpu_limit_percent(150);
547 assert!(builder.config.cpu_quota.is_none());
548 }
549
550 #[test]
551 fn test_sandbox_builder_privilege_mode() {
552 let builder = SandboxBuilder::new("test").privilege_mode(PrivilegeMode::Unprivileged);
553 assert_eq!(builder.config.privilege_mode, PrivilegeMode::Unprivileged);
554 }
555
556 #[test]
557 fn test_sandbox_builder_build_creates_sandbox() {
558 let tmp = tempdir().unwrap();
559 let sandbox = SandboxBuilder::new("build-test").root(tmp.path()).build();
560 assert!(sandbox.is_ok());
562 }
563
564 #[test]
565 fn test_sandbox_builder_build_validates_config() {
566 let tmp = tempdir().unwrap();
567 let result = SandboxBuilder::new("").root(tmp.path()).build();
568 assert!(result.is_err());
569 }
570
571 #[test]
572 fn sandbox_provides_id_and_root() {
573 let (_tmp, config) = config_with_temp_root("sand-id");
574 let sandbox = Sandbox::new(config).unwrap();
575 assert_eq!(sandbox.id(), "sand-id");
576 assert!(sandbox.root().ends_with("root"));
577 assert!(!sandbox.is_running());
578 }
579
580 #[test]
581 fn sandbox_run_returns_error_if_already_running() {
582 let (_tmp, config) = config_with_temp_root("already-running");
583 let mut sandbox = Sandbox::new(config).unwrap();
584 sandbox.pid = Some(Pid::from_raw(1));
585
586 let args: [&str; 1] = ["test"];
587 let result = sandbox.run("/bin/echo", &args);
588 assert!(result.is_err());
589 assert!(result.unwrap_err().to_string().contains("already running"));
590 }
591
592 #[test]
593 fn sandbox_kill_handles_missing_pid() {
594 let (_tmp, config) = config_with_temp_root("kill-test");
595 let mut sandbox = Sandbox::new(config).unwrap();
596 sandbox.kill().unwrap();
597 }
598
599 #[test]
600 fn sandbox_kill_terminates_real_process() {
601 let (_tmp, config) = config_with_temp_root("kill-proc");
602 let mut sandbox = Sandbox::new(config).unwrap();
603 let mut child = std::process::Command::new("sleep")
604 .arg("1")
605 .spawn()
606 .unwrap();
607 sandbox.pid = Some(Pid::from_raw(child.id() as i32));
608 sandbox.kill().unwrap();
609 let _ = child.wait();
610 }
611
612 #[test]
613 fn sandbox_get_resource_usage_without_cgroup_returns_zeros() {
614 let (_tmp, config) = config_with_temp_root("no-cgroup");
615 let sandbox = Sandbox::new(config).unwrap();
616 let result = sandbox.get_resource_usage();
618 assert!(result.is_ok());
619 assert_eq!(result.unwrap(), (0, 0));
620 }
621
622 #[test]
623 fn sandbox_reports_resource_usage_from_cgroup() {
624 let (tmp, mut config) = config_with_temp_root("resource-test");
625 config.root = tmp.path().join("root");
626 let mut sandbox = Sandbox::new(config).unwrap();
627
628 let cg_path = tmp.path().join("cgroup");
629 std::fs::create_dir_all(&cg_path).unwrap();
630 std::fs::write(cg_path.join("memory.current"), "1234").unwrap();
631 std::fs::write(cg_path.join("cpu.stat"), "usage_usec 77\n").unwrap();
632
633 sandbox.cgroup = Some(Cgroup::for_testing(cg_path));
634 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
635 assert_eq!(mem, 1234);
636 assert_eq!(cpu, 77);
637 }
638
639 #[test]
640 fn test_sandbox_result_killed_by_seccomp() {
641 let result = SandboxResult {
642 exit_code: 159,
643 signal: None,
644 timed_out: false,
645 memory_peak: 0,
646 cpu_time_us: 0,
647 wall_time_ms: 0,
648 };
649 assert!(result.killed_by_seccomp());
650 }
651
652 #[test]
653 fn test_sandbox_result_not_killed_by_seccomp() {
654 let result = SandboxResult {
655 exit_code: 0,
656 signal: None,
657 timed_out: false,
658 memory_peak: 0,
659 cpu_time_us: 0,
660 wall_time_ms: 0,
661 };
662 assert!(!result.killed_by_seccomp());
663 }
664
665 #[test]
666 fn test_sandbox_result_check_seccomp_error() {
667 let result = SandboxResult {
668 exit_code: 159,
669 signal: None,
670 timed_out: false,
671 memory_peak: 0,
672 cpu_time_us: 0,
673 wall_time_ms: 0,
674 };
675 assert!(result.check_seccomp_error().is_err());
676
677 let ok_result = SandboxResult {
678 exit_code: 0,
679 signal: None,
680 timed_out: false,
681 memory_peak: 0,
682 cpu_time_us: 0,
683 wall_time_ms: 0,
684 };
685 assert!(ok_result.check_seccomp_error().is_ok());
686 }
687}