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::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/// Process execution configuration
19#[derive(Debug, Clone, Default)]
20pub struct ProcessConfig {
21    /// Program to execute
22    pub program: String,
23    /// Program arguments
24    pub args: Vec<String>,
25    /// Environment variables
26    pub env: Vec<(String, String)>,
27    /// Working directory (inside sandbox)
28    pub cwd: Option<String>,
29    /// Root directory for chroot
30    pub chroot_dir: Option<String>,
31    /// UID to run as
32    pub uid: Option<u32>,
33    /// GID to run as
34    pub gid: Option<u32>,
35    /// Seccomp filter
36    pub seccomp: Option<SeccompFilter>,
37}
38
39/// Result of process execution
40#[derive(Debug, Clone)]
41pub struct ProcessResult {
42    /// Process ID
43    pub pid: Pid,
44    /// Exit status
45    pub exit_status: i32,
46    /// Signal if killed
47    pub signal: Option<i32>,
48    /// Execution time in milliseconds
49    pub exec_time_ms: u64,
50}
51
52/// Process executor
53pub struct ProcessExecutor;
54
55impl ProcessExecutor {
56    /// Execute process with namespace isolation
57    pub fn execute(
58        config: ProcessConfig,
59        namespace_config: NamespaceConfig,
60    ) -> Result<ProcessResult> {
61        let flags = namespace_config.to_clone_flags();
62
63        // Create child process with cloned namespaces
64        // Using stack for child function
65        let mut child_stack = vec![0u8; 8192]; // 8KB stack
66
67        let config_ptr = Box::into_raw(Box::new(config.clone()));
68
69        // Clone and execute
70        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                // Wait for child
87                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    /// Execute process with streaming output
102    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                // Return immediately with the process stream
163                // The exit status is NOT known yet - it will be sent via the stream when available
164                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    /// Setup child process environment
178    fn child_setup(config: ProcessConfig) -> isize {
179        // Apply seccomp filter
180        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        // Change root if specified
192        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        // Change directory
204        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        // Set UID/GID if specified
211        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        // Prepare environment
234        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        // Execute program
243        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    /// Setup child process with pipe redirection
270    fn child_setup_with_pipes(config: ProcessConfig, stdout_fd: i32, stderr_fd: i32) -> isize {
271        // Redirect stdout and stderr to pipes
272        // SAFETY: FDs are valid from parent and we're in a child process about to exec
273        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
290/// Wait for child process and get exit status
291fn 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, // Continue if other status
301            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), // SIGKILL
367            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}