mod canon;
mod child;
mod lifetime;
mod probe;
mod spawn;
mod sys;
pub(crate) use sys::{open_terminal, set_window};
mod violations;
use std::collections::HashMap;
use std::fmt;
use std::io;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex, OnceLock};
use procage_policy::lower::GrantSlot;
use procage_policy::{
LowerError, LowerOptions, Plan, Policy, Relax, Relaxed, SeatbeltPlan, Target, Tty, lift, lower,
};
pub use crate::unix::TMPDIR_GRANT;
pub use crate::violation::{Coverage, DeniedBy, Violation, Violations};
pub use child::Child;
pub use probe::{ApplyMethod, probe};
pub fn end_session(pid: u32) {
let Ok(sid) = libc::pid_t::try_from(pid) else {
return;
};
if sid <= 1 {
return;
}
let mut killed = Vec::new();
for _ in 0..64 {
let members: Vec<libc::pid_t> = sys::all_pids()
.unwrap_or_default()
.into_iter()
.filter(|&p| {
p > 0
&& !killed.contains(&p)
&& sys::session_of(p) == Some(sid)
&& sys::live_of_user(p)
})
.collect();
if members.is_empty() {
break;
}
for member in members {
sys::kill(member);
killed.push(member);
}
}
sys::killpg(sid);
}
use crate::unix::TmpdirGuard;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Step {
Session,
Rlimit,
Apply,
Verify,
Consume,
Fds,
}
impl Step {
fn from_code(code: u32) -> Option<Step> {
Some(match code {
sys::steps::SESSION => Step::Session,
sys::steps::RLIMIT => Step::Rlimit,
sys::steps::APPLY => Step::Apply,
sys::steps::VERIFY => Step::Verify,
sys::steps::CONSUME => Step::Consume,
sys::steps::FDS => Step::Fds,
_ => return None,
})
}
}
impl fmt::Display for Step {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Step::Session => "new session",
Step::Rlimit => "resource limits",
Step::Apply => "apply profile",
Step::Verify => "verify sandbox",
Step::Consume => "consume grant token",
Step::Fds => "close inherited descriptors",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unsupported {
Symbol {
symbol: &'static str,
os_build: String,
},
ApplyMethod {
os_build: String,
detail: String,
},
Setting {
setting: &'static str,
},
RunMarker {
os_build: String,
detail: String,
},
EnvClear,
}
impl fmt::Display for Unsupported {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unsupported::Symbol { symbol, os_build } => {
write!(
f,
"libsandbox symbol `{symbol}` is missing on macOS build {os_build}"
)
}
Unsupported::ApplyMethod { os_build, detail } => {
write!(
f,
"the in-child apply method failed its probe on macOS build {os_build}: {detail}"
)
}
Unsupported::Setting { setting } => {
write!(f, "{setting} is not supported in this release")
}
Unsupported::EnvClear => write!(
f,
"EnvPolicy::Passthrough needs to see whether Command::env_clear was called, \
which this Rust standard library does not show; use EnvPolicy::Allowlist"
),
Unsupported::RunMarker { os_build, detail } => {
write!(
f,
"the run-marker query failed its probe on macOS build {os_build}: {detail}"
)
}
}
}
}
#[derive(Debug)]
pub enum GrantError {
Undeclared {
name: String,
},
Missing {
name: String,
path: PathBuf,
error: io::Error,
},
Reserved {
name: String,
},
TmpdirWithExec,
Issue {
name: String,
path: String,
},
}
impl fmt::Display for GrantError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GrantError::Undeclared { name } => {
write!(f, "the policy declares no grant named {name:?}")
}
GrantError::Missing { name, path, error } => {
write!(
f,
"grant {name}: cannot resolve {}: {error}",
path.display()
)
}
GrantError::TmpdirWithExec => write!(
f,
"the tmpdir grant needs a handle that removes the directory, and Cage::exec \
returns none: use Cage::spawn, or bind a directory of your own to another grant"
),
GrantError::Reserved { name } => {
write!(
f,
"grant {name} is bound by procage itself (a private temporary directory)"
)
}
GrantError::Issue { name, path } => {
write!(f, "grant {name}: no extension token issued for {path}")
}
}
}
}
#[cfg(feature = "tokio")]
pub use crate::async_child::AsyncChild;
#[derive(Debug)]
#[non_exhaustive]
pub enum SpawnError {
GroupLeader,
Unsupported(Unsupported),
AlreadySandboxed,
Lower(LowerError),
Path {
path: String,
error: io::Error,
},
CaseSensitive {
path: String,
},
Compile {
message: String,
},
LimitExceeded {
limit: usize,
rules: usize,
},
Grant(GrantError),
Fd {
fd: i32,
},
Tmpdir(io::Error),
Apply {
step: Step,
errno: i32,
},
Io(io::Error),
HardLinks(Vec<crate::LinkFinding>),
Explain(procage_policy::explain::ExplainError),
#[cfg(feature = "tokio")]
NoRuntime,
}
impl fmt::Display for SpawnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpawnError::GroupLeader => f.write_str(
"Cage::exec_session in a process group leader that leads no session: \
start the helper as a plain child, or in a session of its own",
),
SpawnError::Unsupported(u) => write!(f, "unsupported: {u}"),
SpawnError::AlreadySandboxed => f.write_str("the calling process is already sandboxed"),
SpawnError::Lower(e) => write!(f, "{e}"),
SpawnError::Path { path, error } => write!(f, "cannot resolve {path}: {error}"),
SpawnError::CaseSensitive { path } => write!(
f,
"{path} is on a case-sensitive volume, where Seatbelt would also allow \
its case variants: allow rules and grants there are refused"
),
SpawnError::Compile { message } => {
write!(f, "libsandbox refused the profile: {message}")
}
SpawnError::LimitExceeded { limit, rules } => {
write!(
f,
"the profile has {rules} rules, more than the in-process compile limit of {limit}"
)
}
SpawnError::Grant(e) => write!(f, "{e}"),
SpawnError::Fd { fd } => write!(f, "descriptor {fd} handed to the spawn is not open"),
SpawnError::Tmpdir(e) => {
write!(f, "cannot create the private temporary directory: {e}")
}
SpawnError::Apply { step, errno } => write!(
f,
"the child failed at step `{step}`: {}; the target did not run",
io::Error::from_raw_os_error(*errno)
),
SpawnError::Io(e) => write!(f, "{e}"),
SpawnError::HardLinks(found) => f.write_str(&crate::audit::refusal(found)),
SpawnError::Explain(e) => write!(f, "{e}"),
#[cfg(feature = "tokio")]
SpawnError::NoRuntime => {
f.write_str("spawn_async needs a tokio runtime on the calling thread")
}
}
}
}
impl std::error::Error for SpawnError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
SpawnError::Lower(e) => Some(e),
SpawnError::Path { error, .. } | SpawnError::Io(error) | SpawnError::Tmpdir(error) => {
Some(error)
}
SpawnError::Grant(GrantError::Missing { error, .. }) => Some(error),
_ => None,
}
}
}
impl From<LowerError> for SpawnError {
fn from(e: LowerError) -> Self {
SpawnError::Lower(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PathRewrite {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CageOptions {
pub lower: LowerOptions,
pub compile_limit: usize,
pub violations: bool,
pub policy_in_receipt: bool,
pub link_audit: crate::LinkAudit,
pub link_exempt: Vec<PathBuf>,
pub handoff_audit: bool,
pub walk_limit: usize,
#[cfg(feature = "proxy")]
pub proxy: Option<procage_proxy::Proxy>,
}
impl Default for CageOptions {
fn default() -> Self {
Self {
lower: LowerOptions::default(),
compile_limit: 2000,
violations: false,
policy_in_receipt: false,
link_audit: crate::LinkAudit::Refuse,
link_exempt: Vec::new(),
handoff_audit: false,
walk_limit: 200_000,
#[cfg(feature = "proxy")]
proxy: None,
}
}
}
impl CageOptions {
pub fn with_lower(mut self, lower: LowerOptions) -> Self {
self.lower = lower;
self
}
pub fn with_compile_limit(mut self, limit: usize) -> Self {
self.compile_limit = limit;
self
}
pub fn with_link_audit(mut self, mode: crate::LinkAudit) -> Self {
self.link_audit = mode;
self
}
pub fn with_link_exemption(mut self, path: impl AsRef<Path>) -> Self {
self.link_exempt.push(path.as_ref().to_path_buf());
self
}
pub fn with_handoff_audit(mut self) -> Self {
self.handoff_audit = true;
self
}
pub fn with_walk_limit(mut self, limit: usize) -> Self {
self.walk_limit = limit;
self
}
pub fn with_policy_in_receipt(mut self) -> Self {
self.policy_in_receipt = true;
self
}
pub fn with_violations(mut self) -> Self {
self.violations = true;
self
}
#[cfg(feature = "proxy")]
pub fn with_proxy(mut self, proxy: procage_proxy::Proxy) -> Self {
self.proxy = Some(proxy);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Grants {
bindings: Vec<(String, PathBuf)>,
}
impl Grants {
pub fn new() -> Self {
Self::default()
}
pub fn bind(mut self, name: &str, path: impl AsRef<Path>) -> Self {
self.bindings
.push((name.to_string(), path.as_ref().to_path_buf()));
self
}
}
type CacheKey = (String, Vec<(String, String)>);
fn cache() -> &'static Mutex<HashMap<CacheKey, Arc<[u8]>>> {
static CACHE: OnceLock<Mutex<HashMap<CacheKey, Arc<[u8]>>>> = OnceLock::new();
CACHE.get_or_init(Default::default)
}
fn compiled(
lib: &sys::Libsandbox,
plan: &SeatbeltPlan,
marker: &[(String, String)],
overrides: &[(String, String)],
) -> Result<(Arc<[u8]>, bool), SpawnError> {
let mut params = plan.params.clone();
params.extend_from_slice(marker);
for (name, value) in overrides {
match params.iter_mut().find(|(n, _)| n == name) {
Some(param) => param.1.clone_from(value),
None => params.push((name.clone(), value.clone())),
}
}
let key = (plan.profile.clone(), params);
let cached = cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&key)
.cloned();
if let Some(bytecode) = cached {
return Ok((bytecode, false));
}
let bytecode: Arc<[u8]> = lib
.compile(&plan.profile, &key.1)
.map_err(|message| SpawnError::Compile {
message: message.unwrap_or_else(|| "<no error text>".into()),
})?
.into();
cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(key, bytecode.clone());
Ok((bytecode, true))
}
fn libsandbox() -> Result<&'static sys::Libsandbox, SpawnError> {
sys::Libsandbox::get().map_err(|symbol| {
SpawnError::Unsupported(Unsupported::Symbol {
symbol,
os_build: sys::os_build(),
})
})
}
pub fn is_sandboxed() -> Result<bool, SpawnError> {
Ok(libsandbox()?.is_sandboxed())
}
#[derive(Debug, Clone)]
pub struct Cage {
plan: SeatbeltPlan,
bytecode: Arc<[u8]>,
rewrites: Vec<PathRewrite>,
compiled: bool,
violations: bool,
facts: Arc<crate::receipt::CageFacts>,
audit: Arc<crate::audit::CageAudit>,
explainer: Arc<crate::explain::Lazy>,
#[cfg(feature = "proxy")]
proxied: Option<crate::proxied::Proxied>,
}
impl Cage {
pub fn new(policy: &Policy) -> Result<Cage, SpawnError> {
Cage::with_options(policy, &CageOptions::default())
}
pub fn fit(
policy: &Policy,
options: &CageOptions,
relax: &Relax,
) -> Result<(Cage, Vec<Relaxed>), SpawnError> {
let mut fitted = policy.clone();
let mut relaxed = Vec::new();
for _ in 0..4 {
let gaps = match Cage::with_options(&fitted, options) {
Ok(cage) => return Ok(cage.relaxed(&fitted, options, relaxed)),
Err(SpawnError::Lower(LowerError::Gaps(gaps))) => gaps,
Err(e) => return Err(e),
};
let done = relax
.apply(&mut fitted, &gaps)
.map_err(|rest| SpawnError::Lower(LowerError::Gaps(rest)))?;
relaxed.extend(done.into_iter().map(|(gap, relaxation)| Relaxed {
lift: lift(policy, &gap, &options.lower, 0),
gap,
relaxation,
}));
}
Cage::with_options(&fitted, options).map(|cage| cage.relaxed(&fitted, options, relaxed))
}
fn relaxed(
mut self,
fitted: &Policy,
options: &CageOptions,
relaxed: Vec<Relaxed>,
) -> (Cage, Vec<Relaxed>) {
self.facts = Arc::new(facts(fitted, options, &self.plan, &relaxed));
(self, relaxed)
}
pub fn with_options(policy: &Policy, options: &CageOptions) -> Result<Cage, SpawnError> {
let lib = libsandbox()?;
if lib.is_sandboxed() {
return Err(SpawnError::AlreadySandboxed);
}
#[cfg(feature = "proxy")]
let proxied = match &options.proxy {
None => None,
Some(proxy) => Some(crate::proxied::Proxied::new(proxy, &policy.net).ok_or(
SpawnError::Unsupported(Unsupported::Setting {
setting: "CageOptions::with_proxy without Network::Proxy",
}),
)?),
};
let (resolved, rewrites) = canon::resolve_policy(policy)?;
let violations = options.violations || options.lower.violations;
let mut lower_options = options.lower.clone();
if violations {
lower_options = lower_options.with_violations();
}
let Plan::Seatbelt(plan) = lower(&resolved, Target::MacOs, &lower_options)? else {
unreachable!("lowering for macOS yields a Seatbelt plan");
};
if plan.rule_count > options.compile_limit {
return Err(SpawnError::LimitExceeded {
limit: options.compile_limit,
rules: plan.rule_count,
});
}
let marker = if plan.run_marker || violations {
lifetime::first_params().to_vec()
} else {
Vec::new()
};
let (bytecode, compiled) = compiled(lib, &plan, &marker, &[])?;
let facts = Arc::new(facts(policy, options, &plan, &[]));
let audit = Arc::new(cage_audit(&resolved, options)?);
let explainer = Arc::new(crate::explain::Lazy::new(
resolved,
Target::MacOs,
lower_options,
));
Ok(Cage {
plan,
bytecode,
rewrites,
compiled,
violations,
facts,
audit,
explainer,
#[cfg(feature = "proxy")]
proxied,
})
}
pub fn explain(
&self,
op: procage_policy::explain::Op,
path: impl AsRef<Path>,
grants: &Grants,
) -> Result<procage_policy::explain::Answer, SpawnError> {
let path = path.as_ref();
let question = crate::explain::question(op, path).map_err(|error| SpawnError::Path {
path: path.display().to_string(),
error,
})?;
let mut bound = Vec::new();
for (name, p) in &grants.bindings {
if self.grant_slot(name).is_none() {
return Err(SpawnError::Grant(GrantError::Undeclared {
name: name.clone(),
}));
}
let real = canon::resolve_existing(p).map_err(|error| {
SpawnError::Grant(GrantError::Missing {
name: name.clone(),
path: p.clone(),
error,
})
})?;
let real: &std::ffi::OsStr = std::os::unix::ffi::OsStrExt::from_bytes(real.as_bytes());
bound.push((name.clone(), PathBuf::from(real)));
}
let context = crate::explain::context(bound, &[]).map_err(SpawnError::Io)?;
self.explainer
.get()?
.explain(&question, &context)
.map_err(SpawnError::Explain)
}
pub fn plan(&self) -> &SeatbeltPlan {
&self.plan
}
pub fn rewrites(&self) -> &[PathRewrite] {
&self.rewrites
}
pub fn compiled(&self) -> bool {
self.compiled
}
fn grant_slot(&self, name: &str) -> Option<&GrantSlot> {
self.plan.grants.iter().find(|g| g.name.as_str() == name)
}
pub fn spawn(&self, command: &mut Command, grants: &Grants) -> Result<Child, SpawnError> {
self.spawn_with_fds(command, grants, &[])
}
pub fn spawn_with_fds(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
) -> Result<Child, SpawnError> {
let launch = self.launch(command, grants, fds)?;
let spawned = command.spawn();
launch.release(command);
match spawned {
Ok(inner) => Ok(self.adopt(launch, crate::unix::Proc::Std(inner))),
Err(e) => Err(launch.abandon(e)),
}
}
pub(crate) fn launch(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
) -> Result<Launch, SpawnError> {
let session = match self.plan.hygiene.tty {
Tty::Inherit => sys::Session::Group,
Tty::NewPty => sys::Session::Terminal,
Tty::None => sys::Session::New,
};
#[cfg(feature = "proxy")]
let (listeners, overrides) = self.proxy_listeners(command)?;
#[cfg(not(feature = "proxy"))]
let overrides = Vec::new();
let prepared = self.prepare(command, grants, fds, session, &overrides)?;
let run_audit = self.audit.before_spawn().map_err(SpawnError::HardLinks)?;
let report = match &prepared.run {
Some(run) if self.violations => Some(violations::register(run.marker())),
_ => None,
};
let names = grants.bindings.iter().map(|(n, _)| n.clone()).collect();
Ok(Launch {
prepared,
run_audit,
report,
names,
#[cfg(feature = "proxy")]
listeners,
})
}
#[cfg(feature = "proxy")]
#[allow(clippy::type_complexity)]
fn proxy_listeners(
&self,
command: &mut Command,
) -> Result<
(
Vec<(procage_proxy::Protocol, std::net::TcpListener)>,
Vec<(String, String)>,
),
SpawnError,
> {
let Some(proxied) = &self.proxied else {
return Ok((Vec::new(), Vec::new()));
};
let listeners = proxied.bind().map_err(SpawnError::Io)?;
let mut addrs = Vec::new();
let mut overrides = Vec::new();
for (protocol, listener) in &listeners {
let addr = listener.local_addr().map_err(SpawnError::Io)?;
let param = match protocol {
procage_proxy::Protocol::Http => procage_policy::PROXY_HTTP_PARAM,
procage_proxy::Protocol::Socks5 => procage_policy::PROXY_SOCKS_PARAM,
};
overrides.push((param.to_string(), format!("localhost:{}", addr.port())));
addrs.push((*protocol, addr));
}
crate::proxied::set_env(command, &addrs);
Ok((listeners, overrides))
}
pub(crate) fn adopt(&self, launch: Launch, inner: crate::unix::Proc) -> Child {
let Launch {
prepared,
run_audit,
report,
mut names,
#[cfg(feature = "proxy")]
listeners,
} = launch;
let tmpdir = prepared.tmpdir.disarm();
if tmpdir.is_some() {
names.push(TMPDIR_GRANT.into());
}
#[cfg(feature = "proxy")]
let proxy_run = self
.proxied
.as_ref()
.and_then(|p| p.serve(listeners, report.clone()));
let mut child = Child::new(
inner,
prepared.dropped,
tmpdir,
prepared.run,
child::Reporting {
forking: self.plan.run_marker,
record: report,
},
crate::receipt::RunFacts::new(self.facts.clone(), names),
run_audit,
);
#[cfg(feature = "proxy")]
child.set_proxy(proxy_run);
child.set_pty(prepared.pty);
child
}
pub fn exec(&self, command: &mut Command, grants: &Grants) -> SpawnError {
self.exec_with_fds(command, grants, &[])
}
pub fn exec_with_fds(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
) -> SpawnError {
self.exec_as(command, grants, fds, sys::Session::Keep)
}
pub fn exec_session(&self, command: &mut Command, grants: &Grants) -> SpawnError {
self.exec_session_with_fds(command, grants, &[])
}
pub fn exec_session_with_fds(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
) -> SpawnError {
if self.plan.hygiene.tty == Tty::Inherit {
return SpawnError::Unsupported(Unsupported::Setting {
setting: "Cage::exec_session with Tty::Inherit",
});
}
if let (false, true) = sys::leads() {
return SpawnError::GroupLeader;
}
self.exec_as(command, grants, fds, sys::Session::Lead)
}
fn exec_as(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
session: sys::Session,
) -> SpawnError {
use std::os::unix::process::CommandExt;
if self.plan.hygiene.tty == Tty::NewPty {
return SpawnError::Unsupported(Unsupported::Setting {
setting: "Cage::exec with Tty::NewPty",
});
}
if self.grant_slot(TMPDIR_GRANT).is_some() {
return SpawnError::Grant(GrantError::TmpdirWithExec);
}
#[cfg(feature = "proxy")]
if self.proxied.is_some() {
return SpawnError::Unsupported(Unsupported::Setting {
setting: "Cage::exec with CageOptions::with_proxy",
});
}
match self.prepare(command, grants, fds, session, &[]) {
Ok(_prepared) => match self.audit.before_spawn() {
Ok(_) => spawn::decode(command.exec()),
Err(found) => SpawnError::HardLinks(found),
},
Err(e) => e,
}
}
fn prepare(
&self,
command: &mut Command,
grants: &Grants,
fds: &[BorrowedFd<'_>],
session: sys::Session,
overrides: &[(String, String)],
) -> Result<Prepared, SpawnError> {
let lib = libsandbox()?;
if lib.is_sandboxed() {
return Err(SpawnError::AlreadySandboxed);
}
probe()?;
let keep = kept_fds(fds.iter().map(AsRawFd::as_raw_fd))?;
if grants.bindings.iter().any(|(name, _)| name == TMPDIR_GRANT) {
return Err(SpawnError::Grant(GrantError::Reserved {
name: TMPDIR_GRANT.into(),
}));
}
let tmpdir = match self.grant_slot(TMPDIR_GRANT) {
Some(_) => Some(crate::unix::private_tmpdir().map_err(SpawnError::Tmpdir)?),
None => None,
};
let removal = TmpdirGuard(tmpdir.clone());
let mut grants = grants.clone();
if let Some(dir) = &tmpdir {
grants = grants.bind(TMPDIR_GRANT, dir);
if !command.get_envs().any(|(k, _)| k == "TMPDIR") {
command.env("TMPDIR", dir);
}
}
let mut tokens = spawn::tokens(lib, self, &grants)?;
let pty = if session == sys::Session::Terminal {
let (pty, device) = crate::Pty::open().map_err(SpawnError::Io)?;
tokens.push(spawn::terminal_token(lib, pty.device())?);
crate::unix::terminal_stdio(command, &device).map_err(SpawnError::Io)?;
Some(pty)
} else {
None
};
let dropped = crate::unix::environment(command, &self.plan.hygiene.env)
.map_err(|_| SpawnError::Unsupported(Unsupported::EnvClear))?;
if matches!(session, sys::Session::Keep | sys::Session::Lead) {
crate::unix::find_program(command).map_err(SpawnError::Io)?;
}
let (run, bytecode) = self.run_and_bytecode(lib, overrides)?;
sys::ChildPlan {
ops: lib.child_ops(),
bytecode,
tokens,
limits: crate::unix::limits(&self.plan.hygiene.limits),
session,
keep,
fd_buffer: sys::fd_buffer(),
}
.install(command);
Ok(Prepared {
dropped,
tmpdir: removal,
run,
pty,
})
}
fn run_and_bytecode(
&self,
lib: &sys::Libsandbox,
overrides: &[(String, String)],
) -> Result<(Option<lifetime::Run>, Arc<[u8]>), SpawnError> {
if !self.plan.run_marker && !self.violations {
if overrides.is_empty() {
return Ok((None, self.bytecode.clone()));
}
return Ok((None, compiled(lib, &self.plan, &[], overrides)?.0));
}
if self.plan.run_marker {
lifetime::probe(lib)?;
}
let run = lifetime::Run::take();
if run.is_first() && overrides.is_empty() {
return Ok((Some(run), self.bytecode.clone()));
}
match compiled(lib, &self.plan, &run.params(), overrides) {
Ok((bytecode, _)) => Ok((Some(run), bytecode)),
Err(e) => {
run.free();
Err(e)
}
}
}
}
pub(crate) struct Launch {
prepared: Prepared,
run_audit: crate::audit::RunAudit,
report: Option<violations::Sink>,
names: Vec<String>,
#[cfg(feature = "proxy")]
listeners: Vec<(procage_proxy::Protocol, std::net::TcpListener)>,
}
impl Launch {
pub(crate) fn release(&self, command: &mut Command) {
if self.prepared.pty.is_some() {
crate::unix::release_stdio(command);
}
}
pub(crate) fn abandon(self, e: std::io::Error) -> SpawnError {
if let Some(run) = &self.prepared.run {
violations::unregister(run.marker());
}
spawn::decode(e)
}
}
struct Prepared {
dropped: Vec<String>,
tmpdir: TmpdirGuard,
run: Option<lifetime::Run>,
pty: Option<crate::Pty>,
}
fn kept_fds(fds: impl IntoIterator<Item = i32>) -> Result<Vec<i32>, SpawnError> {
let keep: Vec<i32> = fds.into_iter().filter(|&fd| fd > 2).collect();
match keep.iter().find(|&&fd| !sys::fd_is_open(fd)) {
Some(&fd) => Err(SpawnError::Fd { fd }),
None => Ok(keep),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_descriptor_that_is_not_open_is_refused() {
let closed = 1_000_000;
assert!(matches!(kept_fds([closed]), Err(SpawnError::Fd { fd }) if fd == closed));
assert_eq!(kept_fds([0, 1, 2]).unwrap(), Vec::<i32>::new());
let file = std::fs::File::open("/").unwrap();
let fd = file.as_raw_fd();
assert_eq!(kept_fds([fd]).unwrap(), vec![fd]);
}
}
#[doc(hidden)]
pub mod testing {
use super::{SpawnError, Unsupported, sys};
pub fn resolve_symbol(symbol: &'static str) -> Result<(), SpawnError> {
if sys::has_symbol(symbol) {
Ok(())
} else {
Err(SpawnError::Unsupported(Unsupported::Symbol {
symbol,
os_build: sys::os_build(),
}))
}
}
pub fn stop_violation_monitor() {
super::violations::stop();
}
pub fn run_members(child: &super::Child) -> std::io::Result<Vec<u32>> {
let Some(run) = child.run() else {
return Ok(Vec::new());
};
let lib = super::libsandbox().map_err(std::io::Error::other)?;
let pids = super::lifetime::members(lib, run)?;
Ok(pids.into_iter().map(i32::cast_unsigned).collect())
}
pub fn run_slot(child: &super::Child) -> Option<u32> {
child.run().map(super::lifetime::Run::slot)
}
pub fn is_session_leader() -> bool {
sys::is_session_leader()
}
pub fn is_group_leader() -> bool {
sys::is_group_leader()
}
pub fn dyld_churn(stop: &std::sync::atomic::AtomicBool) {
sys::dyld_churn(stop);
}
pub fn clear_cloexec(fd: i32) -> bool {
sys::clear_cloexec(fd).is_ok()
}
pub fn fd_is_open(fd: i32) -> bool {
sys::fd_is_open(fd)
}
pub fn alive(pid: u32) -> bool {
libc::pid_t::try_from(pid).is_ok_and(sys::alive)
}
pub fn spawn_sleeper(setsid: bool) -> std::io::Result<u32> {
sys::spawn_sleeper(setsid)
}
pub fn nofile_and_core() -> Option<(u64, u64)> {
Some((
sys::getrlimit(libc::RLIMIT_NOFILE)?.0,
sys::getrlimit(libc::RLIMIT_CORE)?.0,
))
}
pub fn mach_lookup(name: &str) -> i32 {
std::ffi::CString::new(name).map_or(libc::EINVAL, |c| sys::mach_lookup(&c))
}
pub fn fcntl_errno(fd: i32, cmd: i32) -> i32 {
sys::fcntl_errno(fd, cmd)
}
pub fn procargs_errno(pid: u32) -> i32 {
libc::pid_t::try_from(pid).map_or(libc::EINVAL, sys::procargs_errno)
}
pub fn procargs1_errno(pid: u32) -> i32 {
libc::pid_t::try_from(pid).map_or(libc::EINVAL, sys::procargs1_errno)
}
pub fn pidinfo_errno(pid: u32) -> i32 {
libc::pid_t::try_from(pid).map_or(libc::EINVAL, sys::pidinfo_errno)
}
pub fn kill_errno(pid: u32, sig: i32) -> i32 {
libc::pid_t::try_from(pid).map_or(libc::EINVAL, |p| sys::kill_errno(p, sig))
}
pub fn open_pty() -> std::io::Result<(std::os::fd::OwnedFd, std::path::PathBuf)> {
sys::open_pty()
}
pub fn make_controlling(fd: i32) -> i32 {
sys::ioctl_errno(fd, libc::TIOCSCTTY.into())
}
pub fn tiocsti_errno(fd: i32, byte: u8) -> i32 {
sys::tiocsti_errno(fd, byte)
}
}
fn facts(
policy: &Policy,
options: &CageOptions,
plan: &SeatbeltPlan,
relaxed: &[Relaxed],
) -> crate::receipt::CageFacts {
crate::receipt::CageFacts::new(
policy,
options.policy_in_receipt,
crate::ReceiptBackend::MacOs {
apply: "path-a".into(),
os_release: sys::os_release(),
os_build: sys::os_build(),
},
crate::receipt::approximations(&plan.degradations),
relaxed,
)
}
fn cage_audit(
policy: &Policy,
options: &CageOptions,
) -> Result<crate::audit::CageAudit, SpawnError> {
use procage_policy::{HardDenyClass, presets};
use std::os::unix::ffi::OsStrExt;
let homes: Vec<PathBuf> = std::fs::read_dir("/Users")
.map(|e| {
e.flatten()
.filter(|e| e.file_type().is_ok_and(|t| t.is_dir()))
.map(|e| e.path())
.collect()
})
.unwrap_or_default();
let mut class_paths = Vec::new();
if !policy.waive.contains(&HardDenyClass::Secrets) {
class_paths.extend(crate::audit::class_paths(
&homes,
presets::SECRETS_HOME,
&[],
true,
));
}
if !policy.waive.contains(&HardDenyClass::TrustHandoff) {
class_paths.extend(crate::audit::class_paths(
&homes,
presets::HANDOFF_HOME,
&[],
false,
));
}
if !policy.waive.contains(&HardDenyClass::Persistence) {
let system: Vec<PathBuf> = presets::PERSISTENCE_SYSTEM
.iter()
.map(PathBuf::from)
.collect();
class_paths.extend(crate::audit::class_paths(
&homes,
presets::PERSISTENCE_HOME,
&system,
false,
));
}
let exempt = crate::audit::exemptions(&options.link_exempt, |p| {
canon::resolve_existing(p)
.ok()
.map(|a| PathBuf::from(std::ffi::OsStr::from_bytes(a.as_bytes())))
})
.map_err(|setting| SpawnError::Unsupported(Unsupported::Setting { setting }))?;
let targets = crate::audit::Targets::new(policy, class_paths, sys::uid()).exempting(exempt);
let handoff = options.handoff_audit.then(|| {
let trees = policy
.fs
.iter()
.filter(|r| r.access == procage_policy::Access::Write)
.filter_map(|r| match &r.target {
procage_policy::FsTarget::Subpath(p) => {
Some(PathBuf::from(std::ffi::OsStr::from_bytes(p.as_bytes())))
}
_ => None,
})
.collect();
let home = homes
.iter()
.flat_map(|h| presets::HANDOFF_HOME.iter().map(move |r| h.join(r)))
.collect();
(
trees,
presets::HANDOFF_ANYWHERE
.iter()
.map(|n| n.to_string())
.collect(),
home,
)
});
Ok(crate::audit::CageAudit::new(
options.link_audit,
targets,
handoff,
options.walk_limit,
))
}