Skip to main content

evalbox_sandbox/
workspace.rs

1//! Workspace and pipe management for sandboxed execution.
2//!
3//! The workspace is a temporary directory containing the sandbox's writable areas
4//! and all the pipes for parent-child communication.
5//!
6//! ## Pipes
7//!
8//! - **stdin**: Parent writes → Child reads
9//! - **stdout**: Child writes → Parent reads
10//! - **stderr**: Child writes → Parent reads
11//! - **sync**: Eventfd for parent-child synchronization
12//!
13//! ## Important: Pipe Hygiene
14//!
15//! After `fork()`, each side must close unused pipe ends:
16//! - Parent closes: stdin.read, stdout.write, stderr.write
17//! - Child closes: stdin.write, stdout.read, stderr.read
18//!
19//! This is required for `poll()` to work correctly - EOF is only signaled
20//! when ALL write ends are closed.
21
22use std::fs;
23use std::io;
24use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
25use std::path::{Path, PathBuf};
26
27use tempfile::TempDir;
28
29/// Unidirectional pipe.
30#[derive(Debug)]
31pub struct Pipe {
32    pub read: OwnedFd,
33    pub write: OwnedFd,
34}
35
36impl Pipe {
37    pub fn new() -> io::Result<Self> {
38        let mut fds = [0i32; 2];
39        // SAFETY: pipe2 writes to valid array.
40        if unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
41            return Err(io::Error::last_os_error());
42        }
43        // SAFETY: On success, fds are valid file descriptors.
44        Ok(Self {
45            read: unsafe { OwnedFd::from_raw_fd(fds[0]) },
46            write: unsafe { OwnedFd::from_raw_fd(fds[1]) },
47        })
48    }
49
50    #[inline]
51    pub fn read_fd(&self) -> RawFd {
52        self.read.as_raw_fd()
53    }
54
55    #[inline]
56    pub fn write_fd(&self) -> RawFd {
57        self.write.as_raw_fd()
58    }
59}
60
61/// Eventfd-based parent-child synchronization.
62///
63/// Used when `NotifyMode::Disabled` — the child signals readiness via eventfd
64/// after completing setup, and the parent writes back to let it proceed to exec.
65#[derive(Debug)]
66pub struct SyncPair {
67    pub child_ready: OwnedFd,
68    pub parent_done: OwnedFd,
69}
70
71impl SyncPair {
72    pub fn new() -> io::Result<Self> {
73        // SAFETY: eventfd returns a valid fd on success, -1 on error.
74        let child_ready = unsafe { libc::eventfd(0, 0) };
75        if child_ready < 0 {
76            return Err(io::Error::last_os_error());
77        }
78        let parent_done = unsafe { libc::eventfd(0, 0) };
79        if parent_done < 0 {
80            unsafe { libc::close(child_ready) };
81            return Err(io::Error::last_os_error());
82        }
83        Ok(Self {
84            child_ready: unsafe { OwnedFd::from_raw_fd(child_ready) },
85            parent_done: unsafe { OwnedFd::from_raw_fd(parent_done) },
86        })
87    }
88
89    #[inline]
90    pub fn child_ready_fd(&self) -> RawFd {
91        self.child_ready.as_raw_fd()
92    }
93
94    #[inline]
95    pub fn parent_done_fd(&self) -> RawFd {
96        self.parent_done.as_raw_fd()
97    }
98}
99
100/// All pipes for sandbox I/O.
101#[derive(Debug)]
102pub struct Pipes {
103    pub stdin: Pipe,
104    pub stdout: Pipe,
105    pub stderr: Pipe,
106    pub sync: SyncPair,
107}
108
109impl Pipes {
110    pub fn new() -> io::Result<Self> {
111        Ok(Self {
112            stdin: Pipe::new()?,
113            stdout: Pipe::new()?,
114            stderr: Pipe::new()?,
115            sync: SyncPair::new()?,
116        })
117    }
118}
119
120/// Temporary workspace for sandbox execution.
121#[derive(Debug)]
122pub struct Workspace {
123    root: PathBuf,
124    pub pipes: Pipes,
125    _tempdir: TempDir,
126}
127
128impl Workspace {
129    pub fn new() -> io::Result<Self> {
130        Self::with_prefix("evalbox-")
131    }
132
133    pub fn with_prefix(prefix: &str) -> io::Result<Self> {
134        let tempdir = TempDir::with_prefix(prefix)?;
135        Ok(Self {
136            root: tempdir.path().to_path_buf(),
137            pipes: Pipes::new()?,
138            _tempdir: tempdir,
139        })
140    }
141
142    #[inline]
143    pub fn root(&self) -> &Path {
144        &self.root
145    }
146
147    pub fn write_file(&self, path: &str, content: &[u8], executable: bool) -> io::Result<PathBuf> {
148        use std::os::unix::fs::PermissionsExt;
149
150        let full = self.root.join(path);
151        if let Some(parent) = full.parent() {
152            fs::create_dir_all(parent)?;
153        }
154        fs::write(&full, content)?;
155
156        if executable {
157            fs::set_permissions(&full, fs::Permissions::from_mode(0o755))?;
158        }
159
160        Ok(full)
161    }
162
163    pub fn create_dir(&self, path: &str) -> io::Result<PathBuf> {
164        let full = self.root.join(path);
165        fs::create_dir_all(&full)?;
166        Ok(full)
167    }
168
169    /// Create standard sandbox directories.
170    ///
171    /// Only creates the writable workspace directories (work, tmp, home).
172    /// No rootfs directories (proc, dev, etc.) needed since we don't use `pivot_root`.
173    pub fn setup_sandbox_dirs(&self) -> io::Result<()> {
174        for dir in ["work", "tmp", "home"] {
175            self.create_dir(dir)?;
176        }
177        Ok(())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn pipe_creation() {
187        let pipe = Pipe::new().unwrap();
188        assert!(pipe.read_fd() >= 0);
189        assert_ne!(pipe.read_fd(), pipe.write_fd());
190    }
191
192    #[test]
193    fn workspace_creation() {
194        let ws = Workspace::new().unwrap();
195        assert!(ws.root().exists());
196    }
197
198    #[test]
199    fn workspace_write_file() {
200        let ws = Workspace::new().unwrap();
201        let path = ws.write_file("test.txt", b"hello", false).unwrap();
202        assert!(path.exists());
203    }
204
205    #[test]
206    fn workspace_write_executable() {
207        use std::os::unix::fs::PermissionsExt;
208
209        let ws = Workspace::new().unwrap();
210        let path = ws.write_file("binary", b"\x7fELF", true).unwrap();
211        assert!(path.exists());
212        let perms = std::fs::metadata(&path).unwrap().permissions();
213        assert_eq!(perms.mode() & 0o777, 0o755);
214    }
215
216    #[test]
217    fn workspace_sandbox_dirs() {
218        let ws = Workspace::new().unwrap();
219        ws.setup_sandbox_dirs().unwrap();
220        assert!(ws.root().join("work").exists());
221        assert!(ws.root().join("tmp").exists());
222        assert!(ws.root().join("home").exists());
223    }
224}