use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NotifyMode {
#[default]
Disabled,
Monitor,
Virtualize,
}
#[derive(Debug, Clone)]
pub struct Mount {
pub source: PathBuf,
pub target: PathBuf,
pub writable: bool,
pub executable: bool,
}
impl Mount {
pub fn ro(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
source: path.clone(),
target: path,
writable: false,
executable: true,
}
}
pub fn ro_noexec(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
source: path.clone(),
target: path,
writable: false,
executable: false,
}
}
pub fn rw(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
source: path.clone(),
target: path,
writable: true,
executable: true,
}
}
pub fn bind(source: impl Into<PathBuf>, target: impl Into<PathBuf>) -> Self {
Self {
source: source.into(),
target: target.into(),
writable: false,
executable: true,
}
}
pub fn writable(mut self) -> Self {
self.writable = true;
self
}
pub fn noexec(mut self) -> Self {
self.executable = false;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Syscalls {
pub allowed: HashSet<i64>,
pub denied: HashSet<i64>,
}
impl Syscalls {
pub fn new() -> Self {
Self::default()
}
pub fn allow(mut self, syscall: i64) -> Self {
self.allowed.insert(syscall);
self.denied.remove(&syscall);
self
}
pub fn deny(mut self, syscall: i64) -> Self {
self.denied.insert(syscall);
self.allowed.remove(&syscall);
self
}
pub fn allow_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
for syscall in syscalls {
self.allowed.insert(syscall);
self.denied.remove(&syscall);
}
self
}
pub fn deny_many(mut self, syscalls: impl IntoIterator<Item = i64>) -> Self {
for syscall in syscalls {
self.denied.insert(syscall);
self.allowed.remove(&syscall);
}
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Landlock {
pub read_paths: Vec<PathBuf>,
pub write_paths: Vec<PathBuf>,
pub execute_paths: Vec<PathBuf>,
}
impl Landlock {
pub fn new() -> Self {
Self::default()
}
pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
self.read_paths.push(path.into());
self
}
pub fn allow_read_write(mut self, path: impl Into<PathBuf>) -> Self {
self.write_paths.push(path.into());
self
}
pub fn allow_execute(mut self, path: impl Into<PathBuf>) -> Self {
self.execute_paths.push(path.into());
self
}
}
#[derive(Debug, Clone)]
pub struct UserFile {
pub path: String,
pub content: Vec<u8>,
pub executable: bool,
}
impl UserFile {
pub fn new(path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
Self {
path: path.into(),
content: content.into(),
executable: false,
}
}
pub fn executable(mut self) -> Self {
self.executable = true;
self
}
}
#[must_use]
#[derive(Debug, Clone)]
pub struct Plan {
pub cmd: Vec<String>,
pub binary_path: Option<PathBuf>,
pub env: HashMap<String, String>,
pub stdin: Option<Vec<u8>>,
pub cwd: String,
pub mounts: Vec<Mount>,
pub user_files: Vec<UserFile>,
pub workspace_size: u64,
pub timeout: Duration,
pub memory_limit: u64,
pub max_pids: u32,
pub max_output: u64,
pub network_blocked: bool,
pub syscalls: Option<Syscalls>,
pub landlock: Option<Landlock>,
pub notify_mode: NotifyMode,
}
impl Default for Plan {
fn default() -> Self {
Self {
cmd: Vec::new(),
binary_path: None,
env: default_env(),
stdin: None,
cwd: "/work".into(),
mounts: Vec::new(),
user_files: Vec::new(),
workspace_size: 64 * 1024 * 1024,
timeout: Duration::from_secs(30),
memory_limit: 256 * 1024 * 1024,
max_pids: 64,
max_output: 16 * 1024 * 1024,
network_blocked: true,
syscalls: None,
landlock: None,
notify_mode: NotifyMode::Disabled,
}
}
}
impl Plan {
pub fn new(cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
cmd: cmd.into_iter().map(Into::into).collect(),
..Default::default()
}
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn stdin(mut self, data: impl Into<Vec<u8>>) -> Self {
self.stdin = Some(data.into());
self
}
pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
self.cwd = cwd.into();
self
}
pub fn mount(mut self, mount: Mount) -> Self {
self.mounts.push(mount);
self
}
pub fn mounts(mut self, mounts: impl IntoIterator<Item = Mount>) -> Self {
self.mounts.extend(mounts);
self
}
pub fn binary_path(mut self, path: impl Into<PathBuf>) -> Self {
self.binary_path = Some(path.into());
self
}
pub fn file(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
self.user_files.push(UserFile::new(path, content));
self
}
pub fn executable(mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
self.user_files
.push(UserFile::new(path, content).executable());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn memory_limit(mut self, limit: u64) -> Self {
self.memory_limit = limit;
self
}
pub fn max_pids(mut self, max: u32) -> Self {
self.max_pids = max;
self
}
pub fn max_output(mut self, max: u64) -> Self {
self.max_output = max;
self
}
pub fn network_blocked(mut self, blocked: bool) -> Self {
self.network_blocked = blocked;
self
}
pub fn network(mut self, enabled: bool) -> Self {
self.network_blocked = !enabled;
self
}
pub fn memory(self, limit: u64) -> Self {
self.memory_limit(limit)
}
pub fn syscalls(mut self, syscalls: Syscalls) -> Self {
self.syscalls = Some(syscalls);
self
}
pub fn landlock(mut self, landlock: Landlock) -> Self {
self.landlock = Some(landlock);
self
}
pub fn notify_mode(mut self, mode: NotifyMode) -> Self {
self.notify_mode = mode;
self
}
pub fn exec(self) -> Result<crate::Output, crate::ExecutorError> {
crate::Executor::run(self)
}
}
fn default_env() -> HashMap<String, String> {
let default_path = if std::path::Path::new("/nix/store").exists() {
"/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin"
} else {
"/usr/local/bin:/usr/bin:/bin"
};
HashMap::from([
("PATH".into(), default_path.into()),
("HOME".into(), "/home".into()),
("USER".into(), "sandbox".into()),
("LANG".into(), "C.UTF-8".into()),
("LC_ALL".into(), "C.UTF-8".into()),
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_new() {
let plan = Plan::new(["echo", "hello"]);
assert_eq!(plan.cmd, vec!["echo", "hello"]);
assert!(plan.network_blocked);
}
#[test]
fn plan_builder() {
let plan = Plan::new(["python", "main.py"])
.env("PYTHONPATH", "/work")
.stdin(b"input".to_vec())
.timeout(Duration::from_secs(10))
.file("main.py", b"print('hello')");
assert_eq!(plan.env.get("PYTHONPATH"), Some(&"/work".into()));
assert_eq!(plan.stdin, Some(b"input".to_vec()));
assert_eq!(plan.timeout, Duration::from_secs(10));
assert_eq!(plan.user_files.len(), 1);
}
#[test]
fn plan_network_methods() {
let plan = Plan::new(["echo"]).network(true);
assert!(!plan.network_blocked);
let plan = Plan::new(["echo"]).network(false);
assert!(plan.network_blocked);
}
#[test]
fn plan_syscalls_config() {
let syscalls = Syscalls::default().allow(1).allow(2).deny(3);
assert!(syscalls.allowed.contains(&1));
assert!(syscalls.allowed.contains(&2));
assert!(syscalls.denied.contains(&3));
}
#[test]
fn plan_landlock_config() {
let landlock = Landlock::new().allow_read("/etc").allow_read_write("/tmp");
assert_eq!(landlock.read_paths.len(), 1);
assert_eq!(landlock.write_paths.len(), 1);
}
}