use std::path::Path;
use std::process::Command;
use super::jail::{JailBackend, SpawnSpec};
use super::{Runner, RunnerError};
const SANDBOX_EXEC: &str = "/usr/bin/sandbox-exec";
const RUNTIME_READ_SUBPATHS: &[&str] = &["/usr/lib", "/System"];
const RUNTIME_READ_DEVICES: &[&str] = &["/dev/urandom", "/dev/random", "/dev/null"];
pub struct SeatbeltJail;
fn profile(jail: &Path, worker: &Path) -> Result<String, RunnerError> {
let literal = |path: &Path| -> Result<String, RunnerError> {
let text = path.to_str().ok_or_else(|| RunnerError {
code: "sandbox_unavailable",
message: format!("path {} is not UTF-8", path.display()),
})?;
if text.contains('"') || text.contains('\\') {
return Err(RunnerError {
code: "sandbox_unavailable",
message: format!("path {text:?} cannot be quoted in a Seatbelt profile"),
});
}
Ok(text.to_string())
};
let jail = literal(jail)?;
let worker = literal(worker)?;
let runtime_reads = RUNTIME_READ_SUBPATHS
.iter()
.map(|path| format!(" (subpath \"{path}\")"))
.chain(
RUNTIME_READ_DEVICES
.iter()
.map(|path| format!(" (literal \"{path}\")")),
)
.collect::<Vec<_>>()
.join("\n");
Ok(format!(
r#"(version 1)
(deny default)
(deny network*)
(allow process-fork)
(allow process-exec* (literal "{worker}"))
(allow mach-lookup)
(allow mach-bootstrap)
(allow ipc-posix-shm*)
(allow sysctl-read)
(allow system-sched)
(allow signal (target same-sandbox))
(import "dyld-support.sb")
(allow file-read-metadata)
(allow file-read* file-map-executable (literal "{worker}"))
(allow file-read* file-map-executable
{runtime_reads}
(subpath "/dev/fd"))
(allow file-read* file-write* (subpath "{jail}"))
"#
))
}
impl JailBackend for SeatbeltJail {
fn name(&self) -> &'static str {
"macos-seatbelt"
}
fn command(&self, spec: &SpawnSpec<'_>) -> Result<Command, RunnerError> {
if !Path::new(SANDBOX_EXEC).is_file() {
return Err(RunnerError {
code: "sandbox_unavailable",
message: format!("{SANDBOX_EXEC} is missing, refusing to run adapters"),
});
}
let mut command = Command::new(SANDBOX_EXEC);
command
.arg("-p")
.arg(profile(spec.jail, spec.worker)?)
.arg(spec.worker)
.arg("sandbox-helper")
.arg("--jail")
.arg(spec.jail)
.arg("--worker")
.arg(spec.worker)
.arg("--adapter")
.arg(spec.mode)
.arg("--rlimit-as")
.arg(spec.limits.address_space_bytes.to_string())
.arg("--rlimit-cpu")
.arg(spec.limits.cpu_seconds.to_string())
.arg("--rlimit-fsize")
.arg(spec.limits.file_size_bytes.to_string())
.arg("--rlimit-nproc")
.arg(spec.limits.max_processes.to_string())
.arg("--seccomp")
.arg("off");
Ok(command)
}
fn policy_material(&self) -> String {
format!(
"seatbelt deny-default deny-network runtime-read={} devices={} jail-read-write",
RUNTIME_READ_SUBPATHS.join(","),
RUNTIME_READ_DEVICES.join(",")
)
}
fn probe(&self, runner: &Runner) -> Result<(), RunnerError> {
super::jail::probe_net(runner, false)
}
}