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::{Pid, chdir, chroot, execve};
13use std::ffi::CString;
14use std::os::fd::IntoRawFd;
15use std::os::unix::io::AsRawFd;
16use std::thread;
17
18#[derive(Debug, Clone, Default)]
20pub struct ProcessConfig {
21 pub program: String,
23 pub args: Vec<String>,
25 pub env: Vec<(String, String)>,
27 pub cwd: Option<String>,
29 pub chroot_dir: Option<String>,
31 pub uid: Option<u32>,
33 pub gid: Option<u32>,
35 pub seccomp: Option<SeccompFilter>,
37}
38
39#[derive(Debug, Clone)]
41pub struct ProcessResult {
42 pub pid: Pid,
44 pub exit_status: i32,
46 pub signal: Option<i32>,
48 pub exec_time_ms: u64,
50}
51
52pub struct ProcessExecutor;
54
55impl ProcessExecutor {
56 pub fn execute(
58 config: ProcessConfig,
59 namespace_config: NamespaceConfig,
60 ) -> Result<ProcessResult> {
61 let flags = namespace_config.to_clone_flags();
62
63 let mut child_stack = vec![0u8; 8192]; let config_ptr = Box::into_raw(Box::new(config.clone()));
68
69 let result = unsafe {
71 clone(
72 Box::new(move || {
73 let config = Box::from_raw(config_ptr);
74 Self::child_setup(*config)
75 }),
76 &mut child_stack,
77 flags,
78 Some(Signal::SIGCHLD as i32),
79 )
80 };
81
82 match result {
83 Ok(child_pid) => {
84 let start = std::time::Instant::now();
85
86 let status = wait_for_child(child_pid)?;
88 let exec_time_ms = start.elapsed().as_millis() as u64;
89
90 Ok(ProcessResult {
91 pid: child_pid,
92 exit_status: status,
93 signal: None,
94 exec_time_ms,
95 })
96 }
97 Err(e) => Err(SandboxError::Syscall(format!("clone failed: {}", e))),
98 }
99 }
100
101 pub fn execute_with_stream(
103 config: ProcessConfig,
104 namespace_config: NamespaceConfig,
105 enable_streams: bool,
106 ) -> Result<(ProcessResult, Option<ProcessStream>)> {
107 if !enable_streams {
108 let result = Self::execute(config, namespace_config)?;
109 return Ok((result, None));
110 }
111
112 let (stdout_read, stdout_write) = nix::unistd::pipe()
113 .map_err(|e| SandboxError::Io(std::io::Error::other(format!("pipe failed: {}", e))))?;
114 let (stderr_read, stderr_write) = nix::unistd::pipe()
115 .map_err(|e| SandboxError::Io(std::io::Error::other(format!("pipe failed: {}", e))))?;
116
117 let flags = namespace_config.to_clone_flags();
118 let mut child_stack = vec![0u8; 8192];
119
120 let config_ptr = Box::into_raw(Box::new(config.clone()));
121 let stdout_write_fd = stdout_write.as_raw_fd();
122 let stderr_write_fd = stderr_write.as_raw_fd();
123
124 let result = unsafe {
125 clone(
126 Box::new(move || {
127 let config = Box::from_raw(config_ptr);
128 Self::child_setup_with_pipes(*config, stdout_write_fd, stderr_write_fd)
129 }),
130 &mut child_stack,
131 flags,
132 Some(Signal::SIGCHLD as i32),
133 )
134 };
135
136 drop(stdout_write);
137 drop(stderr_write);
138
139 match result {
140 Ok(child_pid) => {
141 let (stream_writer, process_stream) = ProcessStream::new();
142
143 let tx1 = stream_writer.tx.clone();
144 let tx2 = stream_writer.tx.clone();
145
146 spawn_fd_reader(stdout_read.into_raw_fd(), false, tx1).map_err(|e| {
147 SandboxError::Io(std::io::Error::other(format!("spawn reader failed: {}", e)))
148 })?;
149 spawn_fd_reader(stderr_read.into_raw_fd(), true, tx2).map_err(|e| {
150 SandboxError::Io(std::io::Error::other(format!("spawn reader failed: {}", e)))
151 })?;
152
153 thread::spawn(move || match wait_for_child(child_pid) {
154 Ok(status) => {
155 let _ = stream_writer.send_exit(status, None);
156 }
157 Err(_) => {
158 let _ = stream_writer.send_exit(1, None);
159 }
160 });
161
162 let process_result = ProcessResult {
165 pid: child_pid,
166 exit_status: 0,
167 signal: None,
168 exec_time_ms: 0,
169 };
170
171 Ok((process_result, Some(process_stream)))
172 }
173 Err(e) => Err(SandboxError::Syscall(format!("clone failed: {}", e))),
174 }
175 }
176
177 fn child_setup(config: ProcessConfig) -> isize {
179 if let Some(filter) = &config.seccomp {
181 if utils::is_root() {
182 if let Err(e) = SeccompBpf::load(filter) {
183 eprintln!("Failed to load seccomp: {}", e);
184 return 1;
185 }
186 } else {
187 warn!("Skipping seccomp installation because process lacks root privileges");
188 }
189 }
190
191 if let Some(chroot_path) = &config.chroot_dir {
193 if utils::is_root() {
194 if let Err(e) = chroot(chroot_path.as_str()) {
195 eprintln!("chroot failed: {}", e);
196 return 1;
197 }
198 } else {
199 warn!("Skipping chroot to {} without root privileges", chroot_path);
200 }
201 }
202
203 let cwd = config.cwd.as_deref().unwrap_or("/");
205 if let Err(e) = chdir(cwd) {
206 eprintln!("chdir failed: {}", e);
207 return 1;
208 }
209
210 if let Some(gid) = config.gid {
212 if utils::is_root() {
213 if unsafe { libc::setgid(gid) } != 0 {
214 eprintln!("setgid failed");
215 return 1;
216 }
217 } else {
218 warn!("Skipping setgid without root privileges");
219 }
220 }
221
222 if let Some(uid) = config.uid {
223 if utils::is_root() {
224 if unsafe { libc::setuid(uid) } != 0 {
225 eprintln!("setuid failed");
226 return 1;
227 }
228 } else {
229 warn!("Skipping setuid without root privileges");
230 }
231 }
232
233 let env_vars: Vec<CString> = config
235 .env
236 .iter()
237 .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
238 .collect();
239
240 let env_refs: Vec<&CString> = env_vars.iter().collect();
241
242 let program_cstring = match CString::new(config.program.clone()) {
244 Ok(s) => s,
245 Err(_) => {
246 eprintln!("program name contains nul byte");
247 return 1;
248 }
249 };
250
251 let args_cstrings: Vec<CString> = config
252 .args
253 .iter()
254 .map(|s| CString::new(s.clone()).unwrap_or_else(|_| CString::new("").unwrap()))
255 .collect();
256
257 let mut args_refs: Vec<&CString> = vec![&program_cstring];
258 args_refs.extend(args_cstrings.iter());
259
260 match execve(&program_cstring, &args_refs, &env_refs) {
261 Ok(_) => 0,
262 Err(e) => {
263 eprintln!("execve failed: {}", e);
264 1
265 }
266 }
267 }
268
269 fn child_setup_with_pipes(config: ProcessConfig, stdout_fd: i32, stderr_fd: i32) -> isize {
271 unsafe {
274 if libc::dup2(stdout_fd, 1) < 0 {
275 eprintln!("dup2 stdout failed");
276 return 1;
277 }
278 if libc::dup2(stderr_fd, 2) < 0 {
279 eprintln!("dup2 stderr failed");
280 return 1;
281 }
282 _ = libc::close(stdout_fd);
283 _ = libc::close(stderr_fd);
284 }
285
286 Self::child_setup(config)
287 }
288}
289
290fn wait_for_child(pid: Pid) -> Result<i32> {
292 use nix::sys::wait::{WaitStatus, waitpid};
293
294 loop {
295 match waitpid(pid, None) {
296 Ok(WaitStatus::Exited(_, status)) => return Ok(status),
297 Ok(WaitStatus::Signaled(_, signal, _)) => {
298 return Ok(128 + signal as i32);
299 }
300 Ok(_) => continue, Err(e) => return Err(SandboxError::Syscall(format!("waitpid failed: {}", e))),
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::test_support::serial_guard;
310 use nix::unistd::{ForkResult, fork};
311
312 #[test]
313 fn test_process_config_default() {
314 let config = ProcessConfig::default();
315 assert!(config.program.is_empty());
316 assert!(config.args.is_empty());
317 assert!(config.env.is_empty());
318 assert!(config.cwd.is_none());
319 assert!(config.uid.is_none());
320 assert!(config.gid.is_none());
321 }
322
323 #[test]
324 fn test_process_config_with_args() {
325 let config = ProcessConfig {
326 program: "echo".to_string(),
327 args: vec!["hello".to_string(), "world".to_string()],
328 ..Default::default()
329 };
330
331 assert_eq!(config.program, "echo");
332 assert_eq!(config.args.len(), 2);
333 }
334
335 #[test]
336 fn test_process_config_with_env() {
337 let config = ProcessConfig {
338 env: vec![("MY_VAR".to_string(), "my_value".to_string())],
339 ..Default::default()
340 };
341
342 assert_eq!(config.env.len(), 1);
343 assert_eq!(config.env[0].0, "MY_VAR");
344 }
345
346 #[test]
347 fn test_process_result() {
348 let result = ProcessResult {
349 pid: Pid::from_raw(123),
350 exit_status: 0,
351 signal: None,
352 exec_time_ms: 100,
353 };
354
355 assert_eq!(result.pid, Pid::from_raw(123));
356 assert_eq!(result.exit_status, 0);
357 assert!(result.signal.is_none());
358 assert_eq!(result.exec_time_ms, 100);
359 }
360
361 #[test]
362 fn test_process_result_with_signal() {
363 let result = ProcessResult {
364 pid: Pid::from_raw(456),
365 exit_status: 0,
366 signal: Some(9), exec_time_ms: 50,
368 };
369
370 assert!(result.signal.is_some());
371 assert_eq!(result.signal.unwrap(), 9);
372 }
373
374 #[test]
375 fn wait_for_child_returns_exit_status() {
376 let _guard = serial_guard();
377 match unsafe { fork() } {
378 Ok(ForkResult::Child) => {
379 std::process::exit(42);
380 }
381 Ok(ForkResult::Parent { child }) => {
382 let status = wait_for_child(child).unwrap();
383 assert_eq!(status, 42);
384 }
385 Err(e) => panic!("fork failed: {}", e),
386 }
387 }
388
389 #[test]
390 fn process_executor_runs_program_without_namespaces() {
391 let _guard = serial_guard();
392 let config = ProcessConfig {
393 program: "/bin/echo".to_string(),
394 args: vec!["sandbox".to_string()],
395 env: vec![("TEST_EXEC".to_string(), "1".to_string())],
396 ..Default::default()
397 };
398
399 let namespace = NamespaceConfig {
400 pid: false,
401 ipc: false,
402 net: false,
403 mount: false,
404 uts: false,
405 user: false,
406 };
407
408 let result = ProcessExecutor::execute(config, namespace).unwrap();
409 assert_eq!(result.exit_status, 0);
410 }
411
412 #[test]
413 fn execute_with_stream_disabled() {
414 let _guard = serial_guard();
415 let config = ProcessConfig {
416 program: "/bin/echo".to_string(),
417 args: vec!["test_output".to_string()],
418 ..Default::default()
419 };
420
421 let namespace = NamespaceConfig {
422 pid: false,
423 ipc: false,
424 net: false,
425 mount: false,
426 uts: false,
427 user: false,
428 };
429
430 let (result, stream) =
431 ProcessExecutor::execute_with_stream(config, namespace, false).unwrap();
432 assert_eq!(result.exit_status, 0);
433 assert!(stream.is_none());
434 }
435
436 #[test]
437 fn execute_with_stream_enabled() {
438 let _guard = serial_guard();
439 let config = ProcessConfig {
440 program: "/bin/echo".to_string(),
441 args: vec!["streamed_output".to_string()],
442 ..Default::default()
443 };
444
445 let namespace = NamespaceConfig {
446 pid: false,
447 ipc: false,
448 net: false,
449 mount: false,
450 uts: false,
451 user: false,
452 };
453
454 let (result, stream) =
455 ProcessExecutor::execute_with_stream(config, namespace, true).unwrap();
456 assert_eq!(result.exit_status, 0);
457 assert!(stream.is_some());
458 }
459
460 #[test]
461 fn process_config_clone() {
462 let original = ProcessConfig {
463 program: "/bin/true".to_string(),
464 args: vec!["arg1".to_string()],
465 env: vec![("VAR".to_string(), "val".to_string())],
466 cwd: Some("/tmp".to_string()),
467 chroot_dir: Some("/root".to_string()),
468 uid: Some(1000),
469 gid: Some(1000),
470 seccomp: None,
471 };
472
473 let cloned = original.clone();
474 assert_eq!(original.program, cloned.program);
475 assert_eq!(original.args, cloned.args);
476 assert_eq!(original.env, cloned.env);
477 assert_eq!(original.cwd, cloned.cwd);
478 assert_eq!(original.chroot_dir, cloned.chroot_dir);
479 assert_eq!(original.uid, cloned.uid);
480 assert_eq!(original.gid, cloned.gid);
481 }
482
483 #[test]
484 fn process_result_clone() {
485 let original = ProcessResult {
486 pid: Pid::from_raw(999),
487 exit_status: 42,
488 signal: Some(15),
489 exec_time_ms: 500,
490 };
491
492 let cloned = original.clone();
493 assert_eq!(original.pid, cloned.pid);
494 assert_eq!(original.exit_status, cloned.exit_status);
495 assert_eq!(original.signal, cloned.signal);
496 assert_eq!(original.exec_time_ms, cloned.exec_time_ms);
497 }
498
499 #[test]
500 fn process_config_with_cwd() {
501 let config = ProcessConfig {
502 program: "test".to_string(),
503 cwd: Some("/tmp".to_string()),
504 ..Default::default()
505 };
506
507 assert_eq!(config.cwd, Some("/tmp".to_string()));
508 }
509
510 #[test]
511 fn process_config_with_chroot() {
512 let config = ProcessConfig {
513 program: "test".to_string(),
514 chroot_dir: Some("/root".to_string()),
515 ..Default::default()
516 };
517
518 assert_eq!(config.chroot_dir, Some("/root".to_string()));
519 }
520
521 #[test]
522 fn process_config_with_uid_gid() {
523 let config = ProcessConfig {
524 program: "test".to_string(),
525 uid: Some(1000),
526 gid: Some(1000),
527 ..Default::default()
528 };
529
530 assert_eq!(config.uid, Some(1000));
531 assert_eq!(config.gid, Some(1000));
532 }
533
534 #[test]
535 fn wait_for_child_with_signal() {
536 let _guard = serial_guard();
537 match unsafe { fork() } {
538 Ok(ForkResult::Child) => {
539 unsafe { libc::raise(libc::SIGTERM) };
540 std::process::exit(1);
541 }
542 Ok(ForkResult::Parent { child }) => {
543 let status = wait_for_child(child).unwrap();
544 assert!(status > 0);
545 }
546 Err(e) => panic!("fork failed: {}", e),
547 }
548 }
549
550 #[test]
551 fn execute_with_stream_true_collects_chunks() {
552 let _guard = serial_guard();
553 let config = ProcessConfig {
554 program: "/bin/echo".to_string(),
555 args: vec!["hello".to_string(), "world".to_string()],
556 ..Default::default()
557 };
558
559 let namespace = NamespaceConfig {
560 pid: false,
561 ipc: false,
562 net: false,
563 mount: false,
564 uts: false,
565 user: false,
566 };
567
568 let (_result, stream_opt) =
569 ProcessExecutor::execute_with_stream(config, namespace, true).unwrap();
570
571 if let Some(stream) = stream_opt {
572 let chunk = stream.try_recv().unwrap();
573 assert!(chunk.is_none() || chunk.is_some());
574 } else {
575 panic!("Expected stream to be present");
576 }
577 }
578}