use std::io;
use std::os::unix::process::CommandExt;
use std::process::{Child, Command};
use std::time::{Duration, Instant};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicI32, Ordering};
use super::{ResolvedLimits, Wait};
static CHILD_PGID: AtomicI32 = AtomicI32::new(0);
extern "C" fn terminate_child_group(sig: libc::c_int) {
let pgid = CHILD_PGID.load(Ordering::SeqCst);
if pgid > 1 {
unsafe { libc::kill(-pgid, libc::SIGKILL) };
}
unsafe { libc::_exit(128 + sig) };
}
fn term_signal_set() -> libc::sigset_t {
unsafe {
let mut set: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut set);
libc::sigaddset(&mut set, libc::SIGTERM);
libc::sigaddset(&mut set, libc::SIGINT);
libc::sigaddset(&mut set, libc::SIGHUP);
set
}
}
fn block_term_signals() -> libc::sigset_t {
unsafe {
let set = term_signal_set();
let mut old: libc::sigset_t = std::mem::zeroed();
libc::pthread_sigmask(libc::SIG_BLOCK, &set, &mut old);
old
}
}
fn restore_signal_mask(old: &libc::sigset_t) {
unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, old, std::ptr::null_mut()) };
}
fn install_termination_handler() {
let handler = terminate_child_group as extern "C" fn(libc::c_int);
let h = handler as usize as libc::sighandler_t;
unsafe {
libc::signal(libc::SIGTERM, h);
libc::signal(libc::SIGINT, h);
libc::signal(libc::SIGHUP, h);
}
}
#[derive(Clone, Copy, Default)]
pub struct Rlimits {
cpu: u64,
address_space: Option<u64>,
fsize: u64,
nofile: u64,
nproc: u64,
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
type RlimitResource = libc::__rlimit_resource_t;
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
type RlimitResource = libc::c_int;
fn hard_limit(resource: RlimitResource) -> Option<u64> {
let mut rl: libc::rlimit = unsafe { std::mem::zeroed() };
if unsafe { libc::getrlimit(resource, &mut rl) } == 0 {
Some(rl.rlim_max)
} else {
None
}
}
fn clamp(resource: RlimitResource, want: u64) -> (u64, bool) {
match hard_limit(resource) {
Some(hard) if hard != libc::RLIM_INFINITY && hard < want => (hard, true),
_ => (want, false),
}
}
pub fn resolve(limits: &ResolvedLimits) -> (Rlimits, Vec<&'static str>) {
let mut unenforced = Vec::new();
let (cpu, c) = clamp(libc::RLIMIT_CPU, limits.cpu_secs);
if c {
unenforced.push("cpu_limit_clamped_to_hard_rlimit");
}
let (fsize, c) = clamp(libc::RLIMIT_FSIZE, limits.fsize_bytes);
if c {
unenforced.push("file_size_limit_clamped_to_hard_rlimit");
}
let (nofile, _) = clamp(libc::RLIMIT_NOFILE, limits.nofile);
let (nproc, nproc_clamped) = clamp(libc::RLIMIT_NPROC, limits.max_processes);
if nproc_clamped {
unenforced.push("process_limit_clamped_to_hard_rlimit");
}
if current_uid_tasks().is_none() {
unenforced.push("process_limit_is_a_fixed_ceiling_not_measured");
}
if unsafe { libc::geteuid() } == 0 {
unenforced.push("process_limit_not_enforced_for_uid_0");
}
let address_space = if cfg!(target_os = "macos") {
unenforced.push("memory_limit_not_enforced_on_macos");
None
} else {
let (v, c) = clamp(libc::RLIMIT_AS, limits.memory_bytes);
if c {
unenforced.push("memory_limit_clamped_to_hard_rlimit");
}
Some(v)
};
(
Rlimits {
cpu,
address_space,
fsize,
nofile,
nproc,
},
unenforced,
)
}
fn apply(r: &Rlimits) {
unsafe {
let set = |res: RlimitResource, v: u64| {
let rl = libc::rlimit {
rlim_cur: v,
rlim_max: v,
};
libc::setrlimit(res, &rl);
};
set(libc::RLIMIT_CPU, r.cpu);
if let Some(as_bytes) = r.address_space {
set(libc::RLIMIT_AS, as_bytes);
}
set(libc::RLIMIT_FSIZE, r.fsize);
set(libc::RLIMIT_NOFILE, r.nofile);
set(libc::RLIMIT_NPROC, r.nproc);
set(libc::RLIMIT_CORE, 0);
}
}
fn maxrss_to_kb(ru_maxrss: i64) -> u64 {
let v = ru_maxrss.max(0) as u64;
if cfg!(any(target_os = "macos", target_os = "ios")) {
v / 1024
} else {
v
}
}
#[cfg(target_os = "linux")]
mod seccomp {
#[cfg(target_arch = "x86_64")]
const NATIVE_AUDIT_ARCH: u32 = 0xC000_003E; #[cfg(target_arch = "aarch64")]
const NATIVE_AUDIT_ARCH: u32 = 0xC000_00B7;
const OFF_NR: u32 = 0;
const OFF_ARCH: u32 = 4;
const OFF_ARG0: u32 = 16;
const SECCOMP_RET_DATA: u32 = 0x0000_ffff;
#[inline]
fn stmt(code: u16, k: u32) -> libc::sock_filter {
libc::sock_filter {
code,
jt: 0,
jf: 0,
k,
}
}
#[inline]
fn jeq(k: u32, jt: u8, jf: u8) -> libc::sock_filter {
libc::sock_filter {
code: (libc::BPF_JMP | libc::BPF_JEQ | libc::BPF_K) as u16,
jt,
jf,
k,
}
}
fn filter() -> Vec<libc::sock_filter> {
let ld = (libc::BPF_LD | libc::BPF_W | libc::BPF_ABS) as u16;
let ret = (libc::BPF_RET | libc::BPF_K) as u16;
let jge = (libc::BPF_JMP | libc::BPF_JGE | libc::BPF_K) as u16;
vec![
stmt(ld, OFF_ARCH), jeq(NATIVE_AUDIT_ARCH, 0, 13), stmt(ld, OFF_NR), libc::sock_filter {
code: jge,
jt: 11,
jf: 0,
k: 0x4000_0000,
}, jeq(libc::SYS_io_uring_setup as u32, 7, 0), jeq(libc::SYS_io_uring_enter as u32, 6, 0), jeq(libc::SYS_io_uring_register as u32, 5, 0), jeq(libc::SYS_socket as u32, 0, 6), stmt(ld, OFF_ARG0), jeq(libc::AF_INET as u32, 3, 0), jeq(libc::AF_INET6 as u32, 2, 0), stmt(ret, libc::SECCOMP_RET_ALLOW), stmt(
ret,
libc::SECCOMP_RET_ERRNO | (libc::ENOSYS as u32 & SECCOMP_RET_DATA),
), stmt(
ret,
libc::SECCOMP_RET_ERRNO | (libc::EACCES as u32 & SECCOMP_RET_DATA),
), stmt(ret, libc::SECCOMP_RET_ALLOW), stmt(ret, libc::SECCOMP_RET_KILL_PROCESS), ]
}
pub fn available() -> bool {
unsafe { libc::prctl(21) >= 0 }
}
pub fn program() -> Vec<libc::sock_filter> {
filter()
}
fn installable_with(prog: &[libc::sock_filter]) -> bool {
if !available() {
return false;
}
let pid = unsafe { libc::fork() };
if pid < 0 {
return false;
}
if pid == 0 {
let rc = unsafe { install(prog) };
unsafe { libc::_exit(if rc.is_ok() { 0 } else { 1 }) };
}
let mut status: libc::c_int = 0;
loop {
let r = unsafe { libc::waitpid(pid, &mut status, 0) };
if r == pid {
break;
}
if r == -1 {
let e = std::io::Error::last_os_error();
if e.raw_os_error() != Some(libc::EINTR) {
return false;
}
}
}
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0
}
pub fn installable() -> bool {
installable_with(&program())
}
pub unsafe fn install(prog: &[libc::sock_filter]) -> std::io::Result<()> {
let fprog = libc::sock_fprog {
len: prog.len() as u16,
filter: prog.as_ptr() as *mut libc::sock_filter,
};
let rc = unsafe {
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
return Err(std::io::Error::last_os_error());
}
libc::prctl(
libc::PR_SET_SECCOMP,
libc::SECCOMP_MODE_FILTER as libc::c_ulong,
&fprog as *const libc::sock_fprog as libc::c_ulong,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{installable, installable_with, program};
#[test]
fn the_real_program_installs_on_this_seccomp_capable_host() {
assert!(installable());
}
#[test]
fn an_uninstallable_program_reports_false() {
assert!(!installable_with(&[]));
}
#[test]
fn installable_delegates_to_the_real_program() {
assert_eq!(installable(), installable_with(&program()));
}
}
}
#[cfg(target_os = "linux")]
pub fn no_net_kernel_enforcement_available() -> bool {
seccomp::installable()
}
#[cfg(not(target_os = "linux"))]
pub fn no_net_kernel_enforcement_available() -> bool {
false
}
pub fn spawn_and_wait(
mut cmd: Command,
limits: &ResolvedLimits,
_stdio: super::RawStdio,
) -> io::Result<Wait> {
let (rlimits, unenforced) = resolve(limits);
cmd.process_group(0);
#[cfg(target_os = "linux")]
let net_filter: Option<Vec<libc::sock_filter>> = if limits.no_net && seccomp::available() {
Some(seccomp::program())
} else {
None
};
#[cfg(target_os = "linux")]
let no_net_seccomp_enforced = net_filter.is_some();
#[cfg(not(target_os = "linux"))]
let no_net_seccomp_enforced = false;
unsafe {
cmd.pre_exec(move || {
apply(&rlimits);
#[cfg(target_os = "linux")]
{
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
if libc::getppid() == 1 {
libc::_exit(1);
}
}
let mut empty: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut empty);
libc::sigprocmask(libc::SIG_SETMASK, &empty, std::ptr::null_mut());
#[cfg(target_os = "linux")]
if let Some(ref prog) = net_filter {
seccomp::install(prog)?;
}
Ok(())
});
}
install_termination_handler();
let saved_mask = block_term_signals();
let spawned = cmd.spawn();
let mut child: Child = match spawned {
Ok(c) => c,
Err(e) => {
restore_signal_mask(&saved_mask);
return Err(e);
}
};
CHILD_PGID.store(child.id() as i32, Ordering::SeqCst);
restore_signal_mask(&saved_mask);
let start = Instant::now();
let mut status: libc::c_int = 0;
let mut rusage: libc::rusage = unsafe { std::mem::zeroed() };
let mut timed_out = false;
let pid = child.id() as libc::pid_t;
loop {
let r = unsafe { libc::wait4(pid, &mut status, libc::WNOHANG, &mut rusage) };
if r == pid {
break;
} else if r == -1 {
let e = io::Error::last_os_error();
if e.raw_os_error() != Some(libc::EINTR) {
if pid > 1 {
unsafe { libc::kill(-pid, libc::SIGKILL) };
}
let _ = child.wait();
CHILD_PGID.store(0, Ordering::SeqCst);
return Err(e);
}
} else {
if start.elapsed() >= Duration::from_secs(limits.timeout_secs) {
unsafe { libc::killpg(pid, libc::SIGKILL) };
let _ = unsafe { libc::wait4(pid, &mut status, 0, &mut rusage) };
timed_out = true;
break;
}
std::thread::sleep(Duration::from_millis(10));
}
}
if pid > 1 {
unsafe { libc::killpg(pid, libc::SIGKILL) };
}
let (exit_code, signal) = if libc::WIFEXITED(status) {
(libc::WEXITSTATUS(status) as i64, None)
} else if libc::WIFSIGNALED(status) {
(0, Some(libc::WTERMSIG(status)))
} else {
(status as i64, None)
};
let secs = rusage.ru_utime.tv_sec as i64 + rusage.ru_stime.tv_sec as i64;
let usecs = rusage.ru_utime.tv_usec as i64 + rusage.ru_stime.tv_usec as i64;
let cpu_ms = (secs * 1000 + usecs / 1000).max(0) as u64;
CHILD_PGID.store(0, Ordering::SeqCst);
Ok(Wait {
exit_code,
signal,
timed_out,
cpu_ms,
peak_memory_kb: maxrss_to_kb(rusage.ru_maxrss as i64),
no_net_seccomp_enforced,
unenforced,
})
}
static UID_TASKS: OnceLock<Option<u64>> = OnceLock::new();
pub fn current_uid_tasks() -> Option<u64> {
*UID_TASKS.get_or_init(measure_uid_tasks)
}
fn measure_uid_tasks() -> Option<u64> {
if !cfg!(target_os = "linux") {
return None;
}
use std::io::Read;
let uid = unsafe { libc::getuid() };
let mut total: u64 = 0;
let mut seen_any = false;
let mut buf: Vec<u8> = Vec::with_capacity(4096);
for entry in std::fs::read_dir("/proc").ok()?.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.is_empty() || !name.bytes().all(|b| b.is_ascii_digit()) {
continue;
}
let Ok(mut f) = std::fs::File::open(format!("/proc/{name}/status")) else {
continue; };
buf.clear();
if f.read_to_end(&mut buf).is_err() {
continue; }
let status = String::from_utf8_lossy(&buf);
let mut this_uid: Option<u32> = None;
let mut threads: Option<u64> = None;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Uid:") {
this_uid = rest.split_whitespace().next().and_then(|v| v.parse().ok());
} else if let Some(rest) = line.strip_prefix("Threads:") {
threads = rest.trim().parse().ok();
}
if this_uid.is_some() && threads.is_some() {
break;
}
}
if this_uid == Some(uid) {
seen_any = true;
total += threads.unwrap_or(1);
}
}
if seen_any { Some(total) } else { None }
}