use std::path::Path;
use std::process::{Command, Output};
use super::{
policy_allows_network, policy_allows_workspace_write,
process_sandbox_developer_toolchain_read_roots,
process_sandbox_package_manager_config_read_roots, process_sandbox_policy_read_roots,
process_sandbox_policy_write_roots, process_sandbox_presets, process_sandbox_readonly_roots,
process_sandbox_roots, process_spawn_error, spawn_error, unavailable, PrepareOutcome,
ProcessCommandConfig, SandboxBackend,
};
use crate::orchestration::{CapabilityPolicy, ProcessSandboxPreset, SandboxProfile};
use crate::value::VmError;
mod toolchain_roots;
const SANDBOX_EXEC_PATH: &str = "/usr/bin/sandbox-exec";
pub(super) struct Backend;
impl SandboxBackend for Backend {
fn name() -> &'static str {
"macos"
}
fn available() -> bool {
Path::new(SANDBOX_EXEC_PATH).exists()
}
fn prepare_std_command(
program: &str,
args: &[String],
_command: &mut Command,
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<PrepareOutcome, VmError> {
wrap_with_sandbox_exec(program, args, policy, profile)
}
fn prepare_tokio_command(
program: &str,
args: &[String],
_command: &mut tokio::process::Command,
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<PrepareOutcome, VmError> {
wrap_with_sandbox_exec(program, args, policy, profile)
}
fn run_to_output(
program: &str,
args: &[String],
config: &ProcessCommandConfig,
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<Output, VmError> {
let mut command = super::build_std_command::<Self>(program, args, policy, profile)?;
super::apply_process_config(&mut command, config, Some(policy));
let output = crate::op_interrupt::capture_output_interruptible(&mut command)
.map_err(|error| process_spawn_error(&error).unwrap_or_else(|| spawn_error(error)))?;
match crate::process_sandbox::macos_wrapped_spawn_io_error(
output.status.code().unwrap_or(-1),
&output.stderr,
) {
Some(error) => Err(spawn_error(error)),
None => Ok(output),
}
}
}
fn wrap_with_sandbox_exec(
program: &str,
args: &[String],
policy: &CapabilityPolicy,
profile: SandboxProfile,
) -> Result<PrepareOutcome, VmError> {
if !Path::new(SANDBOX_EXEC_PATH).exists() {
return unavailable("macOS sandbox-exec is not available", profile);
}
let mut wrapped_args = vec![
"-p".to_string(),
render_profile_for_program(policy, program),
"--".to_string(),
program.to_string(),
];
wrapped_args.extend(macos_sandbox_compatible_args(program, args));
Ok(PrepareOutcome::WrappedExec {
wrapper: SANDBOX_EXEC_PATH.to_string(),
args: wrapped_args,
})
}
fn render_profile_for_program(policy: &CapabilityPolicy, program: &str) -> String {
let mut developer_toolchain_read_roots = process_sandbox_developer_toolchain_read_roots(policy);
developer_toolchain_read_roots.extend(toolchain_roots::go_read_root(policy, program));
developer_toolchain_read_roots.sort_unstable();
developer_toolchain_read_roots.dedup();
render_profile_with_extra_read_roots(
policy,
&developer_toolchain_read_roots,
&process_sandbox_package_manager_config_read_roots(policy),
&super::process_sandbox_developer_toolchain_cache_roots(policy),
)
}
fn macos_sandbox_compatible_args(program: &str, args: &[String]) -> Vec<String> {
if is_swiftpm_invocation(program, args) {
return swiftpm_outer_sandbox_args(args);
}
args.to_vec()
}
fn is_swiftpm_invocation(program: &str, args: &[String]) -> bool {
Path::new(program)
.file_name()
.and_then(|name| name.to_str())
== Some("swift")
&& matches!(
args.first().map(String::as_str),
Some("build" | "test" | "run" | "package")
)
}
fn swiftpm_outer_sandbox_args(args: &[String]) -> Vec<String> {
let mut rewritten = Vec::with_capacity(args.len() + 9);
rewritten.push(args[0].clone());
if !has_swiftpm_option(args, "--disable-sandbox") {
rewritten.push("--disable-sandbox".to_string());
}
if !has_swiftpm_option(args, "--manifest-cache") {
rewritten.extend(["--manifest-cache".to_string(), "local".to_string()]);
}
if !has_swiftpm_option(args, "--cache-path") {
rewritten.extend([
"--cache-path".to_string(),
".build/harn/swiftpm/cache".to_string(),
]);
}
if !has_swiftpm_option(args, "--config-path") {
rewritten.extend([
"--config-path".to_string(),
".build/harn/swiftpm/config".to_string(),
]);
}
if !has_swiftpm_option(args, "--security-path") {
rewritten.extend([
"--security-path".to_string(),
".build/harn/swiftpm/security".to_string(),
]);
}
rewritten.extend(args.iter().skip(1).cloned());
rewritten
}
fn has_swiftpm_option(args: &[String], option: &str) -> bool {
let equals_prefix = format!("{option}=");
args.iter()
.any(|arg| arg == option || arg.starts_with(&equals_prefix))
}
#[cfg(test)]
fn render_profile(policy: &CapabilityPolicy) -> String {
let developer_toolchain_read_roots = process_sandbox_developer_toolchain_read_roots(policy);
let package_manager_read_roots = process_sandbox_package_manager_config_read_roots(policy);
let developer_toolchain_cache_roots =
super::process_sandbox_developer_toolchain_cache_roots(policy);
render_profile_with_extra_read_roots(
policy,
&developer_toolchain_read_roots,
&package_manager_read_roots,
&developer_toolchain_cache_roots,
)
}
fn render_profile_with_extra_read_roots(
policy: &CapabilityPolicy,
developer_toolchain_read_roots: &[std::path::PathBuf],
package_manager_read_roots: &[std::path::PathBuf],
developer_toolchain_cache_roots: &[std::path::PathBuf],
) -> String {
let developer_toolchain_read_roots = normalize_profile_roots(developer_toolchain_read_roots);
let read_deny_roots = super::process_sandbox_read_deny_roots(policy);
let package_manager_read_roots = normalize_profile_roots(package_manager_read_roots);
let developer_toolchain_cache_roots = normalize_profile_roots(developer_toolchain_cache_roots);
let roots = process_sandbox_roots(policy);
let read_only_roots = process_sandbox_readonly_roots(policy);
let policy_read_roots = process_sandbox_policy_read_roots(policy);
let policy_write_roots = process_sandbox_policy_write_roots(policy);
let mut profile = String::from(
"(version 1)\n\
(deny default)\n\
(allow process*)\n\
(allow signal (target same-sandbox))\n\
(allow sysctl-read)\n\
(allow mach-lookup)\n\
(allow file-read-metadata)\n\
(allow file-read-data (literal \"/\"))\n",
);
profile.push_str(standard_device_profile_rules());
for root in preset_read_roots(policy) {
profile.push_str(&format!(
"(allow file-read* (subpath \"{}\"))\n",
sandbox_profile_escape(root)
));
}
for root in developer_toolchain_read_roots
.iter()
.chain(developer_toolchain_cache_roots.iter())
{
profile.push_str(&format!(
"(allow file-read* (subpath \"{}\"))\n",
sandbox_profile_escape(&root.display().to_string())
));
}
for root in roots
.iter()
.chain(read_only_roots.iter())
.chain(policy_read_roots.iter())
.chain(package_manager_read_roots.iter())
{
profile.push_str(&format!(
"(allow file-read* (subpath \"{}\"))\n",
sandbox_profile_escape(&root.display().to_string())
));
}
if policy_allows_workspace_write(policy) {
for root in preset_write_roots(policy) {
profile.push_str(&format!(
"(allow file-read* (subpath \"{}\"))\n",
sandbox_profile_escape(root)
));
}
for root in &policy_write_roots {
profile.push_str(&format!(
"(allow file-read* (subpath \"{}\"))\n",
sandbox_profile_escape(&root.display().to_string())
));
}
profile.push_str("(allow file-write*");
for root in preset_write_roots(policy) {
profile.push_str(&format!(" (subpath \"{}\")", sandbox_profile_escape(root)));
}
for root in policy_write_roots
.iter()
.chain(developer_toolchain_cache_roots.iter())
{
profile.push_str(&format!(
" (subpath \"{}\")",
sandbox_profile_escape(&root.display().to_string())
));
}
profile.push_str(")\n");
for root in &roots {
profile.push_str(&format!(
"(allow file-write* (subpath \"{}\"))\n",
sandbox_profile_escape(&root.display().to_string())
));
}
for root in read_only_roots
.iter()
.chain(package_manager_read_roots.iter())
{
if developer_toolchain_cache_roots
.iter()
.any(|cache| root.starts_with(cache))
{
continue;
}
for path in sandbox_profile_path_aliases(&root.display().to_string()) {
profile.push_str(&format!(
"(deny file-write* (subpath \"{}\"))\n",
sandbox_profile_escape(&path)
));
}
}
for root in granted_write_roots(&roots, &policy_write_roots, policy)
.iter()
.filter(|root| {
read_only_roots
.iter()
.any(|read_only| root.starts_with(read_only))
})
{
profile.push_str(&format!(
"(allow file-write* (subpath \"{}\"))\n",
sandbox_profile_escape(&root.display().to_string())
));
}
profile.push_str(standard_device_profile_rules());
}
if policy_allows_network(policy) && !policy.process_sandbox.allow_tcp_loopback {
if let Some(proxy) = policy.process_network_proxy {
for port in [proxy.http_port, proxy.socks_port] {
profile.push_str(&format!(
"(allow network-outbound (remote ip \"localhost:{port}\"))\n"
));
}
} else {
profile.push_str("(allow network*)\n");
}
}
if policy.process_sandbox.allow_tcp_loopback {
profile.push_str("(allow network-bind (local ip \"localhost:*\"))\n");
profile.push_str("(allow network-inbound (local ip \"localhost:*\"))\n");
profile.push_str("(allow network-outbound (remote ip \"localhost:*\"))\n");
}
for root in read_deny_roots {
for path in sandbox_profile_path_aliases(&root.display().to_string()) {
profile.push_str(&format!(
"(deny file-read* (subpath \"{}\"))\n",
sandbox_profile_escape(&path)
));
}
}
profile
}
fn normalize_profile_roots(roots: &[std::path::PathBuf]) -> Vec<std::path::PathBuf> {
roots
.iter()
.map(|root| super::normalize_for_policy(root))
.collect()
}
fn preset_read_roots(policy: &CapabilityPolicy) -> Vec<&'static str> {
let mut roots = Vec::new();
for preset in process_sandbox_presets(policy) {
match preset {
ProcessSandboxPreset::SystemRuntime => roots.extend([
"/bin",
"/etc",
"/Library",
"/opt/homebrew",
"/private/etc",
"/private/var/select",
"/System",
"/usr",
"/var/select",
]),
ProcessSandboxPreset::DeveloperToolchains => roots.extend([
"/Applications",
"/Library/Developer",
"/System/Library/Developer",
]),
ProcessSandboxPreset::PackageManagerConfig => {}
ProcessSandboxPreset::UserTemp => {}
}
}
roots.sort_unstable();
roots.dedup();
roots
}
fn granted_write_roots(
workspace_roots: &[std::path::PathBuf],
policy_write_roots: &[std::path::PathBuf],
policy: &CapabilityPolicy,
) -> Vec<std::path::PathBuf> {
let mut granted = workspace_roots.to_vec();
granted.extend(policy_write_roots.iter().cloned());
granted.extend(
preset_write_roots(policy)
.into_iter()
.map(std::path::PathBuf::from),
);
granted.sort();
granted.dedup();
granted
}
fn preset_write_roots(policy: &CapabilityPolicy) -> Vec<&'static str> {
let mut roots = Vec::new();
if process_sandbox_presets(policy).contains(&ProcessSandboxPreset::UserTemp) {
roots.extend([
"/private/tmp",
"/private/var/folders",
"/tmp",
"/var/folders",
"/var/tmp",
]);
}
roots
}
fn sandbox_profile_escape(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
fn sandbox_profile_path_aliases(path: &str) -> Vec<String> {
let mut aliases = vec![path.to_string()];
if path == "/tmp" || path.starts_with("/tmp/") || path == "/var" || path.starts_with("/var/") {
aliases.push(format!("/private{path}"));
} else if path == "/private/tmp"
|| path.starts_with("/private/tmp/")
|| path == "/private/var"
|| path.starts_with("/private/var/")
{
aliases.push(path.replacen("/private", "", 1));
}
aliases.sort_unstable();
aliases.dedup();
aliases
}
fn standard_device_profile_rules() -> &'static str {
"(allow file-read* \
(literal \"/dev/null\") \
(literal \"/dev/zero\") \
(literal \"/dev/random\") \
(literal \"/dev/urandom\") \
(literal \"/dev/stdin\") \
(literal \"/dev/stdout\") \
(literal \"/dev/stderr\") \
(subpath \"/dev/fd\"))\n\
(allow file-write* \
(literal \"/dev/null\") \
(literal \"/dev/stdout\") \
(literal \"/dev/stderr\") \
(subpath \"/dev/fd\"))\n"
}
#[cfg(test)]
#[path = "macos_tests.rs"]
mod tests;