pub mod fsevents;
pub mod limits;
pub mod pdeath_watch;
pub mod seatbelt;
use std::ffi::CString;
use std::os::fd::{AsRawFd, RawFd};
use anyhow::{anyhow, bail, Result};
use super::handle::{KillStrategy, SandboxHandle, SpawnOptions, StdioMode};
use super::Spawned;
use crate::config::NetMode;
use crate::policy::Policy;
pub struct MacosSandbox {
pub net: NetMode,
}
impl MacosSandbox {
pub fn new(net: NetMode) -> Self {
Self { net }
}
pub fn seatbelt_available() -> bool {
seatbelt::is_native_seatbelt_available()
|| std::path::Path::new("/usr/bin/sandbox-exec").exists()
}
pub fn spawn(self, policy: &Policy, opts: SpawnOptions) -> Result<Spawned> {
if !Self::seatbelt_available() {
bail!(
"missing Seatbelt primitive (sandbox_init_with_parameters / sandbox-exec unavailable); \
refusing to run unsandboxed (fail-closed)\n\
action: ensure macOS sandbox libraries and /usr/bin/sandbox-exec are accessible; run `vetto doctor` for the full capability picture"
);
}
if matches!(self.net, NetMode::Allowlist(_)) {
bail!(
"--net=allowlist requires the Linux network-namespace relay and is unavailable on macOS; \
refusing silently-weaker enforcement (fail-closed)\n\
action: run with `--net=off` on macOS; run `vetto doctor` for the full capability picture"
);
}
if matches!(self.net, NetMode::Strict(_)) {
bail!(
"--net=strict requires the Linux network-namespace relay and is unavailable on macOS; \
refusing silently-weaker enforcement (fail-closed)\n\
action: run with `--net=off` on macOS; run `vetto doctor` for the full capability picture"
);
}
if matches!(self.net, NetMode::Ask) {
bail!(
"--net=ask requires the Linux network-namespace relay and is unavailable on macOS; \
refusing silently-weaker enforcement (fail-closed)\n\
action: run with `--net=off` on macOS; run `vetto doctor` for the full capability picture"
);
}
let (err_r, err_w) = pipe2()?;
let err_w_raw = err_w.as_raw_fd();
let agent_c: Vec<CString> = opts
.agent_cmd
.iter()
.map(|s| CString::new(s.as_str()).unwrap_or_default())
.collect();
let env_c = build_envp(policy, &opts);
let opts_ref = &opts;
let policy_ref = policy;
let net_ref = &self.net;
let pid = unsafe { libc::fork() };
if pid < 0 {
bail!("fork: {}", std::io::Error::last_os_error());
}
if pid == 0 {
child(policy_ref, net_ref, agent_c, env_c, err_w_raw, opts_ref);
}
drop(err_w);
pdeath_watch::spawn(unsafe { libc::getpid() }, pid);
let mut b = [0u8; 1];
let n = unsafe { libc::read(err_r.as_raw_fd(), b.as_mut_ptr().cast(), 1) };
match (n, b[0]) {
(1, b'R') => {}
(1, b'E') => {
let code = reap(pid);
return Err(anyhow!("sandbox setup failed (child exit {code})"));
}
_ => {
let code = reap(pid);
return Err(anyhow!("sandbox child died during setup (exit {code})"));
}
}
Ok(Spawned {
handle: SandboxHandle {
root_pid: pid as u32,
strategy: Some(KillStrategy::ProcessGroup {
pid,
pgid: pid,
sweep: false,
}),
},
broker_ctrl_fd: None,
relay_port: None,
notif_listener: None,
})
}
}
fn reap(pid: libc::pid_t) -> i32 {
let mut status = 0i32;
unsafe { libc::waitpid(pid, &mut status, 0) };
if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else {
-1
}
}
fn pipe2() -> Result<(std::os::fd::OwnedFd, std::os::fd::OwnedFd)> {
use std::os::fd::{FromRawFd, OwnedFd};
let mut fds = [0 as libc::c_int; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
bail!("pipe: {}", std::io::Error::last_os_error());
}
Ok((unsafe { OwnedFd::from_raw_fd(fds[0]) }, unsafe {
OwnedFd::from_raw_fd(fds[1])
}))
}
fn child_write_all(fd: RawFd, mut buf: &[u8]) {
while !buf.is_empty() {
let n = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
if n > 0 {
buf = &buf[n as usize..];
} else if n < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
continue;
} else {
return;
}
}
}
fn child_fail(err_w: RawFd, code: i32, msg: &str) -> ! {
let mut reason = String::with_capacity(msg.len() + 4);
reason.push_str("E:");
reason.push_str(msg);
reason.push('\n');
child_write_all(err_w, reason.as_bytes());
unsafe { libc::_exit(code) }
}
fn close_all_except(keep: &[RawFd]) {
let max = unsafe { libc::sysconf(libc::_SC_OPEN_MAX) } as i32;
let max = max.clamp(16, 65_536);
for fd in 3..max {
if !keep.contains(&fd) {
unsafe { libc::close(fd) };
}
}
}
fn dup2_to(fd: RawFd, target: RawFd) -> Result<(), String> {
loop {
if unsafe { libc::dup2(fd, target) } >= 0 {
return Ok(());
}
let error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(format!("dup2({fd}, {target}): {error}"));
}
}
fn child_trace(stage: &str) {
if std::env::var_os("VETTO_CHILD_TRACE").is_some() {
eprintln!("vetto: child stage: {stage}");
}
}
fn child(
policy: &Policy,
net: &NetMode,
agent: Vec<CString>,
env: Vec<CString>,
err_w: RawFd,
opts: &SpawnOptions,
) -> ! {
child_trace("entered");
let mut keep: Vec<RawFd> = vec![err_w];
match opts.stdio {
StdioMode::Pty { slave_fd } => keep.push(slave_fd),
StdioMode::Captured { stdout_w, stderr_w } => {
keep.push(stdout_w);
keep.push(stderr_w);
}
StdioMode::Inherit => {}
}
close_all_except(&keep);
match opts.stdio {
StdioMode::Pty { .. } => {
if unsafe { libc::setsid() } < 0 {
child_fail(
err_w,
125,
&format!("setsid: {}", std::io::Error::last_os_error()),
);
}
}
StdioMode::Captured { .. } | StdioMode::Inherit => {
if unsafe { libc::setpgid(0, 0) } < 0 {
child_fail(
err_w,
125,
&format!("setpgid: {}", std::io::Error::last_os_error()),
);
}
}
}
match opts.stdio {
StdioMode::Pty { slave_fd } => {
if unsafe { libc::ioctl(slave_fd, libc::TIOCSCTTY as libc::c_ulong, 0i32) } != 0 {
child_fail(
err_w,
124,
&format!("TIOCSCTTY: {}", std::io::Error::last_os_error()),
);
}
for target in [0, 1, 2] {
if let Err(error) = dup2_to(slave_fd, target) {
child_fail(err_w, 124, &error);
}
}
}
StdioMode::Captured { stdout_w, stderr_w } => {
let devnull = unsafe { libc::open(b"/dev/null\0".as_ptr().cast(), libc::O_RDONLY) };
if devnull < 0 {
child_fail(
err_w,
124,
&format!("open /dev/null: {}", std::io::Error::last_os_error()),
);
}
if let Err(error) = dup2_to(devnull, 0) {
child_fail(err_w, 124, &error);
}
if let Err(error) = dup2_to(stdout_w, 1) {
child_fail(err_w, 124, &error);
}
if let Err(error) = dup2_to(stderr_w, 2) {
child_fail(err_w, 124, &error);
}
unsafe { libc::close(devnull) };
}
StdioMode::Inherit => {}
}
child_trace("limits-about-to-apply");
if std::env::var_os("VETTO_NO_MAC_LIMITS").is_some() {
child_trace("limits-skipped-by-env");
} else {
for refused in limits::apply_before_exec(&policy.limits) {
eprintln!("vetto: {refused}");
}
}
child_trace("limits-done");
if let Err(e) = std::env::set_current_dir(&opts.cwd) {
let _ = e;
unsafe { libc::_exit(117) };
}
child_trace("cwd-set");
match std::env::var_os("VETTO_SEATBELT_MODE")
.as_deref()
.and_then(|m| m.to_str())
{
Some("none") => {
child_trace("seatbelt-skipped-by-env");
}
Some(mode @ ("allow-all" | "deny-net-only")) => {
let profile = if mode == "allow-all" {
"(version 1)\n(allow default)\n"
} else {
"(version 1)\n(deny network*)\n"
};
child_trace(&format!("seatbelt-diagnostic-mode: {mode}"));
if let Err(err) = seatbelt::apply_seatbelt_raw(profile, &[]) {
child_fail(err_w, 120, &format!("apply seatbelt ({mode}): {err}"));
}
}
Some(mode @ ("wb-tcpudp" | "wb-mach" | "wb-socket")) => {
let profile = match mode {
"wb-tcpudp" => "(version 1)\n(deny network-outbound (remote tcp-port *))\n(deny network-outbound (remote udp-port *))\n",
"wb-mach" => "(version 1)\n(deny network*)\n(allow mach-lookup)\n(allow system-socket)\n",
_ => "(version 1)\n(deny network*)\n(allow system-socket)\n",
};
child_trace(&format!("seatbelt-diagnostic-mode: {mode}"));
if let Err(err) = seatbelt::apply_seatbelt_raw(profile, &[]) {
child_fail(err_w, 120, &format!("apply seatbelt ({mode}): {err}"));
}
}
_ => {
if let Err(err) = seatbelt::apply_seatbelt(policy, net) {
child_fail(err_w, 120, &format!("apply seatbelt: {err}"));
}
}
}
child_trace("seatbelt-applied");
if std::env::var_os("VETTO_CHILD_TRACE").is_some() {
eprintln!(
"vetto: child seatbelt profile:
{}",
seatbelt::generate(policy, net)
);
}
child_trace("ready-about-to-send");
let _ = unsafe { libc::write(err_w, b"R".as_ptr().cast(), 1) };
child_trace("ready-sent");
close_all_except(&[0, 1, 2]);
let prog = match agent.first() {
Some(p) => p.clone(),
None => unsafe { libc::_exit(127) },
};
let mut argv: Vec<*const libc::c_char> = agent.iter().map(|a| a.as_ptr()).collect();
argv.push(std::ptr::null());
let mut envp: Vec<*const libc::c_char> = env.iter().map(|e| e.as_ptr()).collect();
envp.push(std::ptr::null());
unsafe { libc::execve(prog.as_ptr(), argv.as_ptr(), envp.as_ptr()) };
child_trace("execve-returned");
let errno = std::io::Error::last_os_error();
child_trace(&format!("execve-errno: {errno}"));
unsafe { libc::_exit(127) }
}
fn build_envp(policy: &Policy, opts: &SpawnOptions) -> Vec<CString> {
let mut env: std::collections::BTreeMap<std::ffi::OsString, std::ffi::OsString> =
std::env::vars_os()
.filter(|(key, _)| policy.environment.allows(key))
.collect();
crate::cred_broker::filter_proxy_secrets(&mut env, &policy.secret_proxies);
for (k, v) in &opts.env_extra {
env.insert(
std::ffi::OsString::from(k.as_str()),
std::ffi::OsString::from(v.as_str()),
);
}
env.iter()
.map(|(k, v)| {
let mut entry = k.as_encoded_bytes().to_vec();
entry.push(b'=');
entry.extend_from_slice(v.as_encoded_bytes());
CString::new(entry).unwrap_or_default()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seatbelt_available_does_not_panic() {
let _ = MacosSandbox::seatbelt_available();
}
}