use std::ffi::CString;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use crate::orchestration::CapabilityPolicy;
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Nesting {
NotNested,
Inherit,
Unenforceable(Narrowing),
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Narrowing {
Network,
Write(PathBuf),
Read(PathBuf),
}
impl Narrowing {
pub(super) fn kind(&self) -> &'static str {
match self {
Self::Network => "network",
Self::Write(_) => "write",
Self::Read(_) => "read",
}
}
pub(super) fn describe(&self) -> String {
match self {
Self::Network => "outbound network".to_string(),
Self::Write(path) => format!("writes to {}", path.display()),
Self::Read(path) => format!("reads of {}", path.display()),
}
}
}
pub(super) fn nesting(policy: &CapabilityPolicy) -> Nesting {
if !process_is_sandboxed() {
return Nesting::NotNested;
}
if !super::policy_allows_network(policy) && !outer_denies("network-outbound", None) {
return Nesting::Unenforceable(Narrowing::Network);
}
let writable = writable_roots(policy);
for dir in write_probes(policy) {
if dir.is_dir() && !covered(&dir, &writable) && !outer_denies("file-write-data", Some(&dir))
{
return Nesting::Unenforceable(Narrowing::Write(dir));
}
}
let readable = readable_roots(policy);
let credential_dirs = read_probes()
.into_iter()
.filter(|dir| !covered(dir, &readable));
for dir in credential_dirs.chain(super::super::process_sandbox_read_deny_roots(policy)) {
if dir.exists() && !outer_denies("file-read-data", Some(&dir)) {
return Nesting::Unenforceable(Narrowing::Read(dir));
}
}
Nesting::Inherit
}
pub(super) fn report_unenforceable(narrowing: &Narrowing) {
let mut metadata = std::collections::BTreeMap::new();
metadata.insert("backend".to_string(), serde_json::json!("macos"));
metadata.insert("confinement".to_string(), serde_json::json!("refused"));
metadata.insert("narrowing".to_string(), serde_json::json!(narrowing.kind()));
metadata.insert(
"detail".to_string(),
serde_json::json!(narrowing.describe()),
);
crate::events::log_warn_meta(
"process_sandbox_nested",
"this process is already sandboxed by a profile that does not enforce this run's policy, \
so the child was not started",
metadata,
);
}
pub(super) fn report_inherited() {
static REPORTED: AtomicBool = AtomicBool::new(false);
if REPORTED.swap(true, Ordering::SeqCst) {
return;
}
let mut metadata = std::collections::BTreeMap::new();
metadata.insert("backend".to_string(), serde_json::json!("macos"));
metadata.insert("confinement".to_string(), serde_json::json!("inherited"));
crate::events::log_warn_meta(
"process_sandbox_nested",
"this process is already sandboxed, so its children run under that sandbox; it is at \
least as strict as this run's policy at every probe, and anything the policy narrows \
beyond it is not separately enforced",
metadata,
);
}
pub(super) fn process_is_sandboxed() -> bool {
unsafe { sandbox_check(libc::getpid(), std::ptr::null(), SANDBOX_FILTER_NONE) > 0 }
}
pub(super) fn outer_denies(operation: &str, path: Option<&Path>) -> bool {
let Ok(operation) = CString::new(operation) else {
return false;
};
let pid = unsafe { libc::getpid() };
let result = match path {
None => unsafe { sandbox_check(pid, operation.as_ptr(), SANDBOX_FILTER_NONE) },
Some(path) => {
let Ok(path) = CString::new(path.as_os_str().as_encoded_bytes()) else {
return false;
};
unsafe { sandbox_check(pid, operation.as_ptr(), SANDBOX_FILTER_PATH, path.as_ptr()) }
}
};
result == 1
}
const SANDBOX_FILTER_NONE: libc::c_int = 0;
const SANDBOX_FILTER_PATH: libc::c_int = 1;
unsafe extern "C" {
fn sandbox_check(
pid: libc::pid_t,
operation: *const libc::c_char,
filter: libc::c_int,
...
) -> libc::c_int;
}
fn write_probes(policy: &CapabilityPolicy) -> Vec<PathBuf> {
let mut probes: Vec<PathBuf> = crate::user_dirs::home_dir().into_iter().collect();
probes.extend(
super::process_sandbox_roots(policy)
.iter()
.filter_map(|root| root.parent().map(Path::to_path_buf)),
);
probes
}
fn read_probes() -> Vec<PathBuf> {
let Some(home) = crate::user_dirs::home_dir() else {
return Vec::new();
};
[".ssh", ".aws", ".gnupg"]
.iter()
.map(|name| home.join(name))
.collect()
}
fn writable_roots(policy: &CapabilityPolicy) -> Vec<PathBuf> {
let mut roots = Vec::new();
if super::policy_allows_child_writes(policy) {
roots.extend(super::process_sandbox_roots(policy));
roots.extend(super::process_sandbox_policy_write_roots(policy));
roots.extend(super::preset_write_roots(policy).iter().map(PathBuf::from));
roots.extend(super::super::process_sandbox_developer_toolchain_cache_roots(policy));
}
roots
}
fn readable_roots(policy: &CapabilityPolicy) -> Vec<PathBuf> {
let mut roots = writable_roots(policy);
roots.extend(super::process_sandbox_roots(policy));
roots.extend(super::preset_read_roots(policy).iter().map(PathBuf::from));
roots.extend(super::process_sandbox_developer_toolchain_read_roots(
policy,
));
roots.extend(super::process_sandbox_readonly_roots(policy));
roots.extend(super::process_sandbox_policy_read_roots(policy));
roots.extend(super::process_sandbox_package_manager_config_read_roots(
policy,
));
roots
}
fn covered(path: &Path, roots: &[PathBuf]) -> bool {
roots.iter().any(|root| path.starts_with(root))
}
#[cfg(test)]
#[path = "nested_tests.rs"]
mod tests;