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::{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/// Process execution configuration
21#[derive(Debug, Clone)]
22pub struct ProcessConfig {
23    /// Program to execute
24    pub program: String,
25    /// Program arguments
26    pub args: Vec<String>,
27    /// Environment variables
28    pub env: Vec<(String, String)>,
29    /// Working directory (inside sandbox)
30    pub cwd: Option<String>,
31    /// Root directory for chroot
32    pub chroot_dir: Option<String>,
33    /// UID to run as
34    pub uid: Option<u32>,
35    /// GID to run as
36    pub gid: Option<u32>,
37    /// Seccomp filter
38    pub seccomp: Option<SeccompFilter>,
39    /// Whether to inherit the parent environment (with optional overrides)
40    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    /// Ensure the environment vector reflects the inherited parent environment (plus overrides)
61    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
86/// Resolve a program name to an absolute path using PATH semantics.
87fn 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/// Result of process execution
115#[derive(Debug, Clone)]
116pub struct ProcessResult {
117    /// Process ID
118    pub pid: Pid,
119    /// Exit status
120    pub exit_status: i32,
121    /// Signal if killed
122    pub signal: Option<i32>,
123    /// Execution time in milliseconds
124    pub exec_time_ms: u64,
125}
126
127/// Process executor
128pub struct ProcessExecutor;
129
130impl ProcessExecutor {
131    /// Execute process with namespace isolation
132    pub fn execute(
133        mut config: ProcessConfig,
134        namespace_config: NamespaceConfig,
135    ) -> Result<ProcessResult> {
136        let flags = namespace_config.to_clone_flags();
137
138        // Create child process with cloned namespaces
139        // Using stack for child function
140        let mut child_stack = vec![0u8; 8192]; // 8KB stack
141
142        config.prepare_environment();
143        let config_ptr = Box::into_raw(Box::new(config.clone()));
144
145        // Clone and execute
146        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                // Wait for child
163                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    /// Execute process with streaming output
178    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                // Return immediately with the process stream
240                // The exit status is NOT known yet - it will be sent via the stream when available
241                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    /// Setup child process environment
255    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        // Apply seccomp filter
269        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        // Change root if specified
281        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        // Change directory
293        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        // Set UID/GID if specified
300        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        // Prepare environment
323        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        // Execute program
339        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    /// Setup child process with pipe redirection
365    fn child_setup_with_pipes(config: ProcessConfig, stdout_fd: i32, stderr_fd: i32) -> isize {
366        // Redirect stdout and stderr to pipes
367        // SAFETY: FDs are valid from parent and we're in a child process about to exec
368        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
385/// Wait for child process and get exit status
386fn 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, // Continue if other status
396            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), // SIGKILL
462            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}