sandbox_rs/execution/
process.rs

1//! Process execution within sandbox namespace
2
3use crate::errors::{Result, SandboxError};
4use crate::isolation::namespace::NamespaceConfig;
5use crate::isolation::seccomp::SeccompFilter;
6use crate::isolation::seccomp_bpf::SeccompCompiler;
7use crate::utils;
8use log::warn;
9use nix::sched::clone;
10use nix::sys::signal::Signal;
11use nix::unistd::{Pid, chdir, chroot, execve};
12use std::ffi::CString;
13
14/// Process execution configuration
15#[derive(Debug, Clone, Default)]
16pub struct ProcessConfig {
17    /// Program to execute
18    pub program: String,
19    /// Program arguments
20    pub args: Vec<String>,
21    /// Environment variables
22    pub env: Vec<(String, String)>,
23    /// Working directory (inside sandbox)
24    pub cwd: Option<String>,
25    /// Root directory for chroot
26    pub chroot_dir: Option<String>,
27    /// UID to run as
28    pub uid: Option<u32>,
29    /// GID to run as
30    pub gid: Option<u32>,
31    /// Seccomp filter
32    pub seccomp: Option<SeccompFilter>,
33}
34
35/// Result of process execution
36#[derive(Debug, Clone)]
37pub struct ProcessResult {
38    /// Process ID
39    pub pid: Pid,
40    /// Exit status
41    pub exit_status: i32,
42    /// Signal if killed
43    pub signal: Option<i32>,
44    /// Execution time in milliseconds
45    pub exec_time_ms: u64,
46}
47
48/// Process executor
49pub struct ProcessExecutor;
50
51impl ProcessExecutor {
52    /// Execute process with namespace isolation
53    pub fn execute(
54        config: ProcessConfig,
55        namespace_config: NamespaceConfig,
56    ) -> Result<ProcessResult> {
57        let flags = namespace_config.to_clone_flags();
58
59        // Create child process with cloned namespaces
60        // Using stack for child function
61        let mut child_stack = vec![0u8; 8192]; // 8KB stack
62
63        let config_ptr = Box::into_raw(Box::new(config.clone()));
64
65        // Clone and execute
66        let result = unsafe {
67            clone(
68                Box::new(move || {
69                    let config = Box::from_raw(config_ptr);
70                    Self::child_setup(*config)
71                }),
72                &mut child_stack,
73                flags,
74                Some(Signal::SIGCHLD as i32),
75            )
76        };
77
78        match result {
79            Ok(child_pid) => {
80                let start = std::time::Instant::now();
81
82                // Wait for child
83                let status = wait_for_child(child_pid)?;
84                let exec_time_ms = start.elapsed().as_millis() as u64;
85
86                Ok(ProcessResult {
87                    pid: child_pid,
88                    exit_status: status,
89                    signal: None,
90                    exec_time_ms,
91                })
92            }
93            Err(e) => Err(SandboxError::Syscall(format!("clone failed: {}", e))),
94        }
95    }
96
97    /// Setup child process environment
98    fn child_setup(config: ProcessConfig) -> isize {
99        // Apply seccomp filter
100        if let Some(filter) = &config.seccomp {
101            if utils::is_root() {
102                if let Err(e) = SeccompCompiler::load(filter) {
103                    eprintln!("Failed to load seccomp: {}", e);
104                    return 1;
105                }
106            } else {
107                warn!("Skipping seccomp installation because process lacks root privileges");
108            }
109        }
110
111        // Change root if specified
112        if let Some(chroot_path) = &config.chroot_dir {
113            if utils::is_root() {
114                if let Err(e) = chroot(chroot_path.as_str()) {
115                    eprintln!("chroot failed: {}", e);
116                    return 1;
117                }
118            } else {
119                warn!("Skipping chroot to {} without root privileges", chroot_path);
120            }
121        }
122
123        // Change directory
124        let cwd = config.cwd.as_deref().unwrap_or("/");
125        if let Err(e) = chdir(cwd) {
126            eprintln!("chdir failed: {}", e);
127            return 1;
128        }
129
130        // Set UID/GID if specified
131        if let Some(gid) = config.gid {
132            if utils::is_root() {
133                if unsafe { libc::setgid(gid) } != 0 {
134                    eprintln!("setgid failed");
135                    return 1;
136                }
137            } else {
138                warn!("Skipping setgid without root privileges");
139            }
140        }
141
142        if let Some(uid) = config.uid {
143            if utils::is_root() {
144                if unsafe { libc::setuid(uid) } != 0 {
145                    eprintln!("setuid failed");
146                    return 1;
147                }
148            } else {
149                warn!("Skipping setuid without root privileges");
150            }
151        }
152
153        // Prepare environment
154        let env_vars: Vec<CString> = config
155            .env
156            .iter()
157            .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
158            .collect();
159
160        let env_refs: Vec<&CString> = env_vars.iter().collect();
161
162        // Execute program
163        let program_cstring = match CString::new(config.program.clone()) {
164            Ok(s) => s,
165            Err(_) => {
166                eprintln!("program name contains nul byte");
167                return 1;
168            }
169        };
170
171        let args_cstrings: Vec<CString> = config
172            .args
173            .iter()
174            .map(|s| CString::new(s.clone()).unwrap_or_else(|_| CString::new("").unwrap()))
175            .collect();
176
177        let mut args_refs: Vec<&CString> = vec![&program_cstring];
178        args_refs.extend(args_cstrings.iter());
179
180        match execve(&program_cstring, &args_refs, &env_refs) {
181            Ok(_) => 0,
182            Err(e) => {
183                eprintln!("execve failed: {}", e);
184                1
185            }
186        }
187    }
188}
189
190/// Wait for child process and get exit status
191fn wait_for_child(pid: Pid) -> Result<i32> {
192    use nix::sys::wait::{WaitStatus, waitpid};
193
194    loop {
195        match waitpid(pid, None) {
196            Ok(WaitStatus::Exited(_, status)) => return Ok(status),
197            Ok(WaitStatus::Signaled(_, signal, _)) => {
198                return Ok(128 + signal as i32);
199            }
200            Ok(_) => continue, // Continue if other status
201            Err(e) => return Err(SandboxError::Syscall(format!("waitpid failed: {}", e))),
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::test_support::serial_guard;
210    use nix::unistd::{ForkResult, fork};
211
212    #[test]
213    fn test_process_config_default() {
214        let config = ProcessConfig::default();
215        assert!(config.program.is_empty());
216        assert!(config.args.is_empty());
217        assert!(config.env.is_empty());
218        assert!(config.cwd.is_none());
219        assert!(config.uid.is_none());
220        assert!(config.gid.is_none());
221    }
222
223    #[test]
224    fn test_process_config_with_args() {
225        let config = ProcessConfig {
226            program: "echo".to_string(),
227            args: vec!["hello".to_string(), "world".to_string()],
228            ..Default::default()
229        };
230
231        assert_eq!(config.program, "echo");
232        assert_eq!(config.args.len(), 2);
233    }
234
235    #[test]
236    fn test_process_config_with_env() {
237        let config = ProcessConfig {
238            env: vec![("MY_VAR".to_string(), "my_value".to_string())],
239            ..Default::default()
240        };
241
242        assert_eq!(config.env.len(), 1);
243        assert_eq!(config.env[0].0, "MY_VAR");
244    }
245
246    #[test]
247    fn test_process_result() {
248        let result = ProcessResult {
249            pid: Pid::from_raw(123),
250            exit_status: 0,
251            signal: None,
252            exec_time_ms: 100,
253        };
254
255        assert_eq!(result.pid, Pid::from_raw(123));
256        assert_eq!(result.exit_status, 0);
257        assert!(result.signal.is_none());
258        assert_eq!(result.exec_time_ms, 100);
259    }
260
261    #[test]
262    fn test_process_result_with_signal() {
263        let result = ProcessResult {
264            pid: Pid::from_raw(456),
265            exit_status: 0,
266            signal: Some(9), // SIGKILL
267            exec_time_ms: 50,
268        };
269
270        assert!(result.signal.is_some());
271        assert_eq!(result.signal.unwrap(), 9);
272    }
273
274    #[test]
275    fn wait_for_child_returns_exit_status() {
276        let _guard = serial_guard();
277        match unsafe { fork() } {
278            Ok(ForkResult::Child) => {
279                std::process::exit(42);
280            }
281            Ok(ForkResult::Parent { child }) => {
282                let status = wait_for_child(child).unwrap();
283                assert_eq!(status, 42);
284            }
285            Err(e) => panic!("fork failed: {}", e),
286        }
287    }
288
289    #[test]
290    fn process_executor_runs_program_without_namespaces() {
291        let _guard = serial_guard();
292        let config = ProcessConfig {
293            program: "/bin/echo".to_string(),
294            args: vec!["sandbox".to_string()],
295            env: vec![("TEST_EXEC".to_string(), "1".to_string())],
296            ..Default::default()
297        };
298
299        let namespace = NamespaceConfig {
300            pid: false,
301            ipc: false,
302            net: false,
303            mount: false,
304            uts: false,
305            user: false,
306        };
307
308        let result = ProcessExecutor::execute(config, namespace).unwrap();
309        assert_eq!(result.exit_status, 0);
310    }
311}