#![allow(unsafe_code)]
use std::collections::VecDeque;
use std::fs::File;
use std::io::{self, ErrorKind, Read};
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::time::{Duration, Instant};
use lonca_policy::config::Verification;
use crate::sweep;
pub const OUTPUT_TAIL: usize = 64 * 1024;
const GRACE: Duration = Duration::from_secs(1);
const POLL_MS: libc::c_int = 20;
const SEE_EXIT: libc::c_int = libc::WEXITED | libc::WNOHANG | libc::WNOWAIT;
#[cfg(all(target_os = "linux", target_env = "gnu"))]
type Resource = libc::__rlimit_resource_t;
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
type Resource = libc::c_int;
pub struct Ended {
pub status: Option<ExitStatus>,
pub output: Vec<u8>,
pub left: Option<(String, String)>,
}
pub struct Running {
child: Child,
output: File,
}
pub fn start(mut command: Command, cfg: &Verification) -> io::Result<Running> {
let limits: Vec<(Resource, libc::rlimit)> = wanted(cfg)
.into_iter()
.map(|(resource, value)| (resource, held_to(value, in_force(resource))))
.collect();
let (output, input) = io::pipe()?;
command
.stdin(Stdio::null())
.stdout(input.try_clone()?)
.stderr(input);
unsafe {
command.pre_exec(move || {
libc::setsid();
for (resource, limit) in &limits {
if libc::setrlimit(*resource, limit) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
});
}
let child = command.spawn()?;
Ok(Running {
child,
output: reader(output),
})
}
fn wanted(cfg: &Verification) -> Vec<(Resource, u64)> {
let mut all = vec![
(libc::RLIMIT_CPU, cfg.limit_cpu_seconds),
(libc::RLIMIT_FSIZE, cfg.limit_file_bytes),
(libc::RLIMIT_NOFILE, cfg.limit_open_files),
];
if cfg!(target_os = "linux") {
all.push((libc::RLIMIT_NPROC, cfg.limit_processes));
all.push((libc::RLIMIT_AS, cfg.limit_address_bytes));
}
all.retain(|&(_, value)| value != 0);
all
}
fn in_force(resource: Resource) -> libc::rlimit {
let mut now = libc::rlimit {
rlim_cur: libc::RLIM_INFINITY,
rlim_max: libc::RLIM_INFINITY,
};
unsafe { libc::getrlimit(resource, &mut now) };
now
}
fn held_to(wanted: u64, now: libc::rlimit) -> libc::rlimit {
libc::rlimit {
rlim_cur: wanted.min(now.rlim_cur),
rlim_max: wanted.min(now.rlim_max),
}
}
pub fn watch(running: Running, timeout: Duration, mark: Option<&str>) -> Ended {
let started = Instant::now();
let Running { mut child, output } = running;
let pid = child.id();
let mut pipe = Some(output);
let mut tail = Tail::default();
let mut timed_out = false;
let mut left = None;
let mut killed_at = None;
loop {
pump(&mut pipe, &mut tail);
match killed_at {
None => {
let exited = has_exited(pid);
timed_out = !exited && past(started.elapsed(), timeout);
if exited || timed_out {
kill_group(pid);
left = mark.and_then(sweep::what_would_not_end);
killed_at = Some(Instant::now());
}
}
Some(at) => {
kill_group(pid);
if past(at.elapsed(), GRACE) || pipe.is_none() {
break;
}
}
}
}
Ended {
status: reaped(&mut child).filter(|_| !timed_out),
output: tail.into_bytes(),
left,
}
}
fn reaped(child: &mut Child) -> Option<ExitStatus> {
let started = Instant::now();
while !past(started.elapsed(), GRACE) {
if let Ok(Some(status)) = child.try_wait() {
return Some(status);
}
std::thread::sleep(Duration::from_millis(POLL_MS as u64));
}
None
}
fn past(elapsed: Duration, limit: Duration) -> bool {
elapsed > limit
}
fn reader(pipe: impl Into<OwnedFd>) -> File {
let fd = pipe.into();
unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK) };
File::from(fd)
}
fn pump(pipe: &mut Option<File>, tail: &mut Tail) {
let mut open: Vec<libc::pollfd> = pipe
.iter()
.map(|file| libc::pollfd {
fd: file.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
})
.collect();
unsafe { libc::poll(open.as_mut_ptr(), open.len() as libc::nfds_t, POLL_MS) };
read_some(pipe, tail);
}
fn read_some(pipe: &mut Option<File>, tail: &mut Tail) {
let Some(file) = pipe else {
return;
};
let mut buffer = [0; OUTPUT_TAIL];
let closed = match file.read(&mut buffer) {
Ok(0) => true,
Ok(read) => {
tail.push(&buffer[..read]);
false
}
Err(e) => ends_a_pipe(e.kind()),
};
if closed {
*pipe = None;
}
}
fn ends_a_pipe(kind: ErrorKind) -> bool {
!matches!(kind, ErrorKind::WouldBlock | ErrorKind::Interrupted)
}
fn has_exited(pid: u32) -> bool {
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
let asked = unsafe { libc::waitid(libc::P_PID, pid, &mut info, SEE_EXIT) };
asked != 0 || info.si_signo != 0
}
fn kill_group(pid: u32) {
unsafe { libc::kill(-(pid as libc::pid_t), libc::SIGKILL) };
}
#[derive(Default)]
struct Tail(VecDeque<u8>);
impl Tail {
fn push(&mut self, bytes: &[u8]) {
self.0.extend(bytes);
let over = self.0.len().saturating_sub(OUTPUT_TAIL);
self.0.drain(..over);
}
fn into_bytes(self) -> Vec<u8> {
self.0.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> Verification {
Verification {
limit_cpu_seconds: 1,
limit_address_bytes: 2,
limit_processes: 3,
limit_file_bytes: 4,
limit_open_files: 5,
..Default::default()
}
}
#[test]
fn each_limit_sets_its_own_resource() {
let mut expected = vec![
(libc::RLIMIT_CPU, 1),
(libc::RLIMIT_FSIZE, 4),
(libc::RLIMIT_NOFILE, 5),
];
if cfg!(target_os = "linux") {
expected.push((libc::RLIMIT_NPROC, 3));
expected.push((libc::RLIMIT_AS, 2));
}
assert_eq!(wanted(&cfg()), expected);
}
#[test]
fn zero_sets_no_limit() {
let off = Verification {
limit_address_bytes: 0,
limit_processes: 0,
..cfg()
};
let expected = vec![
(libc::RLIMIT_CPU, 1),
(libc::RLIMIT_FSIZE, 4),
(libc::RLIMIT_NOFILE, 5),
];
assert_eq!(wanted(&off), expected);
}
#[test]
fn a_limit_in_force_is_never_raised() {
let got = |wanted, rlim_cur, rlim_max| {
let now = libc::rlimit { rlim_cur, rlim_max };
let held = held_to(wanted, now);
(held.rlim_cur, held.rlim_max)
};
assert_eq!(got(5, 10, 20), (5, 5));
assert_eq!(got(15, 10, 20), (10, 15));
assert_eq!(got(25, 10, 20), (10, 20));
}
#[test]
fn a_limit_is_past_only_once_it_is_over() {
let limit = Duration::from_secs(1);
assert!(!past(limit - Duration::from_nanos(1), limit));
assert!(!past(limit, limit));
assert!(past(limit + Duration::from_nanos(1), limit));
}
#[test]
fn only_a_pipe_with_nothing_yet_stays_open() {
assert!(!ends_a_pipe(ErrorKind::WouldBlock));
assert!(!ends_a_pipe(ErrorKind::Interrupted));
assert!(ends_a_pipe(ErrorKind::BrokenPipe));
}
#[test]
fn the_tail_keeps_the_end_and_no_more() {
let mut tail = Tail::default();
tail.push(&[1; OUTPUT_TAIL]);
tail.push(&[2, 3]);
let bytes = tail.into_bytes();
assert_eq!(bytes.len(), OUTPUT_TAIL);
assert_eq!(bytes[0], 1);
assert_eq!(bytes[OUTPUT_TAIL - 2..], [2, 3]);
}
#[test]
fn what_fits_is_kept_whole() {
let mut tail = Tail::default();
tail.push(b"ab");
tail.push(b"c");
assert_eq!(tail.into_bytes(), b"abc");
}
}