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
}
}
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);
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());
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));
}
}
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),
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 }
}