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