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_quota() {
453 let builder = SandboxBuilder::new("test").cpu_quota(50000, 100000);
454 assert_eq!(builder.config.cpu_quota, Some(50000));
455 assert_eq!(builder.config.cpu_period, Some(100000));
456 }
457
458 #[test]
459 fn test_sandbox_result() {
460 let result = SandboxResult {
461 exit_code: 0,
462 signal: None,
463 timed_out: false,
464 memory_peak: 1024,
465 cpu_time_us: 5000,
466 wall_time_ms: 100,
467 };
468 assert_eq!(result.exit_code, 0);
469 assert!(!result.timed_out);
470 }
471
472 #[test]
473 fn sandbox_config_invariants_detect_empty_id() {
474 let config = SandboxConfig {
475 id: String::new(),
476 ..Default::default()
477 };
478 assert!(config.validate_invariants().is_err());
479 }
480
481 #[test]
482 fn sandbox_config_invariants_detect_disabled_namespaces() {
483 let config = SandboxConfig {
484 namespace_config: NamespaceConfig {
485 pid: false,
486 ipc: false,
487 net: false,
488 mount: false,
489 uts: false,
490 user: false,
491 },
492 ..Default::default()
493 };
494 assert!(config.validate_invariants().is_err());
495 }
496
497 #[test]
498 fn sandbox_provides_id_and_root() {
499 let (_tmp, config) = config_with_temp_root("sand-id");
500 let sandbox = Sandbox::new(config.clone()).unwrap();
501 assert_eq!(sandbox.id(), "sand-id");
502 assert!(sandbox.root().ends_with("root"));
503 assert!(!sandbox.is_running());
504 }
505
506 #[test]
507 fn sandbox_run_executes_command_without_root() {
508 let _guard = serial_guard();
509 let (_tmp, config) = config_with_temp_root("run-test");
510 let mut sandbox = Sandbox::new(config).unwrap();
511 let args: [&str; 1] = ["hello"];
512 let result = sandbox.run("/bin/echo", &args).unwrap();
513 assert_eq!(result.exit_code, 0);
514 assert!(!sandbox.is_running());
515 }
516
517 #[test]
518 fn sandbox_reports_resource_usage_from_cgroup() {
519 let (tmp, mut config) = config_with_temp_root("resource-test");
520 config.root = tmp.path().join("root");
521 let mut sandbox = Sandbox::new(config).unwrap();
522
523 let cg_path = tmp.path().join("cgroup");
524 std::fs::create_dir_all(&cg_path).unwrap();
525 std::fs::write(cg_path.join("memory.current"), "1234").unwrap();
526 std::fs::write(cg_path.join("cpu.stat"), "usage_usec 77\n").unwrap();
527
528 sandbox.cgroup = Some(Cgroup::for_testing(cg_path.clone()));
529 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
530 assert_eq!(mem, 1234);
531 assert_eq!(cpu, 77);
532 }
533
534 #[test]
535 #[ignore]
536 fn sandbox_builder_builds_when_root_override() {
537 let _guard = serial_guard();
538 let _root_guard = RootOverrideGuard::enable();
539 let tmp = tempdir().unwrap();
540 let _env_guard = EnvVarGuard::new("SANDBOX_CGROUP_ROOT", tmp.path().to_str().unwrap());
541
542 let mut sandbox = SandboxBuilder::new("integration")
543 .memory_limit(1024)
544 .cpu_limit_percent(10)
545 .max_pids(4)
546 .seccomp_profile(SeccompProfile::Minimal)
547 .root(tmp.path())
548 .timeout(Duration::from_secs(1))
549 .namespaces(NamespaceConfig::minimal())
550 .build()
551 .unwrap();
552
553 let args: [&str; 0] = [];
554 let result = sandbox.run("/bin/true", &args).unwrap();
555 assert_eq!(result.exit_code, 0);
556 }
557
558 #[test]
559 fn sandbox_kill_handles_missing_pid() {
560 let (_tmp, config) = config_with_temp_root("kill-test");
561 let mut sandbox = Sandbox::new(config).unwrap();
562 sandbox.kill().unwrap();
563 }
564
565 #[test]
566 fn sandbox_kill_terminates_real_process() {
567 let (_tmp, config) = config_with_temp_root("kill-proc");
568 let mut sandbox = Sandbox::new(config).unwrap();
569 let mut child = std::process::Command::new("sleep")
570 .arg("1")
571 .spawn()
572 .unwrap();
573 sandbox.pid = Some(Pid::from_raw(child.id() as i32));
574 sandbox.kill().unwrap();
575 let _ = child.wait();
576 }
577
578 #[test]
579 fn sandbox_get_resource_usage_without_cgroup() {
580 let (_tmp, config) = config_with_temp_root("no-cgroup");
581 let sandbox = Sandbox::new(config).unwrap();
582 let (mem, cpu) = sandbox.get_resource_usage().unwrap();
583 assert_eq!(mem, 0);
584 assert_eq!(cpu, 0);
585 }
586}