use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use super::{PROCESS_COUNT_EXIT, SANDBOX_SETUP_EXIT, WORKER_BINARY};
#[derive(Debug)]
struct Refusal {
detail: String,
exit: i32,
}
impl From<String> for Refusal {
fn from(detail: String) -> Refusal {
Refusal {
detail,
exit: SANDBOX_SETUP_EXIT,
}
}
}
pub struct HelperConfig {
pub jail: PathBuf,
pub worker: PathBuf,
pub adapter: String,
pub address_space_bytes: Option<u64>,
pub cpu_seconds: u64,
pub file_size_bytes: u64,
pub max_processes: u64,
pub seccomp: bool,
pub exec_grants: Vec<PathBuf>,
pub read_grants: Vec<PathBuf>,
}
pub fn parse_args(args: &[String]) -> Result<HelperConfig, String> {
let mut jail = None;
let mut worker = None;
let mut adapter = None;
let mut address_space_bytes: Option<Option<u64>> = None;
let mut cpu_seconds = None;
let mut file_size_bytes = None;
let mut max_processes = None;
let mut seccomp = None;
let mut exec_grants = Vec::new();
let mut read_grants = Vec::new();
let mut iter = args.iter();
while let Some(flag) = iter.next() {
let value = iter
.next()
.ok_or_else(|| "a helper flag is missing its value".to_string())?;
match flag.as_str() {
"--jail" => jail = Some(PathBuf::from(value)),
"--worker" => worker = Some(PathBuf::from(value)),
"--adapter" => adapter = Some(value.clone()),
"--rlimit-as" => {
address_space_bytes = Some(if value == "none" {
None
} else {
Some(parse_number(flag, value)?)
})
}
"--rlimit-cpu" => cpu_seconds = Some(parse_number(flag, value)?),
"--rlimit-fsize" => file_size_bytes = Some(parse_number(flag, value)?),
"--rlimit-nproc" => max_processes = Some(parse_number(flag, value)?),
"--seccomp" => {
seccomp = Some(match value.as_str() {
"kill" => true,
"off" => false,
_ => return Err("unknown seccomp mode".to_string()),
})
}
"--grant-exec" => exec_grants.push(PathBuf::from(value)),
"--grant-read" => read_grants.push(PathBuf::from(value)),
_ => return Err("unknown helper flag".to_string()),
}
}
Ok(HelperConfig {
jail: jail.ok_or("missing --jail")?,
worker: worker.ok_or("missing --worker")?,
adapter: adapter.ok_or("missing --adapter")?,
address_space_bytes: address_space_bytes.ok_or("missing --rlimit-as")?,
cpu_seconds: cpu_seconds.ok_or("missing --rlimit-cpu")?,
file_size_bytes: file_size_bytes.ok_or("missing --rlimit-fsize")?,
max_processes: max_processes.ok_or("missing --rlimit-nproc")?,
seccomp: seccomp.ok_or("missing --seccomp")?,
exec_grants,
read_grants,
})
}
fn parse_number(flag: &str, value: &str) -> Result<u64, String> {
value
.parse()
.map_err(|_| format!("flag {flag} does not carry a number"))
}
pub fn helper_main(args: &[String]) -> ! {
let config = match parse_args(args) {
Ok(config) => config,
Err(detail) => die(&detail, SANDBOX_SETUP_EXIT),
};
if let Err(refusal) = engage(&config) {
die(&refusal.detail, refusal.exit);
}
let error = exec_adapter(&config);
die(&error, SANDBOX_SETUP_EXIT)
}
fn die(detail: &str, exit: i32) -> ! {
eprintln!("sandbox_setup_failed: {detail}");
std::process::exit(exit)
}
#[cfg(target_os = "linux")]
fn engage(config: &HelperConfig) -> Result<(), Refusal> {
super::linux::unshare_namespaces()?;
apply_rlimits(config)?;
close_extra_fds()?;
super::linux::apply_landlock(
&config.jail,
&config.worker,
&config.exec_grants,
&config.read_grants,
)?;
if config.seccomp {
super::linux::apply_seccomp()?;
}
Ok(())
}
#[cfg(target_os = "macos")]
fn engage(config: &HelperConfig) -> Result<(), Refusal> {
apply_rlimits(config)?;
close_extra_fds()?;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn engage(_config: &HelperConfig) -> Result<(), Refusal> {
Err("no jail backend exists for this platform"
.to_string()
.into())
}
fn apply_rlimits(config: &HelperConfig) -> Result<(), Refusal> {
use rlimit::{Resource, setrlimit};
let apply = |resource: Resource, name: &str, value: u64| {
setrlimit(resource, value, value).map_err(|_| format!("setrlimit {name} failed"))
};
#[cfg(target_os = "linux")]
if let Some(bytes) = config.address_space_bytes {
apply(Resource::AS, "RLIMIT_AS", bytes)?;
}
apply(Resource::CPU, "RLIMIT_CPU", config.cpu_seconds)?;
apply(Resource::FSIZE, "RLIMIT_FSIZE", config.file_size_bytes)?;
apply(
Resource::NPROC,
"RLIMIT_NPROC",
nproc_limit(config.max_processes)?,
)?;
apply(Resource::CORE, "RLIMIT_CORE", 0)?;
Ok(())
}
fn nproc_limit(configured: u64) -> Result<u64, Refusal> {
use rlimit::{Resource, getrlimit};
let (_, hard) =
getrlimit(Resource::NPROC).map_err(|_| "getrlimit RLIMIT_NPROC failed".to_string())?;
headroom_limit(configured, hard, current_uid_process_count)
}
fn headroom_limit(
configured: u64,
hard: u64,
count: impl Fn() -> std::io::Result<u64>,
) -> Result<u64, Refusal> {
let base = count().map_err(|e| Refusal {
detail: format!(
"the real user id's process count could not be measured, so the headroom this limit rides on is unknown: {:?}",
e.kind()
),
exit: PROCESS_COUNT_EXIT,
})?;
Ok(base.saturating_add(configured).min(hard))
}
#[cfg(target_os = "macos")]
fn current_uid_process_count() -> std::io::Result<u64> {
use libproc::processes::{ProcFilter, pids_by_type};
let uid = nix::unistd::getuid().as_raw();
let pids = pids_by_type(ProcFilter::ByUID { uid }).map_err(std::io::Error::other)?;
Ok(pids.len() as u64)
}
#[cfg(target_os = "linux")]
fn current_uid_process_count() -> std::io::Result<u64> {
use std::os::unix::fs::MetadataExt;
let uid = u32::from(nix::unistd::getuid().as_raw());
let mut count: u64 = 0;
for entry in std::fs::read_dir("/proc")? {
let entry = entry?;
let name = entry.file_name();
if name.to_str().is_none_or(|n| n.parse::<u32>().is_err()) {
continue;
}
match entry.metadata() {
Ok(metadata) => {
if metadata.uid() == uid {
count += 1;
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
}
}
Ok(count)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn current_uid_process_count() -> std::io::Result<u64> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"no process listing exists for this platform",
))
}
fn close_extra_fds() -> Result<(), String> {
#[cfg(target_os = "linux")]
const FD_DIR: &str = "/proc/self/fd";
#[cfg(not(target_os = "linux"))]
const FD_DIR: &str = "/dev/fd";
let entries = std::fs::read_dir(FD_DIR)
.map_err(|e| format!("cannot enumerate descriptors: {:?}", e.kind()))?;
let mut fds = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("cannot enumerate descriptors: {:?}", e.kind()))?;
if let Some(fd) = entry
.file_name()
.to_str()
.and_then(|name| name.parse::<i32>().ok())
&& fd > 2
{
fds.push(fd);
}
}
for fd in fds {
let _ = nix::unistd::close(fd);
}
Ok(())
}
fn exec_adapter(config: &HelperConfig) -> String {
let path = match CString::new(config.worker.as_os_str().as_bytes()) {
Ok(path) => path,
Err(_) => return "worker path contains a NUL byte".to_string(),
};
let argv0 = CString::new(WORKER_BINARY).expect("static name has no NUL");
let mode = match CString::new(config.adapter.as_str()) {
Ok(mode) => mode,
Err(_) => return "adapter mode contains a NUL byte".to_string(),
};
match nix::unistd::execv(&path, &[argv0.as_c_str(), mode.as_c_str()]) {
Ok(infallible) => match infallible {},
Err(e) => format!("exec of the adapter mode failed: {e:?}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_process_limit_rides_on_the_measured_count_and_clamps() {
let limit = headroom_limit(64, 4096, || Ok(609)).expect("a measured count computes");
assert_eq!(limit, 673);
let clamped = headroom_limit(64, 650, || Ok(609)).expect("a measured count computes");
assert_eq!(clamped, 650);
let idle = headroom_limit(64, 4096, || Ok(0)).expect("a measured count computes");
assert_eq!(idle, 64);
}
#[test]
fn a_count_the_host_cannot_supply_refuses_the_run() {
let refusal = headroom_limit(64, 4096, || {
Err(std::io::Error::other("the listing is unavailable"))
})
.expect_err("an unmeasurable count must refuse the run");
assert_eq!(refusal.exit, PROCESS_COUNT_EXIT);
assert_ne!(refusal.exit, SANDBOX_SETUP_EXIT);
assert!(refusal.detail.contains("Other"), "{}", refusal.detail);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn this_host_can_be_counted() {
let count = current_uid_process_count().expect("the host answers its own process listing");
assert!(count > 0, "the count was {count}");
}
}