use std::path::Path;
use std::process::Command;
use super::jail::{JailBackend, ProviderJail, RuntimeProfile, SpawnSpec};
use super::{PathClass, Runner, RunnerError, RunnerMessage, TemplateValue};
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;
const ACCELERATOR_OPEN_LINE: &str =
"(allow iokit-open (iokit-user-client-class \"AGXDeviceUserClient\"))\n";
const ACCELERATOR_PROPERTIES_LINE: &str = "(allow iokit-get-properties)\n";
const PROVIDER_PROFILE_TEMPLATE: &str = r#"(version 1)
(deny default)
(deny network*)
(allow network-bind network-outbound
(subpath "{jail}"))
(allow process-fork)
(allow process-exec*
(literal "{worker}")
{closures})
(allow signal (target same-sandbox))
(import "dyld-support.sb")
(allow file-read-metadata)
(allow sysctl-read)
(allow system-sched)
(allow ipc-posix-shm*)
(allow mach-lookup)
(allow mach-bootstrap)
{service}(allow iokit-open)
(allow file-read* file-map-executable
(literal "{worker}")
{closures} (subpath "/usr/lib")
(subpath "/System")
(subpath "/dev/fd"))
(allow file-read*
(literal "/dev/urandom")
(literal "/dev/random")
(literal "/dev/null")
(subpath "/System/Library/Fonts")
(subpath "/Library/Fonts"))
(allow file-write-data (literal "/dev/null"))
(allow file-read* file-write* (subpath "{jail}"))
{grants}"#;
pub fn provider_profile_template_blake3() -> String {
crate::hash::hash_bytes(PROVIDER_PROFILE_TEMPLATE.as_bytes())
}
const PLACEHOLDERS: [&str; 5] = ["{jail}", "{worker}", "{closures}", "{service}", "{grants}"];
fn substitute(template: &str, values: &[(&str, &str)]) -> Result<String, RunnerError> {
for (name, value) in values {
if value.contains('{') || value.contains('}') {
let value = match *name {
"{jail}" => TemplateValue::Jail,
"{worker}" => TemplateValue::Worker,
"{closures}" => TemplateValue::Closures,
"{service}" => TemplateValue::Service,
"{grants}" => TemplateValue::Grants,
_ => unreachable!("the template value table is closed"),
};
return Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::TemplateBrace(value),
));
}
}
let mut out = String::with_capacity(template.len() * 2);
let mut rest = template;
loop {
let next = PLACEHOLDERS
.iter()
.filter_map(|name| rest.find(name).map(|at| (at, *name)))
.min();
let Some((at, name)) = next else {
out.push_str(rest);
break;
};
out.push_str(&rest[..at]);
let value = values
.iter()
.find(|(candidate, _)| *candidate == name)
.map(|(_, value)| *value)
.unwrap_or_default();
out.push_str(value);
rest = &rest[at + name.len()..];
}
Ok(out)
}
fn provider_profile(
jail: &str,
worker: &str,
provider: &ProviderJail,
grant_lines: &str,
) -> Result<String, RunnerError> {
let mut closures = Vec::new();
let total = provider.closures().len();
for (index, path) in provider.closures().iter().enumerate() {
let position = index + 1;
let text = path.to_str().ok_or_else(|| {
RunnerError::new(
"sandbox_unavailable",
RunnerMessage::ClosureNotUtf8 {
position,
count: total,
},
)
})?;
if text.contains('"') || text.contains('\\') {
return Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::ClosureCannotBeQuoted {
position,
count: total,
},
));
}
closures.push(format!(" (subpath \"{text}\")\n (literal \"{text}\")\n"));
}
let service = match provider.service_prefix() {
Some(prefix) => {
if !crate::convert::provider::is_service_namespace(prefix) {
return Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::ServiceNamespace,
));
}
format!("(allow mach-register (global-name-regex #\"{prefix}\"))\n")
}
None => String::new(),
};
substitute(
PROVIDER_PROFILE_TEMPLATE,
&[
("{jail}", jail),
("{worker}", worker),
("{closures}", &closures.concat()),
("{service}", &service),
("{grants}", grant_lines),
],
)
}
fn profile(
jail: &Path,
worker: &Path,
exec_grants: &[std::path::PathBuf],
read_grants: &[std::path::PathBuf],
runtime_profile: RuntimeProfile,
provider: Option<&ProviderJail>,
) -> Result<String, RunnerError> {
let literal = |class: PathClass, path: &Path| -> Result<String, RunnerError> {
let text = path.to_str().ok_or_else(|| {
RunnerError::new("sandbox_unavailable", RunnerMessage::PathNotUtf8(class))
})?;
if text.contains('"') || text.contains('\\') {
return Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::PathCannotBeQuoted(class),
));
}
Ok(text.to_string())
};
let jail = literal(PathClass::Jail, jail)?;
let worker = literal(PathClass::Worker, 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");
let accelerator = runtime_profile == RuntimeProfile::Accelerator && !exec_grants.is_empty();
let mut grant_lines = String::new();
for path in exec_grants {
if accelerator && let Some(parent) = path.parent() {
let parent = literal(PathClass::ExecutableGrantDirectory, parent)?;
grant_lines.push_str(&format!("(allow file-read-data (literal \"{parent}\"))\n"));
}
let path = literal(PathClass::ExecutableGrant, path)?;
grant_lines.push_str(&format!(
"(allow process-exec* file-read* file-map-executable (literal \"{path}\"))\n"
));
}
for path in read_grants {
let path = literal(PathClass::ReadGrant, path)?;
grant_lines.push_str(&format!("(allow file-read* (literal \"{path}\"))\n"));
}
if accelerator {
grant_lines.push_str(ACCELERATOR_PROPERTIES_LINE);
grant_lines.push_str(ACCELERATOR_OPEN_LINE);
}
if runtime_profile == RuntimeProfile::Provider {
let Some(provider) = provider.filter(|_| !exec_grants.is_empty()) else {
return Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::ProviderNeedsWiring,
));
};
return provider_profile(&jail, &worker, provider, &grant_lines);
}
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}"))
{grant_lines}"#
))
}
fn rlimit_as_argument(limit: Option<u64>) -> String {
match limit {
Some(bytes) => bytes.to_string(),
None => "none".to_string(),
}
}
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::new(
"sandbox_unavailable",
RunnerMessage::SandboxHelperMissing,
));
}
let mut command = Command::new(SANDBOX_EXEC);
command
.arg("-p")
.arg(profile(
spec.jail,
spec.worker,
spec.exec_grants,
spec.read_grants,
spec.runtime_profile,
spec.provider,
)?)
.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(rlimit_as_argument(spec.limits.address_space_bytes))
.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");
for path in spec.exec_grants {
command.arg("--grant-exec").arg(path);
}
for path in spec.read_grants {
command.arg("--grant-read").arg(path);
}
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
const BASE_0_6_0: &str = r#"(version 1)
(deny default)
(deny network*)
(allow process-fork)
(allow process-exec* (literal "/opt/worker/text-mirror-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 "/opt/worker/text-mirror-worker"))
(allow file-read* file-map-executable
(subpath "/usr/lib")
(subpath "/System")
(literal "/dev/urandom")
(literal "/dev/random")
(literal "/dev/null")
(subpath "/dev/fd"))
(allow file-read* file-write* (subpath "/jail/run"))
"#;
const ENGINE_LINE: &str =
"(allow process-exec* file-read* file-map-executable (literal \"/deploy/engine-cli\"))\n";
const WEIGHTS_LINE: &str = "(allow file-read* (literal \"/deploy/weights.bin\"))\n";
const PARENT_LISTING_LINE: &str = "(allow file-read-data (literal \"/deploy\"))\n";
const DEVICE_LINES: &str = "(allow iokit-get-properties)\n(allow iokit-open (iokit-user-client-class \"AGXDeviceUserClient\"))\n";
fn render(exec: &[&str], read: &[&str], runtime_profile: RuntimeProfile) -> String {
render_with(exec, read, runtime_profile, None).expect("the fixed paths render")
}
fn render_with(
exec: &[&str],
read: &[&str],
runtime_profile: RuntimeProfile,
provider: Option<&ProviderJail>,
) -> Result<String, RunnerError> {
let exec: Vec<PathBuf> = exec.iter().map(PathBuf::from).collect();
let read: Vec<PathBuf> = read.iter().map(PathBuf::from).collect();
profile(
Path::new("/jail/run"),
Path::new("/opt/worker/text-mirror-worker"),
&exec,
&read,
runtime_profile,
provider,
)
}
const PROVIDER_0_7_0: &str = r#"(version 1)
(deny default)
(deny network*)
(allow network-bind network-outbound
(subpath "/jail/run"))
(allow process-fork)
(allow process-exec*
(literal "/opt/worker/text-mirror-worker")
(subpath "/deploy/closure")
(literal "/deploy/closure")
)
(allow signal (target same-sandbox))
(import "dyld-support.sb")
(allow file-read-metadata)
(allow sysctl-read)
(allow system-sched)
(allow ipc-posix-shm*)
(allow mach-lookup)
(allow mach-bootstrap)
(allow mach-register (global-name-regex #"^ex\.ample\."))
(allow iokit-open)
(allow file-read* file-map-executable
(literal "/opt/worker/text-mirror-worker")
(subpath "/deploy/closure")
(literal "/deploy/closure")
(subpath "/usr/lib")
(subpath "/System")
(subpath "/dev/fd"))
(allow file-read*
(literal "/dev/urandom")
(literal "/dev/random")
(literal "/dev/null")
(subpath "/System/Library/Fonts")
(subpath "/Library/Fonts"))
(allow file-write-data (literal "/dev/null"))
(allow file-read* file-write* (subpath "/jail/run"))
(allow process-exec* file-read* file-map-executable (literal "/deploy/engine-cli"))
"#;
fn provider(closures: &[&str], service: Option<&str>) -> ProviderJail {
ProviderJail::new(
closures.iter().map(PathBuf::from).collect(),
service.map(str::to_string),
None,
)
.expect("the test parameters fit the grammar")
}
fn closure_fixture() -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let closure = dir.path().canonicalize().unwrap().join("closure");
std::fs::create_dir_all(&closure).unwrap();
let text = closure.to_str().unwrap().to_string();
(dir, text)
}
#[test]
fn the_provider_profile_template_is_pinned_to_its_own_bytes() {
assert_eq!(
crate::hash::hash_bytes(PROVIDER_PROFILE_TEMPLATE.as_bytes()),
"6cab9514f16c626cdd936e779a72845a9d72c2c04857c7eb4a6f25942e8ef5f2"
);
}
#[test]
fn a_wired_provider_renders_the_measured_profile_byte_for_byte() {
let (_dir, closure) = closure_fixture();
let rendered = render_with(
&["/deploy/engine-cli"],
&[],
RuntimeProfile::Provider,
Some(&provider(&[&closure], Some(r"^ex\.ample\."))),
)
.expect("the fixed paths render");
let expected = PROVIDER_0_7_0.replace("/deploy/closure", &closure);
assert_eq!(rendered, expected, "{rendered}");
assert!(!rendered.contains("iokit-get-properties"), "{rendered}");
assert!(!rendered.contains("iokit-user-client-class"), "{rendered}");
assert!(!rendered.contains("file-read-data"), "{rendered}");
}
#[test]
fn a_value_carrying_template_syntax_is_rendered_verbatim_or_refused() {
let dir = tempfile::tempdir().unwrap();
let closure = dir.path().canonicalize().unwrap().join("closure-jail");
std::fs::create_dir_all(&closure).unwrap();
let closure = closure.to_str().unwrap().to_string();
let rendered = render_with(
&["/deploy/engine-cli"],
&[],
RuntimeProfile::Provider,
Some(&provider(&[&closure], None)),
)
.expect("a placeholder-looking path renders");
assert!(
rendered.contains(&format!("(subpath \"{closure}\")")),
"{rendered}"
);
let error = substitute(
PROVIDER_PROFILE_TEMPLATE,
&[
("{jail}", "/jail/{worker}"),
("{worker}", "/opt/worker"),
("{closures}", ""),
("{service}", ""),
("{grants}", ""),
],
)
.expect_err("a brace-bearing value must refuse");
assert_eq!(error.code, "sandbox_unavailable");
let rendered = substitute(
PROVIDER_PROFILE_TEMPLATE,
&[
("{jail}", "/jail/one"),
("{worker}", "/opt/worker"),
("{closures}", " (subpath \"/deploy/c\")\n"),
(
"{service}",
"(allow mach-register (global-name-regex #\"^a\\\\.\"))\n",
),
(
"{grants}",
"(allow process-exec* (literal \"/deploy/x\"))\n",
),
],
)
.unwrap();
for name in PLACEHOLDERS {
assert!(
!rendered.contains(name),
"{name} survived rendering: {rendered}"
);
}
assert!(rendered.ends_with("(allow process-exec* (literal \"/deploy/x\"))\n"));
}
#[test]
fn the_render_boundary_holds_the_parameter_grammar() {
let (dir, closure) = closure_fixture();
for bad in [r"^.*", r"ex\.ample\.", r"^(a|b)\.", r"^a\.[a-z]+\."] {
let error =
ProviderJail::new(vec![PathBuf::from(&closure)], Some(bad.to_string()), None)
.expect_err("a non-conforming namespace must refuse");
assert_eq!(error.code, "sandbox_unavailable", "{bad}");
}
let error = ProviderJail::new(vec![PathBuf::from("/usr/lib")], None, None)
.expect_err("a shared root must refuse");
assert_eq!(error.code, "sandbox_unavailable");
let error = ProviderJail::new(
vec![PathBuf::from(&closure)],
None,
Some("lowercase".to_string()),
)
.expect_err("a malformed variable name must refuse");
assert_eq!(error.code, "sandbox_unavailable");
let error = ProviderJail::new(vec![PathBuf::from("/private/tmp/../tmp")], None, None)
.expect_err("an alias of a shared root must refuse");
assert_eq!(error.code, "sandbox_unavailable");
assert!(error.message().contains("normal form"), "{error}");
let store = dir.path().join("store");
std::os::unix::fs::symlink("/usr/lib", &store).unwrap();
let error = ProviderJail::new(vec![store], None, None)
.expect_err("a link to a shared root must refuse");
assert_eq!(error.code, "sandbox_unavailable");
assert!(error.message().contains("symlink"), "{error}");
let alias = dir.path().join("alias");
std::os::unix::fs::symlink(&closure, &alias).unwrap();
let error =
ProviderJail::new(vec![alias], None, None).expect_err("a link entry must refuse");
assert_eq!(error.code, "sandbox_unavailable");
let error = ProviderJail::new(vec![PathBuf::from("/deploy/closure")], None, None)
.expect_err("an entry that does not exist must refuse");
assert_eq!(error.code, "sandbox_unavailable");
assert!(error.message().contains("resolved"), "{error}");
let usrlink = dir.path().join("usrlink");
std::os::unix::fs::symlink("/usr", &usrlink).unwrap();
let error = ProviderJail::new(vec![usrlink.join("lib")], None, None)
.expect_err("a real entry carried into a shared root must refuse");
assert_eq!(error.code, "sandbox_unavailable");
assert!(error.message().contains("shared root"), "{error}");
let socket = dir.path().join("sock");
let _listener = std::os::unix::net::UnixListener::bind(&socket).unwrap();
let error =
ProviderJail::new(vec![socket], None, None).expect_err("a socket entry must refuse");
assert_eq!(error.code, "sandbox_unavailable");
assert!(
error
.message()
.contains("not a directory or a regular file"),
"{error}"
);
let fifo = dir.path().join("fifo");
assert!(
std::process::Command::new("/usr/bin/mkfifo")
.arg(&fifo)
.status()
.unwrap()
.success()
);
let error = ProviderJail::new(vec![fifo.clone()], None, None)
.expect_err("a FIFO entry must refuse");
assert!(
error
.message()
.contains("not a directory or a regular file"),
"{error}"
);
for entry in [
fifo,
PathBuf::from("relative"),
PathBuf::from("/private/tmp/../tmp"),
dir.path().join("store"),
dir.path().join("sock"),
PathBuf::from("/deploy/closure"),
PathBuf::from("/usr/lib"),
usrlink.join("lib"),
] {
let error = ProviderJail::new(vec![entry.clone()], None, None)
.expect_err("every one of these refuses");
assert!(
!error.message().contains('/'),
"{}: {error}",
entry.display()
);
}
let jail = ProviderJail::new(
vec![PathBuf::from(&closure)],
Some(r"^ex\.ample\.".to_string()),
Some("EXAMPLE_TMPDIR".to_string()),
)
.unwrap();
assert_eq!(jail.closures(), &[PathBuf::from(&closure)]);
let unresolved = dir.path().join("closure");
let jail = ProviderJail::new(vec![unresolved], None, None).unwrap();
assert_eq!(jail.closures(), &[PathBuf::from(&closure)]);
}
#[test]
fn the_provider_profile_omits_what_the_deployment_did_not_configure() {
let rendered = render_with(
&["/deploy/engine-cli"],
&[],
RuntimeProfile::Provider,
Some(&provider(&[], None)),
)
.expect("the fixed paths render");
assert!(!rendered.contains("mach-register"), "{rendered}");
assert!(!rendered.contains("subpath \"/deploy"), "{rendered}");
assert!(rendered.contains("(subpath \"/jail/run\")"), "{rendered}");
assert!(rendered.contains("/deploy/engine-cli"), "{rendered}");
}
#[test]
fn the_provider_class_renders_nothing_without_a_wired_provider() {
let error = render_with(&["/deploy/engine-cli"], &[], RuntimeProfile::Provider, None)
.expect_err("an unwired provider class must refuse");
assert_eq!(error.code, "sandbox_unavailable");
let (_dir, closure) = closure_fixture();
let error = render_with(
&[],
&[],
RuntimeProfile::Provider,
Some(&provider(&[&closure], None)),
)
.expect_err("a grant-free provider class must refuse");
assert_eq!(error.code, "sandbox_unavailable");
}
#[test]
fn a_provider_path_that_cannot_be_quoted_refuses_the_run() {
let (dir, _closure) = closure_fixture();
for name in ["quote\"mark", "back\\slash"] {
let closure = dir.path().canonicalize().unwrap().join(name);
std::fs::create_dir_all(&closure).unwrap();
let error = render_with(
&["/deploy/engine-cli"],
&[],
RuntimeProfile::Provider,
Some(&provider(&[closure.to_str().unwrap()], None)),
)
.expect_err("an unquotable closure must refuse");
assert_eq!(error.code, "sandbox_unavailable");
}
let (_dir, closure) = closure_fixture();
let error = ProviderJail::new(
vec![PathBuf::from(&closure)],
Some("bad\"expression".to_string()),
None,
)
.expect_err("an unquotable service expression must refuse");
assert_eq!(error.code, "sandbox_unavailable");
}
#[test]
fn a_grant_free_jail_renders_the_0_6_0_profile_byte_for_byte() {
assert_eq!(render(&[], &[], RuntimeProfile::Plain), BASE_0_6_0);
assert_eq!(render(&[], &[], RuntimeProfile::Accelerator), BASE_0_6_0);
}
#[test]
fn the_plain_class_with_grants_is_the_base_plus_the_literal_grant_lines_only() {
let rendered = render(
&["/deploy/engine-cli"],
&["/deploy/weights.bin"],
RuntimeProfile::Plain,
);
assert_eq!(
rendered,
format!("{BASE_0_6_0}{ENGINE_LINE}{WEIGHTS_LINE}"),
"{rendered}"
);
assert!(!rendered.contains("iokit"), "{rendered}");
assert!(!rendered.contains("file-read-data"), "{rendered}");
}
#[test]
fn the_accelerator_class_carries_the_parent_listing_and_exactly_two_device_lines() {
let rendered = render(
&["/deploy/engine-cli"],
&["/deploy/weights.bin"],
RuntimeProfile::Accelerator,
);
assert_eq!(
rendered,
format!("{BASE_0_6_0}{PARENT_LISTING_LINE}{ENGINE_LINE}{WEIGHTS_LINE}{DEVICE_LINES}"),
"{rendered}"
);
assert!(rendered.contains(PARENT_LISTING_LINE), "{rendered}");
let device_lines: Vec<&str> = rendered
.lines()
.filter(|line| line.contains("iokit"))
.collect();
assert_eq!(
device_lines,
vec![
"(allow iokit-get-properties)",
r#"(allow iokit-open (iokit-user-client-class "AGXDeviceUserClient"))"#,
],
"{rendered}"
);
assert!(!rendered.contains("(allow iokit-open)"), "{rendered}");
assert!(!rendered.contains("(allow iokit*"), "{rendered}");
assert!(!rendered.contains("file-read-data (subpath"), "{rendered}");
}
}