#![allow(unsafe_code)]
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::sync::OnceLock;
#[repr(C)]
struct Profile {
builtin: *mut c_char,
data: *const u8,
size: usize,
}
#[repr(C)]
struct ApplyArgs {
data: *const u8,
size: usize,
container: *const c_char,
container_len: usize,
}
type CompileString =
unsafe extern "C" fn(*const c_char, *mut c_void, *mut *mut c_char) -> *mut Profile;
type FreeProfile = unsafe extern "C" fn(*mut Profile);
type CreateParams = unsafe extern "C" fn() -> *mut c_void;
type SetParam = unsafe extern "C" fn(*mut c_void, *const c_char, *const c_char) -> c_int;
type FreeParams = unsafe extern "C" fn(*mut c_void);
type MacSyscall = unsafe extern "C" fn(*const c_char, c_int, *mut c_void) -> c_int;
type Check = unsafe extern "C" fn(libc::pid_t, *const c_char, c_int, ...) -> c_int;
const SANDBOX_FILTER_GLOBAL_NAME: c_int = 2;
type IssueFile = unsafe extern "C" fn(*const c_char, *const c_char, u32) -> *mut c_char;
type Consume = unsafe extern "C" fn(*const c_char) -> i64;
const LIBSANDBOX: &CStr = c"/usr/lib/libsandbox.1.dylib";
pub(crate) struct Libsandbox {
compile_string: CompileString,
free_profile: FreeProfile,
create_params: CreateParams,
set_param: SetParam,
free_params: FreeParams,
issue_file: IssueFile,
child: ChildOps,
}
#[derive(Clone, Copy)]
pub(crate) struct ChildOps {
mac_syscall: MacSyscall,
check: Check,
consume: Consume,
}
fn resolve(name: &str) -> Option<*mut c_void> {
let cname = CString::new(name).ok()?;
let p = unsafe { libc::dlsym(libc::RTLD_DEFAULT, cname.as_ptr()) };
(!p.is_null()).then_some(p)
}
pub(crate) fn has_symbol(name: &str) -> bool {
load_libsandbox();
resolve(name).is_some()
}
fn load_libsandbox() {
unsafe { libc::dlopen(LIBSANDBOX.as_ptr(), libc::RTLD_NOW | libc::RTLD_GLOBAL) };
}
pub(crate) fn os_build() -> String {
string_sysctl(c"kern.osversion")
}
pub(crate) fn os_release() -> String {
string_sysctl(c"kern.osproductversion")
}
fn string_sysctl(name: &std::ffi::CStr) -> String {
let mut buf = [0u8; 64];
let mut len = buf.len();
let rc = unsafe {
libc::sysctlbyname(
name.as_ptr(),
buf.as_mut_ptr().cast(),
&raw mut len,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return "unknown".into();
}
let end = buf[..len].iter().position(|&b| b == 0).unwrap_or(len);
String::from_utf8_lossy(&buf[..end]).into_owned()
}
impl Libsandbox {
fn load() -> Result<Self, &'static str> {
load_libsandbox();
let get = |name: &'static str| resolve(name).ok_or(name);
unsafe {
Ok(Self {
compile_string: std::mem::transmute::<*mut c_void, CompileString>(get(
"sandbox_compile_string",
)?),
free_profile: std::mem::transmute::<*mut c_void, FreeProfile>(get(
"sandbox_free_profile",
)?),
create_params: std::mem::transmute::<*mut c_void, CreateParams>(get(
"sandbox_create_params",
)?),
set_param: std::mem::transmute::<*mut c_void, SetParam>(get("sandbox_set_param")?),
free_params: std::mem::transmute::<*mut c_void, FreeParams>(get(
"sandbox_free_params",
)?),
issue_file: std::mem::transmute::<*mut c_void, IssueFile>(get(
"sandbox_extension_issue_file",
)?),
child: ChildOps {
mac_syscall: std::mem::transmute::<*mut c_void, MacSyscall>(get(
"__mac_syscall",
)?),
check: std::mem::transmute::<*mut c_void, Check>(get("sandbox_check")?),
consume: std::mem::transmute::<*mut c_void, Consume>(get(
"sandbox_extension_consume",
)?),
},
})
}
}
pub(crate) fn get() -> Result<&'static Libsandbox, &'static str> {
static TABLE: OnceLock<Result<Libsandbox, &'static str>> = OnceLock::new();
TABLE.get_or_init(Libsandbox::load).as_ref().map_err(|e| *e)
}
pub(crate) fn compile(
&self,
sbpl: &str,
params: &[(String, String)],
) -> Result<Vec<u8>, Option<String>> {
let source =
CString::new(sbpl).map_err(|_| Some("profile contains a NUL byte".to_string()))?;
let owned = params
.iter()
.map(|(k, v)| Ok((CString::new(k.as_str())?, CString::new(v.as_str())?)))
.collect::<Result<Vec<_>, std::ffi::NulError>>()
.map_err(|_| Some("parameter contains a NUL byte".to_string()))?;
let mut err: *mut c_char = std::ptr::null_mut();
let profile = unsafe {
let p = if owned.is_empty() {
std::ptr::null_mut()
} else {
(self.create_params)()
};
for (k, v) in &owned {
(self.set_param)(p, k.as_ptr(), v.as_ptr());
}
let profile = (self.compile_string)(source.as_ptr(), p, &raw mut err);
if !p.is_null() {
(self.free_params)(p);
}
profile
};
if profile.is_null() {
if err.is_null() {
return Err(None);
}
let text = unsafe {
let text = CStr::from_ptr(err).to_string_lossy().into_owned();
libc::free(err.cast());
text
};
return Err(Some(text));
}
unsafe {
let p = &*profile;
let data = if p.data.is_null() {
Vec::new()
} else {
std::slice::from_raw_parts(p.data, p.size).to_vec()
};
(self.free_profile)(profile);
Ok(data)
}
}
pub(crate) fn issue_file(&self, class: &str, path: &[u8]) -> Option<CString> {
let class = CString::new(class).ok()?;
let path = CString::new(path).ok()?;
unsafe {
let token = (self.issue_file)(class.as_ptr(), path.as_ptr(), 0);
if token.is_null() {
return None;
}
let owned = CStr::from_ptr(token).to_owned();
libc::free(token.cast());
Some(owned)
}
}
pub(crate) fn is_sandboxed(&self) -> bool {
self.child.verify().is_ok()
}
pub(crate) fn allows_lookup(&self, pid: libc::pid_t, name: &CStr) -> Result<bool, i32> {
let rc = unsafe {
(self.child.check)(
pid,
c"mach-lookup".as_ptr(),
SANDBOX_FILTER_GLOBAL_NAME,
name.as_ptr(),
)
};
match rc {
0 => Ok(true),
1 => Ok(false),
_ => Err(errno()),
}
}
pub(crate) fn child_ops(&self) -> ChildOps {
self.child
}
}
fn errno() -> i32 {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
}
impl ChildOps {
pub(crate) fn apply(&self, bytecode: &[u8]) -> Result<(), i32> {
let mut args = ApplyArgs {
data: bytecode.as_ptr(),
size: bytecode.len(),
container: std::ptr::null(),
container_len: 0,
};
let rc = unsafe { (self.mac_syscall)(c"Sandbox".as_ptr(), 0, (&raw mut args).cast()) };
if rc == 0 { Ok(()) } else { Err(errno()) }
}
pub(crate) fn verify(&self) -> Result<(), i32> {
let rc = unsafe { (self.check)(libc::getpid(), std::ptr::null(), 0) };
if rc == 1 { Ok(()) } else { Err(errno()) }
}
pub(crate) fn consume(&self, token: &CStr) -> Result<(), i32> {
let handle = unsafe { (self.consume)(token.as_ptr()) };
if handle >= 0 {
Ok(())
} else {
Err(errno().max(1))
}
}
}
pub(crate) fn setsid() -> Result<(), i32> {
if unsafe { libc::setsid() } < 0 {
Err(errno())
} else {
Ok(())
}
}
pub(crate) fn setpgid_self() -> Result<(), i32> {
if unsafe { libc::setpgid(0, 0) } == 0 {
Ok(())
} else {
Err(errno())
}
}
pub(crate) fn setrlimit(resource: c_int, cur: u64, max: u64) -> Result<(), i32> {
let lim = libc::rlimit {
rlim_cur: cur,
rlim_max: max,
};
if unsafe { libc::setrlimit(resource, &raw const lim) } == 0 {
Ok(())
} else {
Err(errno())
}
}
pub(crate) fn getrlimit(resource: c_int) -> Option<(u64, u64)> {
let mut lim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
(unsafe { libc::getrlimit(resource, &raw mut lim) } == 0)
.then_some((lim.rlim_cur, lim.rlim_max))
}
pub(crate) fn is_session_leader() -> bool {
unsafe { libc::getsid(0) == libc::getpid() }
}
pub(crate) fn is_group_leader() -> bool {
unsafe { libc::getpgid(0) == libc::getpid() }
}
pub(crate) fn open_readonly(path: &CStr) -> Result<(), i32> {
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Err(errno());
}
unsafe { libc::close(fd) };
Ok(())
}
pub(crate) const STEP_MARKER: i32 = 0x5043_0000;
pub(crate) const fn step_code(step: u32, errno: i32) -> i32 {
STEP_MARKER | ((step as i32 & 0xf) << 12) | (errno & 0xfff)
}
pub(crate) fn decode_step(code: i32) -> Option<(u32, i32)> {
((code as u32 & 0xffff_0000) == STEP_MARKER as u32)
.then_some((((code >> 12) & 0xf) as u32, code & 0xfff))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Session {
New,
Group,
Keep,
}
pub(crate) struct ChildPlan {
pub(crate) ops: ChildOps,
pub(crate) bytecode: std::sync::Arc<[u8]>,
pub(crate) tokens: Vec<CString>,
pub(crate) limits: Vec<(c_int, u64, u64)>,
pub(crate) session: Session,
pub(crate) keep: Vec<c_int>,
pub(crate) fd_buffer: Vec<libc::proc_fdinfo>,
}
pub(crate) mod steps {
pub(crate) const SESSION: u32 = 1;
pub(crate) const RLIMIT: u32 = 2;
pub(crate) const APPLY: u32 = 3;
pub(crate) const VERIFY: u32 = 4;
pub(crate) const CONSUME: u32 = 5;
pub(crate) const FDS: u32 = 6;
}
fn set_fd_flags(fd: c_int, flags: c_int) -> Result<(), i32> {
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags) } == 0 {
Ok(())
} else {
Err(errno())
}
}
fn mark_fds(buf: &mut [libc::proc_fdinfo], keep: &[c_int]) -> Result<(), i32> {
let mark = |fd: c_int| {
if fd < 3 || keep.contains(&fd) {
return Ok(());
}
match set_fd_flags(fd, libc::FD_CLOEXEC) {
Err(libc::EBADF) | Ok(()) => Ok(()),
Err(e) => Err(e),
}
};
let size = std::mem::size_of_val(buf);
let filled = unsafe {
libc::proc_pidinfo(
libc::getpid(),
libc::PROC_PIDLISTFDS,
0,
buf.as_mut_ptr().cast(),
c_int::try_from(size).unwrap_or(c_int::MAX),
)
};
let entry = std::mem::size_of::<libc::proc_fdinfo>();
match usize::try_from(filled) {
Ok(bytes) if bytes > 0 && bytes + entry <= size => {
for info in &buf[..bytes / entry] {
mark(info.proc_fd)?;
}
}
_ => {
let table = unsafe { libc::getdtablesize() };
for fd in 3..table {
mark(fd)?;
}
}
}
for &fd in keep {
set_fd_flags(fd, 0)?;
}
Ok(())
}
pub(crate) fn fd_buffer() -> Vec<libc::proc_fdinfo> {
let needed = unsafe {
libc::proc_pidinfo(
libc::getpid(),
libc::PROC_PIDLISTFDS,
0,
std::ptr::null_mut(),
0,
)
};
let entries = usize::try_from(needed).unwrap_or(0) / std::mem::size_of::<libc::proc_fdinfo>();
vec![
libc::proc_fdinfo {
proc_fd: 0,
proc_fdtype: 0
};
entries * 2 + 1024
]
}
pub(crate) fn fd_is_open(fd: c_int) -> bool {
unsafe { libc::fcntl(fd, libc::F_GETFD) != -1 }
}
pub(crate) fn clear_cloexec(fd: c_int) -> Result<(), i32> {
set_fd_flags(fd, 0)
}
pub(crate) fn alive(pid: libc::pid_t) -> bool {
unsafe { libc::kill(pid, 0) == 0 || errno() == libc::EPERM }
}
fn children(pid: libc::pid_t) -> Vec<libc::pid_t> {
let mut buf = vec![0 as libc::pid_t; 256];
loop {
let bytes = c_int::try_from(std::mem::size_of_val(buf.as_slice())).unwrap_or(c_int::MAX);
let n = unsafe { libc::proc_listchildpids(pid, buf.as_mut_ptr().cast(), bytes) };
let Ok(n) = usize::try_from(n) else {
return Vec::new();
};
if n < buf.len() {
buf.truncate(n);
return buf;
}
buf.resize(buf.len() * 2, 0);
}
}
pub(crate) fn descendants(pid: libc::pid_t) -> Vec<libc::pid_t> {
let mut found: Vec<libc::pid_t> = Vec::new();
for _ in 0..8 {
let before = found.len();
let mut queue = vec![pid];
while let Some(p) = queue.pop() {
for c in children(p) {
if c > 0 && !found.contains(&c) {
found.push(c);
}
if c > 0 {
queue.push(c);
}
}
}
if found.len() == before {
break;
}
}
found
}
pub(crate) fn all_pids() -> Result<Vec<libc::pid_t>, i32> {
let mut buf = vec![0 as libc::pid_t; 1024];
loop {
let bytes = c_int::try_from(std::mem::size_of_val(buf.as_slice())).unwrap_or(c_int::MAX);
let n = unsafe { libc::proc_listallpids(buf.as_mut_ptr().cast(), bytes) };
let Ok(n) = usize::try_from(n) else {
return Err(errno());
};
if n < buf.len() {
buf.truncate(n);
return Ok(buf);
}
buf.resize(buf.len() * 2, 0);
}
}
pub(crate) fn live_of_user(pid: libc::pid_t) -> bool {
let size = c_int::try_from(std::mem::size_of::<libc::proc_bsdinfo>()).unwrap_or(c_int::MAX);
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let buf = (&raw mut info).cast();
let n = unsafe { libc::proc_pidinfo(pid, libc::PROC_PIDTBSDINFO, 0, buf, size) };
let uid = unsafe { libc::getuid() };
n == size && info.pbi_uid == uid && info.pbi_status != libc::SZOMB
}
pub(crate) fn uid() -> u32 {
unsafe { libc::getuid() }
}
pub(crate) fn kill(pid: libc::pid_t) {
unsafe { libc::kill(pid, libc::SIGKILL) };
}
pub(crate) fn killpg(pgid: libc::pid_t) {
unsafe { libc::killpg(pgid, libc::SIGKILL) };
}
pub(crate) fn on_read_only_volume(path: &CStr) -> Result<bool, i32> {
let mut fs = std::mem::MaybeUninit::<libc::statfs>::uninit();
if unsafe { libc::statfs(path.as_ptr(), fs.as_mut_ptr()) } == -1 {
return Err(errno());
}
let fs = unsafe { fs.assume_init() };
Ok(fs.f_flags & libc::MNT_RDONLY as u32 != 0)
}
pub(crate) fn spawn_sleeper(setsid_first: bool) -> std::io::Result<u32> {
use std::os::unix::process::CommandExt;
let mut command = std::process::Command::new("/bin/sleep");
command.arg("30");
if setsid_first {
unsafe {
command.pre_exec(|| setsid().map_err(std::io::Error::from_raw_os_error));
}
}
let child = command.spawn()?;
Ok(child.id())
}
impl ChildPlan {
fn run(&mut self) -> std::io::Result<()> {
let fail =
|step: u32, errno: i32| std::io::Error::from_raw_os_error(step_code(step, errno));
match self.session {
Session::New => setsid().map_err(|e| fail(steps::SESSION, e))?,
Session::Group => setpgid_self().map_err(|e| fail(steps::SESSION, e))?,
Session::Keep => {}
}
for &(resource, cur, max) in &self.limits {
setrlimit(resource, cur, max).map_err(|e| fail(steps::RLIMIT, e))?;
}
mark_fds(&mut self.fd_buffer, &self.keep).map_err(|e| fail(steps::FDS, e))?;
self.ops
.apply(&self.bytecode)
.map_err(|e| fail(steps::APPLY, e))?;
self.ops.verify().map_err(|e| fail(steps::VERIFY, e))?;
for token in &self.tokens {
self.ops
.consume(token)
.map_err(|e| fail(steps::CONSUME, e))?;
}
Ok(())
}
pub(crate) fn install(self, command: &mut std::process::Command) {
use std::os::unix::process::CommandExt;
let mut plan = self;
unsafe {
command.pre_exec(move || plan.run());
}
}
}
pub(crate) fn install_probe(
command: &mut std::process::Command,
ops: ChildOps,
bytecode: std::sync::Arc<[u8]>,
) {
use std::os::unix::process::CommandExt;
let canary = c"/private/etc/hosts";
unsafe {
command.pre_exec(move || {
let fail =
|step: u32, errno: i32| std::io::Error::from_raw_os_error(step_code(step, errno));
ops.apply(&bytecode).map_err(|e| fail(steps::APPLY, e))?;
ops.verify().map_err(|e| fail(steps::VERIFY, e))?;
match open_readonly(canary) {
Err(libc::EPERM) => Err(fail(PROBE_OK, 0)),
Err(e) => Err(fail(PROBE_CANARY, e)),
Ok(()) => Err(fail(PROBE_CANARY, 0)),
}
});
}
}
pub(crate) fn install_fence(
command: &mut std::process::Command,
ops: ChildOps,
bytecode: std::sync::Arc<[u8]>,
) {
use std::os::unix::process::CommandExt;
unsafe {
command.pre_exec(move || {
let code = match ops.apply(&bytecode) {
Ok(()) => i32::from(open_readonly(c"/").is_ok()),
Err(_) => 2,
};
libc::_exit(code)
});
}
}
pub(crate) const PROBE_OK: u32 = 0xe;
pub(crate) const PROBE_CANARY: u32 = 0xd;
pub(crate) fn dyld_churn(stop: &std::sync::atomic::AtomicBool) {
while !stop.load(std::sync::atomic::Ordering::Relaxed) {
unsafe {
let handle = libc::dlopen(c"/usr/lib/libz.1.dylib".as_ptr(), libc::RTLD_NOW);
if !handle.is_null() {
let _ = libc::dlsym(handle, c"zlibVersion".as_ptr());
libc::dlclose(handle);
}
}
}
}
unsafe extern "C" {
static bootstrap_port: libc::mach_port_t;
static mach_task_self_: libc::mach_port_t;
fn bootstrap_look_up(
bp: libc::mach_port_t,
name: *const c_char,
sp: *mut libc::mach_port_t,
) -> libc::kern_return_t;
fn mach_port_deallocate(
task: libc::mach_port_t,
name: libc::mach_port_t,
) -> libc::kern_return_t;
}
pub(crate) fn mach_lookup(name: &CStr) -> i32 {
let mut port: libc::mach_port_t = 0;
unsafe {
let kr = bootstrap_look_up(bootstrap_port, name.as_ptr(), &raw mut port);
if kr == 0 {
mach_port_deallocate(mach_task_self_, port);
}
kr
}
}
pub(crate) fn fcntl_errno(fd: c_int, cmd: c_int) -> i32 {
if unsafe { libc::fcntl(fd, cmd, 0) } == -1 {
errno()
} else {
0
}
}
pub(crate) fn procargs_errno(pid: libc::pid_t) -> i32 {
procargs_query_errno(libc::KERN_PROCARGS2, pid)
}
pub(crate) fn procargs1_errno(pid: libc::pid_t) -> i32 {
procargs_query_errno(libc::KERN_PROCARGS, pid)
}
pub(crate) fn pidinfo_errno(pid: libc::pid_t) -> i32 {
let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::uninit();
let size = c_int::try_from(std::mem::size_of::<libc::proc_bsdinfo>()).unwrap_or(c_int::MAX);
let n = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTBSDINFO,
0,
info.as_mut_ptr().cast(),
size,
)
};
if n == size {
0
} else {
errno().max(libc::EPERM)
}
}
fn procargs_query_errno(query: c_int, pid: libc::pid_t) -> i32 {
let mut mib = [libc::CTL_KERN, query, pid];
let mut buf = vec![0u8; 1 << 20];
let mut len = buf.len();
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
3,
buf.as_mut_ptr().cast(),
&raw mut len,
std::ptr::null_mut(),
0,
)
};
if rc == 0 { 0 } else { errno() }
}
pub(crate) fn kill_errno(pid: libc::pid_t, sig: c_int) -> i32 {
if unsafe { libc::kill(pid, sig) } == 0 {
0
} else {
errno()
}
}
pub(crate) fn open_pty() -> std::io::Result<(std::os::fd::OwnedFd, std::path::PathBuf)> {
use std::os::fd::FromRawFd;
use std::os::unix::ffi::OsStrExt;
let (mut controller, mut device) = (-1, -1);
let mut name = [0 as c_char; 128];
let rc = unsafe {
libc::openpty(
&raw mut controller,
&raw mut device,
name.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
let controller = unsafe {
drop(std::os::fd::OwnedFd::from_raw_fd(device));
std::os::fd::OwnedFd::from_raw_fd(controller)
};
let name = unsafe { CStr::from_ptr(name.as_ptr()) };
Ok((
controller,
std::ffi::OsStr::from_bytes(name.to_bytes()).into(),
))
}
pub(crate) fn ioctl_errno(fd: c_int, request: libc::c_ulong) -> i32 {
if unsafe { libc::ioctl(fd, request, 0) } == -1 {
errno()
} else {
0
}
}
pub(crate) fn tiocsti_errno(fd: c_int, byte: u8) -> i32 {
let c = byte as c_char;
if unsafe { libc::ioctl(fd, libc::TIOCSTI, &raw const c) } == -1 {
errno()
} else {
0
}
}