pub fn failure_cause(code: Option<i32>) -> Option<&'static str> {
match code {
Some(120) => Some("responsibility_spawnattrs_setdisclaim is unavailable"),
Some(121) => Some("responsibility_spawnattrs_setdisclaim failed"),
Some(122) => Some("privacy identity posix_spawn failed"),
Some(123) => Some("privacy identity trampoline setup failed"),
_ => None,
}
}
pub const TRAMPOLINE_PROBE: &str = "subc-privacy-trampoline/v1";
pub const EXEC_REFUSAL_TAG: &str = "SUBC_PRIVACY_REFUSAL_V1 ";
pub fn trampoline_main(args: &[std::ffi::OsString]) {
trampoline_entry(args, false);
}
#[cfg(feature = "test-support")]
pub fn trampoline_main_for_test(args: &[std::ffi::OsString]) {
trampoline_entry(args, true);
}
fn trampoline_entry(args: &[std::ffi::OsString], test_hooks: bool) {
if !args.first().is_some_and(|arg| arg == "__disclaim-exec") {
return;
}
#[cfg(target_os = "macos")]
{
if args.len() == 2 && args[1] == "--probe" {
#[cfg(feature = "test-support")]
let missing =
test_hooks && std::env::var_os("SUBC_TEST_PRIVACY_MISSING_SYMBOL").is_some();
#[cfg(not(feature = "test-support"))]
let missing = false;
match macos::resolve_disclaim(missing) {
Ok(_) => {
println!("{TRAMPOLINE_PROBE}");
std::process::exit(0);
}
Err(error) => {
eprintln!("ck-subc: own privacy identity refused: {error}");
std::process::exit(error.exit_code());
}
}
}
#[cfg(feature = "test-support")]
if test_hooks {
let error = macos::disclaim_exec_for_test(&args[1..])
.expect_err("SETEXEC cannot return on success");
eprintln!("ck-subc: own privacy identity refused: {error}");
std::process::exit(error.exit_code());
}
let _ = test_hooks;
let error = macos::disclaim_exec(&args[1..]).expect_err("SETEXEC cannot return on success");
eprintln!("ck-subc: own privacy identity refused: {error}");
std::process::exit(error.exit_code());
}
#[cfg(not(target_os = "macos"))]
{
let _ = test_hooks;
eprintln!("ck-subc: own privacy identity is only available on macOS");
std::process::exit(123);
}
}
#[cfg(target_os = "macos")]
pub use macos::{disclaim_exec, ExecAcknowledgement, ExecError};
#[cfg(all(target_os = "macos", feature = "test-support"))]
pub use macos::{disclaim_exec_for_test, privacy_observation_for_test};
#[cfg(target_os = "macos")]
mod macos {
use std::{
ffi::{CString, OsString},
fmt, io,
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::ffi::OsStrExt,
},
};
#[derive(Debug)]
pub struct ExecError {
code: i32,
detail: String,
}
impl ExecError {
pub fn exit_code(&self) -> i32 {
self.code
}
fn new(code: i32, detail: impl fmt::Display) -> Self {
Self {
code,
detail: detail.to_string(),
}
}
}
impl fmt::Display for ExecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {}",
super::failure_cause(Some(self.code)).unwrap_or("privacy identity failure"),
self.detail
)
}
}
impl std::error::Error for ExecError {}
pub struct ExecAcknowledgement {
writer: OwnedFd,
}
impl ExecAcknowledgement {
pub fn pipe() -> io::Result<(std::io::PipeReader, Self)> {
let (reader, writer) = io::pipe()?;
#[allow(unsafe_code)]
unsafe {
let flags = libc::fcntl(reader.as_raw_fd(), libc::F_GETFL);
if flags == -1
|| libc::fcntl(reader.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK)
== -1
{
return Err(io::Error::last_os_error());
}
}
#[allow(unsafe_code)]
let fd = unsafe { libc::fcntl(writer.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 4) };
if fd == -1 {
return Err(io::Error::last_os_error());
}
#[allow(unsafe_code)]
let writer = unsafe { OwnedFd::from_raw_fd(fd) };
Ok((reader, Self { writer }))
}
pub fn fd(&self) -> i32 {
self.writer.as_raw_fd()
}
pub fn install(&self, command: &mut std::process::Command) {
use std::os::unix::process::CommandExt;
let fd = self.fd();
#[allow(unsafe_code)]
unsafe {
command.pre_exec(move || {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags == -1
|| libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) == -1
{
return Err(io::Error::last_os_error());
}
Ok(())
});
}
}
}
pub fn disclaim_exec(args: &[OsString]) -> Result<(), ExecError> {
let result = exec(args, false);
if let Err(error) = &result {
write_refusal_record(args, error);
}
result
}
#[cfg(feature = "test-support")]
pub fn disclaim_exec_for_test(args: &[OsString]) -> Result<(), ExecError> {
if std::env::var_os("SUBC_TEST_PRIVACY_CLOSE_ACK_WITHOUT_EXEC").is_some() {
let fd: i32 = args[0].to_str().unwrap().parse().unwrap();
#[allow(unsafe_code)]
unsafe {
libc::close(fd);
}
std::thread::sleep(std::time::Duration::from_secs(6));
return Err(ExecError::new(123, "test trampoline did not exec"));
}
if let Ok(ms) = std::env::var("SUBC_TEST_PRIVACY_EXEC_DELAY_MS") {
std::thread::sleep(std::time::Duration::from_millis(ms.parse().unwrap()));
}
let result = exec(
args,
std::env::var_os("SUBC_TEST_PRIVACY_MISSING_SYMBOL").is_some(),
);
if let Err(error) = &result {
write_refusal_record(args, error);
}
result
}
fn write_refusal_record(args: &[OsString], error: &ExecError) {
let Some(fd) = args
.first()
.and_then(|s| s.to_str())
.and_then(|s| s.parse::<i32>().ok())
.filter(|fd| *fd >= 4)
else {
return;
};
let record = format!("{}{error}\n", super::EXEC_REFUSAL_TAG);
let bytes = record.as_bytes();
let mut written = 0;
while written < bytes.len() {
#[allow(unsafe_code)]
let count =
unsafe { libc::write(fd, bytes[written..].as_ptr().cast(), bytes.len() - written) };
if count > 0 {
written += count as usize;
} else if count == -1 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted
{
continue;
} else {
break;
}
}
}
type Disclaim = unsafe extern "C" fn(*mut libc::posix_spawnattr_t, libc::c_int) -> libc::c_int;
pub(super) fn resolve_disclaim(missing_symbol: bool) -> Result<Disclaim, ExecError> {
let symbol = if missing_symbol {
c"__subc_test_missing_responsibility_symbol"
} else {
c"responsibility_spawnattrs_setdisclaim"
};
#[allow(unsafe_code)]
unsafe {
let address = libc::dlsym(libc::RTLD_DEFAULT, symbol.as_ptr());
if address.is_null() {
return Err(ExecError::new(120, "module was not executed"));
}
Ok(std::mem::transmute::<*mut libc::c_void, Disclaim>(address))
}
}
fn exec(args: &[OsString], missing_symbol: bool) -> Result<(), ExecError> {
let disclaim = resolve_disclaim(missing_symbol)?;
let fd: i32 = args
.first()
.and_then(|s| s.to_str())
.and_then(|s| s.parse().ok())
.filter(|fd| *fd >= 4)
.ok_or_else(|| ExecError::new(123, "missing exec acknowledgement descriptor"))?;
if args.len() < 2 {
return Err(ExecError::new(123, "missing module program"));
}
let strings: Vec<CString> = args[1..]
.iter()
.map(|s| CString::new(s.as_bytes()))
.collect::<Result<_, _>>()
.map_err(|e| ExecError::new(123, e))?;
let mut argv: Vec<*mut libc::c_char> =
strings.iter().map(|s| s.as_ptr().cast_mut()).collect();
argv.push(std::ptr::null_mut());
let environment: Vec<CString> = std::env::vars_os()
.map(|(key, value)| {
let mut bytes = key.as_bytes().to_vec();
bytes.push(b'=');
bytes.extend_from_slice(value.as_bytes());
CString::new(bytes)
})
.collect::<Result<_, _>>()
.map_err(|e| ExecError::new(123, e))?;
let mut envp: Vec<*mut libc::c_char> =
environment.iter().map(|s| s.as_ptr().cast_mut()).collect();
envp.push(std::ptr::null_mut());
#[allow(unsafe_code)]
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags == -1 || libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) == -1 {
return Err(ExecError::new(123, io::Error::last_os_error()));
}
let mut attr = std::mem::MaybeUninit::<libc::posix_spawnattr_t>::uninit();
let result = libc::posix_spawnattr_init(attr.as_mut_ptr());
if result != 0 {
return Err(ExecError::new(123, io::Error::from_raw_os_error(result)));
}
let mut attr = attr.assume_init();
let result = libc::posix_spawnattr_setflags(
&mut attr,
libc::POSIX_SPAWN_SETEXEC as libc::c_short,
);
let (code, result) = if result != 0 {
(123, result)
} else {
let result = disclaim(&mut attr, 1);
if result != 0 {
(121, result)
} else {
let mut pid = 0;
(
122,
libc::posix_spawnp(
&mut pid,
strings[0].as_ptr(),
std::ptr::null(),
&attr,
argv.as_ptr(),
envp.as_ptr(),
),
)
}
};
libc::posix_spawnattr_destroy(&mut attr);
Err(ExecError::new(
code,
if result == 0 {
io::Error::other("SETEXEC unexpectedly returned")
} else {
io::Error::from_raw_os_error(result)
},
))
}
}
#[cfg(feature = "test-support")]
pub fn privacy_observation_for_test() -> io::Result<(u32, u32, u32, u32)> {
#[allow(unsafe_code)]
unsafe {
let address = libc::dlsym(
libc::RTLD_DEFAULT,
c"responsibility_get_pid_responsible_for_pid".as_ptr(),
);
if address.is_null() {
return Err(io::Error::other("responsibility probe unavailable"));
}
let probe: unsafe extern "C" fn(libc::pid_t) -> libc::pid_t =
std::mem::transmute(address);
Ok((
probe(libc::getpid()) as u32,
probe(libc::getppid()) as u32,
libc::getppid() as u32,
libc::getpgrp() as u32,
))
}
}
}