sandbox_rs/execution/
process.rs

1//! Process execution within sandbox namespace
2
3use 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/// Process execution configuration
18#[derive(Debug, Clone, Default)]
19pub struct ProcessConfig {
20    /// Program to execute
21    pub program: String,
22    /// Program arguments
23    pub args: Vec<String>,
24    /// Environment variables
25    pub env: Vec<(String, String)>,
26    /// Working directory (inside sandbox)
27    pub cwd: Option<String>,
28    /// Root directory for chroot
29    pub chroot_dir: Option<String>,
30    /// UID to run as
31    pub uid: Option<u32>,
32    /// GID to run as
33    pub gid: Option<u32>,
34    /// Seccomp filter
35    pub seccomp: Option<SeccompFilter>,
36}
37
38/// Result of process execution
39#[derive(Debug, Clone)]
40pub struct ProcessResult {
41    /// Process ID
42    pub pid: Pid,
43    /// Exit status
44    pub exit_status: i32,
45    /// Signal if killed
46    pub signal: Option<i32>,
47    /// Execution time in milliseconds
48    pub exec_time_ms: u64,
49}
50
51/// Process executor
52pub struct ProcessExecutor;
53
54impl ProcessExecutor {
55    /// Execute process with namespace isolation
56    pub fn execute(
57        config: ProcessConfig,
58        namespace_config: NamespaceConfig,
59    ) -> Result<ProcessResult> {
60        let flags = namespace_config.to_clone_flags();
61
62        // Create child process with cloned namespaces
63        // Using stack for child function
64        let mut child_stack = vec![0u8; 8192]; // 8KB stack
65
66        let config_ptr = Box::into_raw(Box::new(config.clone()));
67
68        // Clone and execute
69        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                // Wait for child
86                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    /// Execute process with streaming output
101    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    /// Setup child process environment
173    fn child_setup(config: ProcessConfig) -> isize {
174        // Apply seccomp filter
175        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        // Change root if specified
187        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        // Change directory
199        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        // Set UID/GID if specified
206        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        // Prepare environment
229        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        // Execute program
238        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    /// Setup child process with pipe redirection
265    fn child_setup_with_pipes(config: ProcessConfig, stdout_fd: i32, stderr_fd: i32) -> isize {
266        // Redirect stdout and stderr to pipes
267        // SAFETY: FDs are valid from parent and we're in a child process about to exec
268        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
285/// Wait for child process and get exit status
286fn 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, // Continue if other status
296            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), // SIGKILL
362            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}