1use std::collections::{HashMap, HashSet};
41use std::path::PathBuf;
42use std::time::Duration;
43
44#[derive(Debug, Clone)]
48pub struct Mount {
49 pub source: PathBuf,
51 pub target: PathBuf,
53 pub writable: bool,
55 pub executable: bool,
57}
58
59impl Mount {
60 pub fn ro(path: impl Into<PathBuf>) -> Self {
62 let path = path.into();
63 Self {
64 source: path.clone(),
65 target: path,
66 writable: false,
67 executable: true,
68 }
69 }
70
71 pub fn ro_noexec(path: impl Into<PathBuf>) -> Self {
73 let path = path.into();
74 Self {
75 source: path.clone(),
76 target: path,
77 writable: false,
78 executable: false,
79 }
80 }
81
82 pub fn rw(path: impl Into<PathBuf>) -> Self {
84 let path = path.into();
85 Self {
86 source: path.clone(),
87 target: path,
88 writable: true,
89 executable: true,
90 }
91 }
92
93 pub fn bind(source: impl Into<PathBuf>, target: impl Into<PathBuf>) -> Self {
95 Self {
96 source: source.into(),
97 target: target.into(),
98 writable: false,
99 executable: true,
100 }
101 }
102
103 pub fn writable(mut self) -> Self {
105 self.writable = true;
106 self
107 }
108
109 pub fn noexec(mut self) -> Self {
111 self.executable = false;
112 self
113 }
114}
115
116#[derive(Debug, Clone, Default)]
136pub struct Syscalls {
137 pub allowed: HashSet<i64>,
139 pub denied: HashSet<i64>,
141}
142
143impl Syscalls {
144 pub fn new() -> Self {
146 Self::default()
147 }
148
149 pub fn allow(mut self, syscall: i64) -> Self {
151 self.allowed.insert(syscall);
152 self.denied.remove(&syscall);
153 self
154 }
155
156 pub fn deny(mut self, syscall: i64) -> Self {
158 self.denied.insert(syscall);
159 self.allowed.remove(&syscall);
160 self
161 }
162
163 pub fn allow_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
165 for syscall in syscalls {
166 self.allowed.insert(syscall);
167 self.denied.remove(&syscall);
168 }
169 self
170 }
171
172 pub fn deny_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
174 for syscall in syscalls {
175 self.denied.insert(syscall);
176 self.allowed.remove(&syscall);
177 }
178 self
179 }
180}
181
182#[derive(Debug, Clone, Default)]
198pub struct Landlock {
199 pub read_paths: Vec<PathBuf>,
201 pub write_paths: Vec<PathBuf>,
203 pub execute_paths: Vec<PathBuf>,
205}
206
207impl Landlock {
208 pub fn new() -> Self {
210 Self::default()
211 }
212
213 pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
215 self.read_paths.push(path.into());
216 self
217 }
218
219 pub fn allow_read_write(mut self, path: impl Into<PathBuf>) -> Self {
221 self.write_paths.push(path.into());
222 self
223 }
224
225 pub fn allow_execute(mut self, path: impl Into<PathBuf>) -> Self {
227 self.execute_paths.push(path.into());
228 self
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct UserFile {
235 pub path: String,
236 pub content: Vec<u8>,
237 pub executable: bool,
238}
239
240impl UserFile {
241 pub fn new(path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
242 Self { path: path.into(), content: content.into(), executable: false }
243 }
244
245 pub fn executable(mut self) -> Self {
246 self.executable = true;
247 self
248 }
249}
250
251#[derive(Debug, Clone)]
270pub struct Plan {
271 pub cmd: Vec<String>,
272 pub binary_path: Option<PathBuf>,
275 pub env: HashMap<String, String>,
276 pub stdin: Option<Vec<u8>>,
277 pub cwd: String,
278 pub mounts: Vec<Mount>,
279 pub user_files: Vec<UserFile>,
280 pub workspace_size: u64,
281 pub timeout: Duration,
282 pub memory_limit: u64,
283 pub max_pids: u32,
284 pub max_output: u64,
285 pub network_blocked: bool,
286 pub syscalls: Option<Syscalls>,
288 pub landlock: Option<Landlock>,
290}
291
292#[deprecated(since = "0.2.0", note = "Use `Plan` instead")]
294pub type SandboxPlan = Plan;
295
296impl Default for Plan {
297 fn default() -> Self {
298 Self {
299 cmd: Vec::new(),
300 binary_path: None,
301 env: default_env(),
302 stdin: None,
303 cwd: "/work".into(),
304 mounts: Vec::new(),
305 user_files: Vec::new(),
306 workspace_size: 64 * 1024 * 1024,
307 timeout: Duration::from_secs(30),
308 memory_limit: 256 * 1024 * 1024,
309 max_pids: 64,
310 max_output: 16 * 1024 * 1024,
311 network_blocked: true,
312 syscalls: None,
313 landlock: None,
314 }
315 }
316}
317
318impl Plan {
319 pub fn new(cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
320 Self { cmd: cmd.into_iter().map(Into::into).collect(), ..Default::default() }
321 }
322
323 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
324 self.env.insert(key.into(), value.into());
325 self
326 }
327
328 pub fn stdin(mut self, data: impl Into<Vec<u8>>) -> Self {
329 self.stdin = Some(data.into());
330 self
331 }
332
333 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
334 self.cwd = cwd.into();
335 self
336 }
337
338 pub fn mount(mut self, mount: Mount) -> Self {
339 self.mounts.push(mount);
340 self
341 }
342
343 pub fn mounts(mut self, mounts: impl IntoIterator<Item = Mount>) -> Self {
345 self.mounts.extend(mounts);
346 self
347 }
348
349 pub fn binary_path(mut self, path: impl Into<PathBuf>) -> Self {
354 self.binary_path = Some(path.into());
355 self
356 }
357
358 pub fn file(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
359 self.user_files.push(UserFile::new(path, content));
360 self
361 }
362
363 pub fn executable(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
365 self.user_files.push(UserFile::new(path, content).executable());
366 self
367 }
368
369 pub fn timeout(mut self, timeout: Duration) -> Self {
370 self.timeout = timeout;
371 self
372 }
373
374 pub fn memory_limit(mut self, limit: u64) -> Self {
375 self.memory_limit = limit;
376 self
377 }
378
379 pub fn max_pids(mut self, max: u32) -> Self {
380 self.max_pids = max;
381 self
382 }
383
384 pub fn max_output(mut self, max: u64) -> Self {
385 self.max_output = max;
386 self
387 }
388
389 pub fn network_blocked(mut self, blocked: bool) -> Self {
390 self.network_blocked = blocked;
391 self
392 }
393
394 pub fn network(mut self, enabled: bool) -> Self {
399 self.network_blocked = !enabled;
400 self
401 }
402
403 pub fn memory(self, limit: u64) -> Self {
405 self.memory_limit(limit)
406 }
407
408 pub fn syscalls(mut self, syscalls: Syscalls) -> Self {
410 self.syscalls = Some(syscalls);
411 self
412 }
413
414 pub fn landlock(mut self, landlock: Landlock) -> Self {
416 self.landlock = Some(landlock);
417 self
418 }
419
420 pub fn exec(self) -> Result<crate::Output, crate::ExecutorError> {
424 crate::Executor::run(self)
425 }
426}
427
428fn default_env() -> HashMap<String, String> {
429 let default_path = if std::path::Path::new("/nix/store").exists() {
432 "/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/bin:/bin"
433 } else {
434 "/usr/local/bin:/usr/bin:/bin"
435 };
436
437 HashMap::from([
438 ("PATH".into(), default_path.into()),
439 ("HOME".into(), "/home".into()),
440 ("USER".into(), "sandbox".into()),
441 ("LANG".into(), "C.UTF-8".into()),
442 ("LC_ALL".into(), "C.UTF-8".into()),
443 ])
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn plan_new() {
452 let plan = Plan::new(["echo", "hello"]);
453 assert_eq!(plan.cmd, vec!["echo", "hello"]);
454 assert!(plan.network_blocked);
455 }
456
457 #[test]
458 fn plan_builder() {
459 let plan = Plan::new(["python", "main.py"])
460 .env("PYTHONPATH", "/work")
461 .stdin(b"input".to_vec())
462 .timeout(Duration::from_secs(10))
463 .file("main.py", b"print('hello')");
464
465 assert_eq!(plan.env.get("PYTHONPATH"), Some(&"/work".into()));
466 assert_eq!(plan.stdin, Some(b"input".to_vec()));
467 assert_eq!(plan.timeout, Duration::from_secs(10));
468 assert_eq!(plan.user_files.len(), 1);
469 }
470
471 #[test]
472 fn plan_network_methods() {
473 let plan = Plan::new(["echo"]).network(true);
474 assert!(!plan.network_blocked);
475
476 let plan = Plan::new(["echo"]).network(false);
477 assert!(plan.network_blocked);
478 }
479
480 #[test]
481 fn plan_syscalls_config() {
482 let syscalls = Syscalls::default()
483 .allow(1)
484 .allow(2)
485 .deny(3);
486
487 assert!(syscalls.allowed.contains(&1));
488 assert!(syscalls.allowed.contains(&2));
489 assert!(syscalls.denied.contains(&3));
490 }
491
492 #[test]
493 fn plan_landlock_config() {
494 let landlock = Landlock::new()
495 .allow_read("/etc")
496 .allow_read_write("/tmp");
497
498 assert_eq!(landlock.read_paths.len(), 1);
499 assert_eq!(landlock.write_paths.len(), 1);
500 }
501}