#![allow(unsafe_code)]
use std::io::{self, Read};
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use crate::evaluators::EvaluatorError;
use super::{SandboxLimits, SandboxOutcome};
pub(super) fn run_sandboxed_unix(
mut command: Command,
limits: &SandboxLimits,
) -> Result<SandboxOutcome, EvaluatorError> {
command.stdout(Stdio::piped()).stderr(Stdio::piped());
install_pre_exec_limits(&mut command, limits.clone());
let mut child = command.spawn().map_err(|err| EvaluatorError::Execution {
reason: format!("sandbox spawn failed: {err}"),
})?;
let wall_clock = limits.wall_clock;
let (outcome, wall_clock_exceeded) = wait_with_wall_clock(&mut child, wall_clock);
let stderr = outcome.stderr.clone();
let classified = classify(&outcome, wall_clock_exceeded, limits);
let final_outcome = SandboxOutcome {
success: classified.is_none() && outcome.exit_code == Some(0),
exit_code: outcome.exit_code,
signal: outcome.signal,
stderr,
limit_exceeded: classified.clone(),
};
if let Some(limit) = classified {
return Err(EvaluatorError::SandboxLimitExceeded { limit });
}
Ok(final_outcome)
}
fn install_pre_exec_limits(command: &mut Command, limits: SandboxLimits) {
unsafe {
command.pre_exec(move || apply_limits(&limits));
}
}
#[allow(clippy::unnecessary_wraps)]
fn apply_limits(limits: &SandboxLimits) -> io::Result<()> {
let cpu = clamp_rlim(limits.cpu.as_secs());
let _ = set_rlimit(libc::RLIMIT_CPU, cpu);
#[cfg(target_os = "linux")]
{
let mem = clamp_rlim(limits.memory_bytes);
let _ = set_rlimit(libc::RLIMIT_AS, mem);
}
#[cfg(not(target_os = "linux"))]
{
let mem = clamp_rlim(limits.memory_bytes);
let _ = set_rlimit(libc::RLIMIT_DATA, mem);
}
let nofile = clamp_rlim(limits.max_open_files);
let _ = set_rlimit(libc::RLIMIT_NOFILE, nofile);
#[cfg(target_os = "linux")]
if !limits.allow_network {
let _ = try_unshare_netns();
}
Ok(())
}
fn clamp_rlim(value: u64) -> libc::rlim_t {
libc::rlim_t::try_from(value).unwrap_or(libc::rlim_t::MAX)
}
#[cfg(target_os = "linux")]
type ResourceId = libc::__rlimit_resource_t;
#[cfg(not(target_os = "linux"))]
type ResourceId = libc::c_int;
fn set_rlimit(resource: ResourceId, value: libc::rlim_t) -> io::Result<()> {
let rlim = libc::rlimit {
rlim_cur: value,
rlim_max: value,
};
let ret = unsafe { libc::setrlimit(resource, &raw const rlim) };
if ret == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(target_os = "linux")]
fn try_unshare_netns() -> io::Result<()> {
let ret = unsafe { libc::unshare(libc::CLONE_NEWNET) };
if ret == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
struct RawOutcome {
exit_code: Option<i32>,
signal: Option<i32>,
stderr: String,
}
fn wait_with_wall_clock(child: &mut Child, wall_clock: Duration) -> (RawOutcome, bool) {
let (tx, rx) = mpsc::channel::<()>();
let pid = child.id().cast_signed();
let deadline = Instant::now() + wall_clock;
let poll_interval = Duration::from_millis(25);
let waiter = thread::spawn(move || {
let mut status = 0i32;
loop {
let ret = unsafe { libc::waitpid(pid, &raw mut status, libc::WNOHANG) };
if ret == pid {
return Some(status);
} else if ret == -1 {
return None;
}
if rx.recv_timeout(poll_interval).is_ok() {
let _ = unsafe { libc::waitpid(pid, &raw mut status, 0) };
return Some(status);
}
}
});
let mut wall_clock_exceeded = false;
let status_code;
loop {
if waiter.is_finished() {
status_code = waiter.join().unwrap_or(None);
break;
}
if Instant::now() >= deadline {
let _ = unsafe { libc::kill(pid, libc::SIGKILL) };
let _ = tx.send(());
wall_clock_exceeded = true;
status_code = waiter.join().unwrap_or(None);
break;
}
thread::sleep(poll_interval);
}
let (exit_code, signal) = decode_status(status_code);
let mut stderr_buf = String::new();
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_string(&mut stderr_buf);
}
(
RawOutcome {
exit_code,
signal,
stderr: stderr_buf,
},
wall_clock_exceeded,
)
}
fn decode_status(status: Option<i32>) -> (Option<i32>, Option<i32>) {
let Some(status) = status else {
return (None, None);
};
let es = std::process::ExitStatus::from_raw(status);
(es.code(), es.signal())
}
fn classify(
outcome: &RawOutcome,
wall_clock_exceeded: bool,
limits: &SandboxLimits,
) -> Option<String> {
if wall_clock_exceeded {
return Some("wall_clock".to_string());
}
if let Some(sig) = outcome.signal {
if sig == libc::SIGXCPU {
return Some("cpu".to_string());
}
if sig == libc::SIGSEGV || sig == libc::SIGBUS {
return Some("memory".to_string());
}
}
let stderr_lower = outcome.stderr.to_ascii_lowercase();
if stderr_lower.contains("too many open files") {
return Some("fds".to_string());
}
if stderr_lower.contains("cannot allocate memory") || stderr_lower.contains("out of memory") {
return Some("memory".to_string());
}
if !limits.allow_network
&& (stderr_lower.contains("network is unreachable")
|| stderr_lower.contains("connection refused")
|| stderr_lower.contains("operation not permitted")
|| stderr_lower.contains("name or service not known")
|| stderr_lower.contains("no address associated with hostname")
|| stderr_lower.contains("could not resolve host")
|| stderr_lower.contains("temporary failure in name resolution"))
{
return Some("network".to_string());
}
None
}