sandbox_rs/execution/
init.rs

1//! Minimal init process for sandbox
2
3use nix::sys::signal::{SigHandler, Signal, signal};
4use nix::unistd::execv;
5use std::ffi::CString;
6use std::process::exit;
7
8/// Simple init process that manages sandbox
9pub struct SandboxInit {
10    /// Arguments to pass to user program
11    pub program: String,
12    pub args: Vec<String>,
13}
14
15impl SandboxInit {
16    /// Create new init process
17    pub fn new(program: String, args: Vec<String>) -> Self {
18        Self { program, args }
19    }
20
21    /// Run init process
22    /// This becomes PID 1 inside the sandbox
23    pub fn run(&self) -> ! {
24        // Setup signal handlers
25        Self::setup_signals();
26
27        Self::mount_procfs();
28        Self::mount_sysfs();
29
30        // Execute user program
31        self.exec_user_program();
32    }
33
34    /// Setup signal handlers for init
35    fn setup_signals() {
36        // Ignore SIGCHLD so we don't become zombie
37        unsafe {
38            let _ = signal(Signal::SIGCHLD, SigHandler::SigIgn);
39            let _ = signal(Signal::SIGTERM, SigHandler::SigDfl);
40        }
41    }
42
43    fn mount_procfs() {
44        let _ = std::fs::create_dir("/proc");
45        let _ = std::process::Command::new("mount")
46            .args(["-t", "proc", "proc", "/proc"])
47            .output();
48    }
49
50    fn mount_sysfs() {
51        let _ = std::fs::create_dir("/sys");
52        let _ = std::process::Command::new("mount")
53            .args(["-t", "sysfs", "sysfs", "/sys"])
54            .output();
55    }
56
57    /// Execute user program
58    fn exec_user_program(&self) -> ! {
59        let program_cstr = match CString::new(self.program.clone()) {
60            Ok(s) => s,
61            Err(_) => {
62                eprintln!("Invalid program name");
63                exit(1);
64            }
65        };
66
67        let args_cstr: Vec<CString> = self
68            .args
69            .iter()
70            .map(|arg| CString::new(arg.clone()).unwrap_or_else(|_| CString::new("").unwrap()))
71            .collect();
72
73        let args_refs: Vec<&CString> = vec![&program_cstr]
74            .into_iter()
75            .chain(args_cstr.iter())
76            .collect();
77
78        match execv(&program_cstr, &args_refs) {
79            Ok(_) => {
80                // execv replaces process, never returns on success
81                exit(0);
82            }
83            Err(e) => {
84                eprintln!("Failed to execute program: {}", e);
85                exit(1);
86            }
87        }
88    }
89
90    /// Reap zombie children
91    pub fn reap_children() {
92        use nix::sys::wait::{WaitStatus, waitpid};
93        use nix::unistd::Pid;
94
95        loop {
96            match waitpid(
97                Pid::from_raw(-1),
98                Some(nix::sys::wait::WaitPidFlag::WNOHANG),
99            ) {
100                Ok(WaitStatus::Exited(pid, _status)) => {
101                    eprintln!("[init] Child {} exited", pid);
102                }
103                Ok(WaitStatus::Signaled(pid, signal, _core)) => {
104                    eprintln!("[init] Child {} killed by {:?}", pid, signal);
105                }
106                Ok(WaitStatus::StillAlive) => break,
107                Ok(_) => continue,
108                Err(_) => break,
109            }
110        }
111    }
112}
113
114/// Write init process binary
115/// This is used for containerized execution
116pub fn generate_init_script(program: &str, args: &[&str]) -> String {
117    format!(
118        r#"#!/bin/sh
119set -e
120
121# Mount essential filesystems
122mkdir -p /proc /sys /dev /tmp
123
124# Don't mount if already mounted (in case of nested)
125mountpoint -q /proc || mount -t proc proc /proc
126mountpoint -q /sys || mount -t sysfs sysfs /sys
127
128# Execute program
129exec "{}" {}
130"#,
131        program,
132        args.join(" ")
133    )
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_init_creation() {
142        let init = SandboxInit::new("/bin/echo".to_string(), vec!["hello".to_string()]);
143
144        assert_eq!(init.program, "/bin/echo");
145        assert_eq!(init.args.len(), 1);
146        assert_eq!(init.args[0], "hello");
147    }
148
149    #[test]
150    fn test_init_with_multiple_args() {
151        let init = SandboxInit::new(
152            "/bin/echo".to_string(),
153            vec![
154                "hello".to_string(),
155                "world".to_string(),
156                "from".to_string(),
157                "init".to_string(),
158            ],
159        );
160
161        assert_eq!(init.args.len(), 4);
162    }
163
164    #[test]
165    fn test_generate_init_script_simple() {
166        let script = generate_init_script("/bin/echo", &["hello"]);
167
168        assert!(script.contains("#!/bin/sh"));
169        assert!(script.contains("/proc"));
170        assert!(script.contains("/bin/echo"));
171        assert!(script.contains("hello"));
172    }
173
174    #[test]
175    fn test_generate_init_script_multiple_args() {
176        let script = generate_init_script("/bin/echo", &["hello", "world"]);
177
178        assert!(script.contains("hello"));
179        assert!(script.contains("world"));
180        assert!(script.contains("exec"));
181    }
182
183    #[test]
184    fn test_generate_init_script_contains_mounts() {
185        let script = generate_init_script("/usr/bin/test", &[]);
186
187        assert!(script.contains("mount -t proc"));
188        assert!(script.contains("mount -t sysfs"));
189        assert!(script.contains("mkdir -p /proc /sys /dev /tmp"));
190    }
191
192    #[test]
193    fn test_init_empty_args() {
194        let init = SandboxInit::new("/bin/sh".to_string(), Vec::new());
195
196        assert!(init.args.is_empty());
197    }
198
199    #[test]
200    fn test_mount_helpers_are_best_effort() {
201        SandboxInit::mount_procfs();
202        SandboxInit::mount_sysfs();
203    }
204
205    #[test]
206    fn test_setup_signals_runs() {
207        // Store original handlers so we can restore them
208        let original_sigchld = unsafe { signal(Signal::SIGCHLD, SigHandler::SigDfl) };
209
210        // Test the setup
211        SandboxInit::setup_signals();
212
213        // Restore original handlers to not affect other tests
214        unsafe {
215            let _ = signal(
216                Signal::SIGCHLD,
217                original_sigchld.unwrap_or(SigHandler::SigDfl),
218            );
219        }
220    }
221
222    #[test]
223    fn test_generate_init_script_with_special_args() {
224        let script = generate_init_script("/bin/sh", &["-c", "echo hello"]);
225
226        assert!(script.contains("/bin/sh"));
227        assert!(script.contains("-c"));
228        assert!(script.contains("echo hello"));
229    }
230
231    #[test]
232    fn test_generate_init_script_complex_program() {
233        let script = generate_init_script("/usr/bin/python3", &["-u", "script.py"]);
234
235        assert!(script.contains("/usr/bin/python3"));
236        assert!(script.contains("-u"));
237        assert!(script.contains("script.py"));
238    }
239
240    #[test]
241    fn test_generate_init_script_many_args() {
242        let args = vec!["arg1", "arg2", "arg3", "arg4", "arg5"];
243        let script = generate_init_script("/bin/cat", &args);
244
245        for arg in &args {
246            assert!(script.contains(arg));
247        }
248        assert!(script.contains("/bin/cat"));
249    }
250
251    #[test]
252    fn test_generate_init_script_format() {
253        let script = generate_init_script("/bin/ls", &["-la"]);
254
255        assert!(script.starts_with("#!/bin/sh"));
256        assert!(script.contains("set -e"));
257        assert!(script.contains("mkdir -p"));
258        assert!(script.contains("exec"));
259    }
260
261    #[test]
262    fn test_init_program_stored_correctly() {
263        let program = "/usr/bin/python3".to_string();
264        let init = SandboxInit::new(program.clone(), vec![]);
265
266        assert_eq!(init.program, program);
267    }
268
269    #[test]
270    fn test_init_args_stored_correctly() {
271        let args = vec!["arg1".to_string(), "arg2".to_string(), "arg3".to_string()];
272        let init = SandboxInit::new("/bin/test".to_string(), args.clone());
273
274        assert_eq!(init.args, args);
275    }
276
277    #[test]
278    fn test_init_clone() {
279        let init1 = SandboxInit::new("/bin/echo".to_string(), vec!["test".to_string()]);
280
281        let init2 = SandboxInit::new(init1.program.clone(), init1.args.clone());
282
283        assert_eq!(init1.program, init2.program);
284        assert_eq!(init1.args, init2.args);
285    }
286
287    #[test]
288    fn test_generate_init_script_empty_program() {
289        let script = generate_init_script("", &[]);
290
291        assert!(script.starts_with("#!/bin/sh"));
292        assert!(script.contains("exec \"\""));
293    }
294
295    #[test]
296    fn test_generate_init_script_preserves_shell_syntax() {
297        let script = generate_init_script("/bin/echo", &["hello"]);
298
299        assert!(script.contains("#!/bin/sh"));
300        assert!(script.contains("set -e"));
301        assert!(script.contains("mkdir -p"));
302        assert!(script.contains("exec"));
303    }
304}