1use std::collections::{HashMap, HashSet};
41use std::path::PathBuf;
42use std::time::Duration;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum NotifyMode {
49 #[default]
51 Disabled,
52 Monitor,
55 Virtualize,
58}
59
60#[derive(Debug, Clone)]
64pub struct Mount {
65 pub source: PathBuf,
67 pub target: PathBuf,
69 pub writable: bool,
71 pub executable: bool,
73}
74
75impl Mount {
76 pub fn ro(path: impl Into<PathBuf>) -> Self {
78 let path = path.into();
79 Self {
80 source: path.clone(),
81 target: path,
82 writable: false,
83 executable: true,
84 }
85 }
86
87 pub fn ro_noexec(path: impl Into<PathBuf>) -> Self {
89 let path = path.into();
90 Self {
91 source: path.clone(),
92 target: path,
93 writable: false,
94 executable: false,
95 }
96 }
97
98 pub fn rw(path: impl Into<PathBuf>) -> Self {
100 let path = path.into();
101 Self {
102 source: path.clone(),
103 target: path,
104 writable: true,
105 executable: true,
106 }
107 }
108
109 pub fn bind(source: impl Into<PathBuf>, target: impl Into<PathBuf>) -> Self {
111 Self {
112 source: source.into(),
113 target: target.into(),
114 writable: false,
115 executable: true,
116 }
117 }
118
119 pub fn writable(mut self) -> Self {
121 self.writable = true;
122 self
123 }
124
125 pub fn noexec(mut self) -> Self {
127 self.executable = false;
128 self
129 }
130}
131
132#[derive(Debug, Clone, Default)]
152pub struct Syscalls {
153 pub allowed: HashSet<i64>,
155 pub denied: HashSet<i64>,
157}
158
159impl Syscalls {
160 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn allow(mut self, syscall: i64) -> Self {
167 self.allowed.insert(syscall);
168 self.denied.remove(&syscall);
169 self
170 }
171
172 pub fn deny(mut self, syscall: i64) -> Self {
174 self.denied.insert(syscall);
175 self.allowed.remove(&syscall);
176 self
177 }
178
179 pub fn allow_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
181 for syscall in syscalls {
182 self.allowed.insert(syscall);
183 self.denied.remove(&syscall);
184 }
185 self
186 }
187
188 pub fn deny_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
190 for syscall in syscalls {
191 self.denied.insert(syscall);
192 self.allowed.remove(&syscall);
193 }
194 self
195 }
196}
197
198#[derive(Debug, Clone, Default)]
214pub struct Landlock {
215 pub read_paths: Vec<PathBuf>,
217 pub write_paths: Vec<PathBuf>,
219 pub execute_paths: Vec<PathBuf>,
221}
222
223impl Landlock {
224 pub fn new() -> Self {
226 Self::default()
227 }
228
229 pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
231 self.read_paths.push(path.into());
232 self
233 }
234
235 pub fn allow_read_write(mut self, path: impl Into<PathBuf>) -> Self {
237 self.write_paths.push(path.into());
238 self
239 }
240
241 pub fn allow_execute(mut self, path: impl Into<PathBuf>) -> Self {
243 self.execute_paths.push(path.into());
244 self
245 }
246}
247
248#[derive(Debug, Clone)]
250pub struct UserFile {
251 pub path: String,
252 pub content: Vec<u8>,
253 pub executable: bool,
254}
255
256impl UserFile {
257 pub fn new(path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
258 Self {
259 path: path.into(),
260 content: content.into(),
261 executable: false,
262 }
263 }
264
265 pub fn executable(mut self) -> Self {
266 self.executable = true;
267 self
268 }
269}
270
271#[derive(Debug, Clone)]
290pub struct Plan {
291 pub cmd: Vec<String>,
292 pub binary_path: Option<PathBuf>,
295 pub env: HashMap<String, String>,
296 pub stdin: Option<Vec<u8>>,
297 pub cwd: String,
298 pub mounts: Vec<Mount>,
299 pub user_files: Vec<UserFile>,
300 pub workspace_size: u64,
301 pub timeout: Duration,
302 pub memory_limit: u64,
303 pub max_pids: u32,
304 pub max_output: u64,
305 pub network_blocked: bool,
306 pub syscalls: Option<Syscalls>,
308 pub landlock: Option<Landlock>,
310 pub notify_mode: NotifyMode,
312}
313
314impl Default for Plan {
315 fn default() -> Self {
316 Self {
317 cmd: Vec::new(),
318 binary_path: None,
319 env: default_env(),
320 stdin: None,
321 cwd: "/work".into(),
322 mounts: Vec::new(),
323 user_files: Vec::new(),
324 workspace_size: 64 * 1024 * 1024,
325 timeout: Duration::from_secs(30),
326 memory_limit: 256 * 1024 * 1024,
327 max_pids: 64,
328 max_output: 16 * 1024 * 1024,
329 network_blocked: true,
330 syscalls: None,
331 landlock: None,
332 notify_mode: NotifyMode::Disabled,
333 }
334 }
335}
336
337impl Plan {
338 pub fn new(cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
339 Self {
340 cmd: cmd.into_iter().map(Into::into).collect(),
341 ..Default::default()
342 }
343 }
344
345 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
346 self.env.insert(key.into(), value.into());
347 self
348 }
349
350 pub fn stdin(mut self, data: impl Into<Vec<u8>>) -> Self {
351 self.stdin = Some(data.into());
352 self
353 }
354
355 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
356 self.cwd = cwd.into();
357 self
358 }
359
360 pub fn mount(mut self, mount: Mount) -> Self {
361 self.mounts.push(mount);
362 self
363 }
364
365 pub fn mounts(mut self, mounts: impl IntoIterator<Item = Mount>) -> Self {
367 self.mounts.extend(mounts);
368 self
369 }
370
371 pub fn binary_path(mut self, path: impl Into<PathBuf>) -> Self {
376 self.binary_path = Some(path.into());
377 self
378 }
379
380 pub fn file(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
381 self.user_files.push(UserFile::new(path, content));
382 self
383 }
384
385 pub fn executable(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
387 self.user_files
388 .push(UserFile::new(path, content).executable());
389 self
390 }
391
392 pub fn timeout(mut self, timeout: Duration) -> Self {
393 self.timeout = timeout;
394 self
395 }
396
397 pub fn memory_limit(mut self, limit: u64) -> Self {
398 self.memory_limit = limit;
399 self
400 }
401
402 pub fn max_pids(mut self, max: u32) -> Self {
403 self.max_pids = max;
404 self
405 }
406
407 pub fn max_output(mut self, max: u64) -> Self {
408 self.max_output = max;
409 self
410 }
411
412 pub fn network_blocked(mut self, blocked: bool) -> Self {
413 self.network_blocked = blocked;
414 self
415 }
416
417 pub fn network(mut self, enabled: bool) -> Self {
422 self.network_blocked = !enabled;
423 self
424 }
425
426 pub fn memory(self, limit: u64) -> Self {
428 self.memory_limit(limit)
429 }
430
431 pub fn syscalls(mut self, syscalls: Syscalls) -> Self {
433 self.syscalls = Some(syscalls);
434 self
435 }
436
437 pub fn landlock(mut self, landlock: Landlock) -> Self {
439 self.landlock = Some(landlock);
440 self
441 }
442
443 pub fn notify_mode(mut self, mode: NotifyMode) -> Self {
449 self.notify_mode = mode;
450 self
451 }
452
453 pub fn exec(self) -> Result<crate::Output, crate::ExecutorError> {
457 crate::Executor::run(self)
458 }
459}
460
461fn default_env() -> HashMap<String, String> {
462 let default_path = if std::path::Path::new("/nix/store").exists() {
465 "/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/bin:/bin"
466 } else {
467 "/usr/local/bin:/usr/bin:/bin"
468 };
469
470 HashMap::from([
471 ("PATH".into(), default_path.into()),
472 ("HOME".into(), "/home".into()),
473 ("USER".into(), "sandbox".into()),
474 ("LANG".into(), "C.UTF-8".into()),
475 ("LC_ALL".into(), "C.UTF-8".into()),
476 ])
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn plan_new() {
485 let plan = Plan::new(["echo", "hello"]);
486 assert_eq!(plan.cmd, vec!["echo", "hello"]);
487 assert!(plan.network_blocked);
488 }
489
490 #[test]
491 fn plan_builder() {
492 let plan = Plan::new(["python", "main.py"])
493 .env("PYTHONPATH", "/work")
494 .stdin(b"input".to_vec())
495 .timeout(Duration::from_secs(10))
496 .file("main.py", b"print('hello')");
497
498 assert_eq!(plan.env.get("PYTHONPATH"), Some(&"/work".into()));
499 assert_eq!(plan.stdin, Some(b"input".to_vec()));
500 assert_eq!(plan.timeout, Duration::from_secs(10));
501 assert_eq!(plan.user_files.len(), 1);
502 }
503
504 #[test]
505 fn plan_network_methods() {
506 let plan = Plan::new(["echo"]).network(true);
507 assert!(!plan.network_blocked);
508
509 let plan = Plan::new(["echo"]).network(false);
510 assert!(plan.network_blocked);
511 }
512
513 #[test]
514 fn plan_syscalls_config() {
515 let syscalls = Syscalls::default().allow(1).allow(2).deny(3);
516
517 assert!(syscalls.allowed.contains(&1));
518 assert!(syscalls.allowed.contains(&2));
519 assert!(syscalls.denied.contains(&3));
520 }
521
522 #[test]
523 fn plan_landlock_config() {
524 let landlock = Landlock::new().allow_read("/etc").allow_read_write("/tmp");
525
526 assert_eq!(landlock.read_paths.len(), 1);
527 assert_eq!(landlock.write_paths.len(), 1);
528 }
529}