use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use super::{SANDBOX_SETUP_EXIT, WORKER_BINARY};
pub struct HelperConfig {
pub jail: PathBuf,
pub worker: PathBuf,
pub adapter: String,
pub address_space_bytes: u64,
pub cpu_seconds: u64,
pub file_size_bytes: u64,
pub max_processes: u64,
pub seccomp: bool,
}
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 = None;
let mut cpu_seconds = None;
let mut file_size_bytes = None;
let mut max_processes = None;
let mut seccomp = None;
let mut iter = args.iter();
while let Some(flag) = iter.next() {
let value = iter
.next()
.ok_or_else(|| format!("flag {flag} is missing its value"))?;
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(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,
other => return Err(format!("unknown seccomp mode {other:?}")),
})
}
other => return Err(format!("unknown flag {other:?}")),
}
}
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")?,
})
}
fn parse_number(flag: &str, value: &str) -> Result<u64, String> {
value
.parse()
.map_err(|e| format!("flag {flag} value {value:?} is not a number: {e}"))
}
pub fn helper_main(args: &[String]) -> ! {
let config = match parse_args(args) {
Ok(config) => config,
Err(detail) => die(&detail),
};
if let Err(detail) = engage(&config) {
die(&detail);
}
let error = exec_adapter(&config);
die(&error)
}
fn die(detail: &str) -> ! {
eprintln!("sandbox_setup_failed: {detail}");
std::process::exit(SANDBOX_SETUP_EXIT)
}
#[cfg(target_os = "linux")]
fn engage(config: &HelperConfig) -> Result<(), String> {
super::linux::unshare_namespaces()?;
apply_rlimits(config)?;
close_extra_fds()?;
super::linux::apply_landlock(&config.jail, &config.worker)?;
if config.seccomp {
super::linux::apply_seccomp()?;
}
Ok(())
}
#[cfg(target_os = "macos")]
fn engage(config: &HelperConfig) -> Result<(), String> {
apply_rlimits(config)?;
close_extra_fds()?;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn engage(_config: &HelperConfig) -> Result<(), String> {
Err("no jail backend exists for this platform".to_string())
}
fn apply_rlimits(config: &HelperConfig) -> Result<(), String> {
use rlimit::{Resource, setrlimit};
let apply = |resource: Resource, name: &str, value: u64| {
setrlimit(resource, value, value).map_err(|e| format!("setrlimit {name} failed: {e}"))
};
#[cfg(target_os = "linux")]
apply(Resource::AS, "RLIMIT_AS", config.address_space_bytes)?;
apply(Resource::CPU, "RLIMIT_CPU", config.cpu_seconds)?;
apply(Resource::FSIZE, "RLIMIT_FSIZE", config.file_size_bytes)?;
apply(Resource::NPROC, "RLIMIT_NPROC", config.max_processes)?;
apply(Resource::CORE, "RLIMIT_CORE", 0)?;
Ok(())
}
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}"))?;
let mut fds = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("cannot enumerate descriptors: {e}"))?;
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}"),
}
}