1use crate::errors::{Result, SandboxError};
4use crate::execution::stream::{ProcessStream, spawn_fd_reader};
5use crate::isolation::namespace::NamespaceConfig;
6use crate::isolation::seccomp::SeccompFilter;
7use crate::isolation::seccomp_bpf::SeccompBpf;
8use crate::utils;
9use log::warn;
10use nix::sched::clone;
11use nix::sys::signal::Signal;
12use nix::unistd::{AccessFlags, Pid, access, chdir, chroot, execve};
13use std::ffi::CString;
14use std::mem;
15use std::os::fd::IntoRawFd;
16use std::os::unix::io::AsRawFd;
17use std::path::Path;
18use std::thread;
19
20#[derive(Debug, Clone)]
22pub struct ProcessConfig {
23 pub program: String,
25 pub args: Vec<String>,
27 pub env: Vec<(String, String)>,
29 pub cwd: Option<String>,
31 pub chroot_dir: Option<String>,
33 pub uid: Option<u32>,
35 pub gid: Option<u32>,
37 pub seccomp: Option<SeccompFilter>,
39 pub inherit_env: bool,
41}
42
43impl Default for ProcessConfig {
44 fn default() -> Self {
45 Self {
46 program: String::new(),
47 args: Vec::new(),
48 env: Vec::new(),
49 cwd: None,
50 chroot_dir: None,
51 uid: None,
52 gid: None,
53 seccomp: None,
54 inherit_env: true,
55 }
56 }
57}
58
59impl ProcessConfig {
60 fn prepare_environment(&mut self) {
62 if !self.inherit_env {
63 return;
64 }
65
66 let overrides = mem::take(&mut self.env);
67 let mut combined: Vec<(String, String)> = std::env::vars().collect();
68
69 if overrides.is_empty() {
70 self.env = combined;
71 return;
72 }
73
74 for (key, value) in overrides {
75 if let Some((_, existing)) = combined.iter_mut().find(|(k, _)| k == &key) {
76 *existing = value;
77 } else {
78 combined.push((key, value));
79 }
80 }
81
82 self.env = combined;
83 }
84}
85
86fn resolve_program_path(
88 program: &str,
89 env: &[(String, String)],
90) -> std::result::Result<String, String> {
91 if program.contains('/') {
92 return Ok(program.to_string());
93 }
94
95 const DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
96 let path_value = env
97 .iter()
98 .find(|(key, _)| key == "PATH")
99 .map(|(_, value)| value.as_str())
100 .unwrap_or(DEFAULT_PATH);
101
102 for entry in path_value.split(':') {
103 let dir = if entry.is_empty() { "." } else { entry };
104 let candidate = Path::new(dir).join(program);
105
106 if access(&candidate, AccessFlags::X_OK).is_ok() {
107 return Ok(candidate.to_string_lossy().into_owned());
108 }
109 }
110
111 Err(format!("execve failed: command not found: {}", program))
112}
113
114#[derive(Debug, Clone)]
116pub struct ProcessResult {
117 pub pid: Pid,
119 pub exit_status: i32,
121 pub signal: Option<i32>,
123 pub exec_time_ms: u64,
125}
126
127pub struct ProcessExecutor;
129
130impl ProcessExecutor {
131 pub fn execute(
133 mut config: ProcessConfig,
134 namespace_config: NamespaceConfig,
135 ) -> Result<ProcessResult> {
136 let flags = namespace_config.to_clone_flags();
137
138 let mut child_stack = vec![0u8; 8192]; config.prepare_environment();
143 let config_ptr = Box::into_raw(Box::new(config.clone()));
144
145 let result = unsafe {
147 clone(
148 Box::new(move || {
149 let config = Box::from_raw(config_ptr);
150 Self::child_setup(*config)
151 }),
152 &mut child_stack,
153 flags,
154 Some(Signal::SIGCHLD as i32),
155 )
156 };
157
158 match result {
159 Ok(child_pid) => {
160 let start = std::time::Instant::now();
161
162 let status = wait_for_child(child_pid)?;
164 let exec_time_ms = start.elapsed().as_millis() as u64;
165
166 Ok(ProcessResult {
167 pid: child_pid,
168 exit_status: status,
169 signal: None,
170 exec_time_ms,
171 })
172 }
173 Err(e) => Err(SandboxError::Syscall(format!("clone failed: {}", e))),
174 }
175 }
176
177 pub fn execute_with_stream(
179 mut config: ProcessConfig,
180 namespace_config: NamespaceConfig,
181 enable_streams: bool,
182 ) -> Result<(ProcessResult, Option<ProcessStream>)> {
183 if !enable_streams {
184 let result = Self::execute(config, namespace_config)?;
185 return Ok((result, None));
186 }
187
188 let (stdout_read, stdout_write) = nix::unistd::pipe()
189 .map_err(|e| SandboxError::Io(std::io::Error::other(format!("pipe failed: {}", e))))?;
190 let (stderr_read, stderr_write) = nix::unistd::pipe()
191 .map_err(|e| SandboxError::Io(std::io::Error::other(format!("pipe failed: {}", e))))?;
192
193 let flags = namespace_config.to_clone_flags();
194 let mut child_stack = vec![0u8; 8192];
195
196 config.prepare_environment();
197 let config_ptr = Box::into_raw(Box::new(config.clone()));
198 let stdout_write_fd = stdout_write.as_raw_fd();
199 let stderr_write_fd = stderr_write.as_raw_fd();
200
201 let result = unsafe {
202 clone(
203 Box::new(move || {
204 let config = Box::from_raw(config_ptr);
205 Self::child_setup_with_pipes(*config, stdout_write_fd, stderr_write_fd)
206 }),
207 &mut child_stack,
208 flags,
209 Some(Signal::SIGCHLD as i32),
210 )
211 };
212
213 drop(stdout_write);
214 drop(stderr_write);
215
216 match result {
217 Ok(child_pid) => {
218 let (stream_writer, process_stream) = ProcessStream::new();
219
220 let tx1 = stream_writer.tx.clone();
221 let tx2 = stream_writer.tx.clone();
222
223 spawn_fd_reader(stdout_read.into_raw_fd(), false, tx1).map_err(|e| {
224 SandboxError::Io(std::io::Error::other(format!("spawn reader failed: {}", e)))
225 })?;
226 spawn_fd_reader(stderr_read.into_raw_fd(), true, tx2).map_err(|e| {
227 SandboxError::Io(std::io::Error::other(format!("spawn reader failed: {}", e)))
228 })?;
229
230 thread::spawn(move || match wait_for_child(child_pid) {
231 Ok(status) => {
232 let _ = stream_writer.send_exit(status, None);
233 }
234 Err(_) => {
235 let _ = stream_writer.send_exit(1, None);
236 }
237 });
238
239 let process_result = ProcessResult {
242 pid: child_pid,
243 exit_status: 0,
244 signal: None,
245 exec_time_ms: 0,
246 };
247
248 Ok((process_result, Some(process_stream)))
249 }
250 Err(e) => Err(SandboxError::Syscall(format!("clone failed: {}", e))),
251 }
252 }
253
254 fn child_setup(config: ProcessConfig) -> isize {
256 let ProcessConfig {
257 program,
258 args,
259 env,
260 cwd,
261 chroot_dir,
262 uid,
263 gid,
264 seccomp,
265 inherit_env: _,
266 } = config;
267
268 if let Some(filter) = &seccomp {
270 if utils::is_root() {
271 if let Err(e) = SeccompBpf::load(filter) {
272 eprintln!("Failed to load seccomp: {}", e);
273 return 1;
274 }
275 } else {
276 warn!("Skipping seccomp installation because process lacks root privileges");
277 }
278 }
279
280 if let Some(chroot_path) = &chroot_dir {
282 if utils::is_root() {
283 if let Err(e) = chroot(chroot_path.as_str()) {
284 eprintln!("chroot failed: {}", e);
285 return 1;
286 }
287 } else {
288 warn!("Skipping chroot to {} without root privileges", chroot_path);
289 }
290 }
291
292 let cwd = cwd.as_deref().unwrap_or("/");
294 if let Err(e) = chdir(cwd) {
295 eprintln!("chdir failed: {}", e);
296 return 1;
297 }
298
299 if let Some(gid) = gid {
301 if utils::is_root() {
302 if unsafe { libc::setgid(gid) } != 0 {
303 eprintln!("setgid failed");
304 return 1;
305 }
306 } else {
307 warn!("Skipping setgid without root privileges");
308 }
309 }
310
311 if let Some(uid) = uid {
312 if utils::is_root() {
313 if unsafe { libc::setuid(uid) } != 0 {
314 eprintln!("setuid failed");
315 return 1;
316 }
317 } else {
318 warn!("Skipping setuid without root privileges");
319 }
320 }
321
322 let env_vars: Vec<CString> = env
324 .iter()
325 .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
326 .collect();
327
328 let env_refs: Vec<&CString> = env_vars.iter().collect();
329
330 let resolved_program = match resolve_program_path(&program, &env) {
331 Ok(path) => path,
332 Err(err) => {
333 eprintln!("{}", err);
334 return 1;
335 }
336 };
337
338 let program_cstring = match CString::new(resolved_program) {
340 Ok(s) => s,
341 Err(_) => {
342 eprintln!("program name contains nul byte");
343 return 1;
344 }
345 };
346
347 let args_cstrings: Vec<CString> = args
348 .iter()
349 .map(|s| CString::new(s.clone()).unwrap_or_else(|_| CString::new("").unwrap()))
350 .collect();
351
352 let mut args_refs: Vec<&CString> = vec![&program_cstring];
353 args_refs.extend(args_cstrings.iter());
354
355 match execve(&program_cstring, &args_refs, &env_refs) {
356 Ok(_) => 0,
357 Err(e) => {
358 eprintln!("execve failed: {}", e);
359 1
360 }
361 }
362 }
363
364 fn child_setup_with_pipes(config: ProcessConfig, stdout_fd: i32, stderr_fd: i32) -> isize {
366 unsafe {
369 if libc::dup2(stdout_fd, 1) < 0 {
370 eprintln!("dup2 stdout failed");
371 return 1;
372 }
373 if libc::dup2(stderr_fd, 2) < 0 {
374 eprintln!("dup2 stderr failed");
375 return 1;
376 }
377 _ = libc::close(stdout_fd);
378 _ = libc::close(stderr_fd);
379 }
380
381 Self::child_setup(config)
382 }
383}
384
385fn wait_for_child(pid: Pid) -> Result<i32> {
387 use nix::sys::wait::{WaitStatus, waitpid};
388
389 loop {
390 match waitpid(pid, None) {
391 Ok(WaitStatus::Exited(_, status)) => return Ok(status),
392 Ok(WaitStatus::Signaled(_, signal, _)) => {
393 return Ok(128 + signal as i32);
394 }
395 Ok(_) => continue, Err(e) => return Err(SandboxError::Syscall(format!("waitpid failed: {}", e))),
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use crate::test_support::serial_guard;
405 use nix::unistd::{ForkResult, fork};
406
407 #[test]
408 fn test_process_config_default() {
409 let config = ProcessConfig::default();
410 assert!(config.program.is_empty());
411 assert!(config.args.is_empty());
412 assert!(config.env.is_empty());
413 assert!(config.cwd.is_none());
414 assert!(config.uid.is_none());
415 assert!(config.gid.is_none());
416 }
417
418 #[test]
419 fn test_process_config_with_args() {
420 let config = ProcessConfig {
421 program: "echo".to_string(),
422 args: vec!["hello".to_string(), "world".to_string()],
423 ..Default::default()
424 };
425
426 assert_eq!(config.program, "echo");
427 assert_eq!(config.args.len(), 2);
428 }
429
430 #[test]
431 fn test_process_config_with_env() {
432 let config = ProcessConfig {
433 env: vec![("MY_VAR".to_string(), "my_value".to_string())],
434 ..Default::default()
435 };
436
437 assert_eq!(config.env.len(), 1);
438 assert_eq!(config.env[0].0, "MY_VAR");
439 }
440
441 #[test]
442 fn test_process_result() {
443 let result = ProcessResult {
444 pid: Pid::from_raw(123),
445 exit_status: 0,
446 signal: None,
447 exec_time_ms: 100,
448 };
449
450 assert_eq!(result.pid, Pid::from_raw(123));
451 assert_eq!(result.exit_status, 0);
452 assert!(result.signal.is_none());
453 assert_eq!(result.exec_time_ms, 100);
454 }
455
456 #[test]
457 fn test_process_result_with_signal() {
458 let result = ProcessResult {
459 pid: Pid::from_raw(456),
460 exit_status: 0,
461 signal: Some(9), exec_time_ms: 50,
463 };
464
465 assert!(result.signal.is_some());
466 assert_eq!(result.signal.unwrap(), 9);
467 }
468
469 #[test]
470 fn wait_for_child_returns_exit_status() {
471 let _guard = serial_guard();
472 match unsafe { fork() } {
473 Ok(ForkResult::Child) => {
474 std::process::exit(42);
475 }
476 Ok(ForkResult::Parent { child }) => {
477 let status = wait_for_child(child).unwrap();
478 assert_eq!(status, 42);
479 }
480 Err(e) => panic!("fork failed: {}", e),
481 }
482 }
483
484 #[test]
485 fn process_executor_runs_program_without_namespaces() {
486 let _guard = serial_guard();
487 let config = ProcessConfig {
488 program: "/bin/echo".to_string(),
489 args: vec!["sandbox".to_string()],
490 env: vec![("TEST_EXEC".to_string(), "1".to_string())],
491 ..Default::default()
492 };
493
494 let namespace = NamespaceConfig {
495 pid: false,
496 ipc: false,
497 net: false,
498 mount: false,
499 uts: false,
500 user: false,
501 };
502
503 let result = ProcessExecutor::execute(config, namespace).unwrap();
504 assert_eq!(result.exit_status, 0);
505 }
506
507 #[test]
508 fn execute_with_stream_disabled() {
509 let _guard = serial_guard();
510 let config = ProcessConfig {
511 program: "/bin/echo".to_string(),
512 args: vec!["test_output".to_string()],
513 ..Default::default()
514 };
515
516 let namespace = NamespaceConfig {
517 pid: false,
518 ipc: false,
519 net: false,
520 mount: false,
521 uts: false,
522 user: false,
523 };
524
525 let (result, stream) =
526 ProcessExecutor::execute_with_stream(config, namespace, false).unwrap();
527 assert_eq!(result.exit_status, 0);
528 assert!(stream.is_none());
529 }
530
531 #[test]
532 fn execute_with_stream_enabled() {
533 let _guard = serial_guard();
534 let config = ProcessConfig {
535 program: "/bin/echo".to_string(),
536 args: vec!["streamed_output".to_string()],
537 ..Default::default()
538 };
539
540 let namespace = NamespaceConfig {
541 pid: false,
542 ipc: false,
543 net: false,
544 mount: false,
545 uts: false,
546 user: false,
547 };
548
549 let (result, stream) =
550 ProcessExecutor::execute_with_stream(config, namespace, true).unwrap();
551 assert_eq!(result.exit_status, 0);
552 assert!(stream.is_some());
553 }
554
555 #[test]
556 fn process_config_clone() {
557 let original = ProcessConfig {
558 program: "/bin/true".to_string(),
559 args: vec!["arg1".to_string()],
560 env: vec![("VAR".to_string(), "val".to_string())],
561 cwd: Some("/tmp".to_string()),
562 chroot_dir: Some("/root".to_string()),
563 uid: Some(1000),
564 gid: Some(1000),
565 seccomp: None,
566 inherit_env: true,
567 };
568
569 let cloned = original.clone();
570 assert_eq!(original.program, cloned.program);
571 assert_eq!(original.args, cloned.args);
572 assert_eq!(original.env, cloned.env);
573 assert_eq!(original.cwd, cloned.cwd);
574 assert_eq!(original.chroot_dir, cloned.chroot_dir);
575 assert_eq!(original.uid, cloned.uid);
576 assert_eq!(original.gid, cloned.gid);
577 }
578
579 #[test]
580 fn resolve_program_path_uses_env_path() {
581 let env = vec![("PATH".to_string(), "/bin:/usr/bin".to_string())];
582 let resolved = super::resolve_program_path("ls", &env).unwrap();
583 assert!(
584 resolved.ends_with("/ls"),
585 "expected ls in path, got {}",
586 resolved
587 );
588 }
589
590 #[test]
591 fn resolve_program_path_reports_missing_binary() {
592 let env = vec![("PATH".to_string(), "/nonexistent".to_string())];
593 let err = super::resolve_program_path("definitely_missing_cmd", &env).unwrap_err();
594 assert!(err.contains("command not found"));
595 }
596
597 #[test]
598 fn process_result_clone() {
599 let original = ProcessResult {
600 pid: Pid::from_raw(999),
601 exit_status: 42,
602 signal: Some(15),
603 exec_time_ms: 500,
604 };
605
606 let cloned = original.clone();
607 assert_eq!(original.pid, cloned.pid);
608 assert_eq!(original.exit_status, cloned.exit_status);
609 assert_eq!(original.signal, cloned.signal);
610 assert_eq!(original.exec_time_ms, cloned.exec_time_ms);
611 }
612
613 #[test]
614 fn process_config_with_cwd() {
615 let config = ProcessConfig {
616 program: "test".to_string(),
617 cwd: Some("/tmp".to_string()),
618 ..Default::default()
619 };
620
621 assert_eq!(config.cwd, Some("/tmp".to_string()));
622 }
623
624 #[test]
625 fn process_config_with_chroot() {
626 let config = ProcessConfig {
627 program: "test".to_string(),
628 chroot_dir: Some("/root".to_string()),
629 ..Default::default()
630 };
631
632 assert_eq!(config.chroot_dir, Some("/root".to_string()));
633 }
634
635 #[test]
636 fn process_config_with_uid_gid() {
637 let config = ProcessConfig {
638 program: "test".to_string(),
639 uid: Some(1000),
640 gid: Some(1000),
641 ..Default::default()
642 };
643
644 assert_eq!(config.uid, Some(1000));
645 assert_eq!(config.gid, Some(1000));
646 }
647
648 #[test]
649 fn wait_for_child_with_signal() {
650 let _guard = serial_guard();
651 match unsafe { fork() } {
652 Ok(ForkResult::Child) => {
653 unsafe { libc::raise(libc::SIGTERM) };
654 std::process::exit(1);
655 }
656 Ok(ForkResult::Parent { child }) => {
657 let status = wait_for_child(child).unwrap();
658 assert!(status > 0);
659 }
660 Err(e) => panic!("fork failed: {}", e),
661 }
662 }
663
664 #[test]
665 fn execute_with_stream_true_collects_chunks() {
666 let _guard = serial_guard();
667 let config = ProcessConfig {
668 program: "/bin/echo".to_string(),
669 args: vec!["hello".to_string(), "world".to_string()],
670 ..Default::default()
671 };
672
673 let namespace = NamespaceConfig {
674 pid: false,
675 ipc: false,
676 net: false,
677 mount: false,
678 uts: false,
679 user: false,
680 };
681
682 let (_result, stream_opt) =
683 ProcessExecutor::execute_with_stream(config, namespace, true).unwrap();
684
685 if let Some(stream) = stream_opt {
686 let chunk = stream.try_recv().unwrap();
687 assert!(chunk.is_none() || chunk.is_some());
688 } else {
689 panic!("Expected stream to be present");
690 }
691 }
692}