1use 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#[derive(Debug, Clone, Default)]
16pub struct ProcessConfig {
17 pub program: String,
19 pub args: Vec<String>,
21 pub env: Vec<(String, String)>,
23 pub cwd: Option<String>,
25 pub chroot_dir: Option<String>,
27 pub uid: Option<u32>,
29 pub gid: Option<u32>,
31 pub seccomp: Option<SeccompFilter>,
33}
34
35#[derive(Debug, Clone)]
37pub struct ProcessResult {
38 pub pid: Pid,
40 pub exit_status: i32,
42 pub signal: Option<i32>,
44 pub exec_time_ms: u64,
46}
47
48pub struct ProcessExecutor;
50
51impl ProcessExecutor {
52 pub fn execute(
54 config: ProcessConfig,
55 namespace_config: NamespaceConfig,
56 ) -> Result<ProcessResult> {
57 let flags = namespace_config.to_clone_flags();
58
59 let mut child_stack = vec![0u8; 8192]; let config_ptr = Box::into_raw(Box::new(config.clone()));
64
65 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 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 fn child_setup(config: ProcessConfig) -> isize {
99 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 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 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 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 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 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
190fn 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, 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), 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}