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        let child_ready = unsafe { libc::eventfd(0, 0) };
74        if child_ready < 0 {
75            return Err(io::Error::last_os_error());
76        }
77        let parent_done = unsafe { libc::eventfd(0, 0) };
78        if parent_done < 0 {
79            unsafe { libc::close(child_ready) };
80            return Err(io::Error::last_os_error());
81        }
82        Ok(Self {
83            child_ready: unsafe { OwnedFd::from_raw_fd(child_ready) },
84            parent_done: unsafe { OwnedFd::from_raw_fd(parent_done) },
85        })
86    }
87
88    #[inline]
89    pub fn child_ready_fd(&self) -> RawFd {
90        self.child_ready.as_raw_fd()
91    }
92
93    #[inline]
94    pub fn parent_done_fd(&self) -> RawFd {
95        self.parent_done.as_raw_fd()
96    }
97}
98
99/// All pipes for sandbox I/O.
100#[derive(Debug)]
101pub struct Pipes {
102    pub stdin: Pipe,
103    pub stdout: Pipe,
104    pub stderr: Pipe,
105    pub sync: SyncPair,
106}
107
108impl Pipes {
109    pub fn new() -> io::Result<Self> {
110        Ok(Self {
111            stdin: Pipe::new()?,
112            stdout: Pipe::new()?,
113            stderr: Pipe::new()?,
114            sync: SyncPair::new()?,
115        })
116    }
117}
118
119/// Temporary workspace for sandbox execution.
120#[derive(Debug)]
121pub struct Workspace {
122    root: PathBuf,
123    pub pipes: Pipes,
124    _tempdir: TempDir,
125}
126
127impl Workspace {
128    pub fn new() -> io::Result<Self> {
129        Self::with_prefix("evalbox-")
130    }
131
132    pub fn with_prefix(prefix: &str) -> io::Result<Self> {
133        let tempdir = TempDir::with_prefix(prefix)?;
134        Ok(Self {
135            root: tempdir.path().to_path_buf(),
136            pipes: Pipes::new()?,
137            _tempdir: tempdir,
138        })
139    }
140
141    #[inline]
142    pub fn root(&self) -> &Path {
143        &self.root
144    }
145
146    pub fn write_file(&self, path: &str, content: &[u8], executable: bool) -> io::Result<PathBuf> {
147        use std::os::unix::fs::PermissionsExt;
148
149        let full = self.root.join(path);
150        if let Some(parent) = full.parent() {
151            fs::create_dir_all(parent)?;
152        }
153        fs::write(&full, content)?;
154
155        if executable {
156            fs::set_permissions(&full, fs::Permissions::from_mode(0o755))?;
157        }
158
159        Ok(full)
160    }
161
162    pub fn create_dir(&self, path: &str) -> io::Result<PathBuf> {
163        let full = self.root.join(path);
164        fs::create_dir_all(&full)?;
165        Ok(full)
166    }
167
168    /// Create standard sandbox directories.
169    ///
170    /// Only creates the writable workspace directories (work, tmp, home).
171    /// No rootfs directories (proc, dev, etc.) needed since we don't use `pivot_root`.
172    pub fn setup_sandbox_dirs(&self) -> io::Result<()> {
173        for dir in ["work", "tmp", "home"] {
174            self.create_dir(dir)?;
175        }
176        Ok(())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn pipe_creation() {
186        let pipe = Pipe::new().unwrap();
187        assert!(pipe.read_fd() >= 0);
188        assert_ne!(pipe.read_fd(), pipe.write_fd());
189    }
190
191    #[test]
192    fn workspace_creation() {
193        let ws = Workspace::new().unwrap();
194        assert!(ws.root().exists());
195    }
196
197    #[test]
198    fn workspace_write_file() {
199        let ws = Workspace::new().unwrap();
200        let path = ws.write_file("test.txt", b"hello", false).unwrap();
201        assert!(path.exists());
202    }
203
204    #[test]
205    fn workspace_write_executable() {
206        use std::os::unix::fs::PermissionsExt;
207
208        let ws = Workspace::new().unwrap();
209        let path = ws.write_file("binary", b"\x7fELF", true).unwrap();
210        assert!(path.exists());
211        let perms = std::fs::metadata(&path).unwrap().permissions();
212        assert_eq!(perms.mode() & 0o777, 0o755);
213    }
214
215    #[test]
216    fn workspace_sandbox_dirs() {
217        let ws = Workspace::new().unwrap();
218        ws.setup_sandbox_dirs().unwrap();
219        assert!(ws.root().join("work").exists());
220        assert!(ws.root().join("tmp").exists());
221        assert!(ws.root().join("home").exists());
222    }
223}