#[cfg(unix)]
use crate::policy::ResourceLimits;
#[cfg(unix)]
use tokio::process::Command;
#[cfg(unix)]
pub fn apply_to_command(cmd: &mut Command, limits: &ResourceLimits) {
let cpu = limits.cpu_time_secs;
let rss = limits.max_rss_bytes;
let fds = limits.max_open_fds;
if cpu.is_none() && rss.is_none() && fds.is_none() {
return;
}
use std::os::unix::process::CommandExt as _;
unsafe {
cmd.as_std_mut().pre_exec(move || {
macro_rules! try_set {
($resource:expr, $value:expr) => {
if let Some(v) = $value {
let limit = libc::rlimit {
rlim_cur: v as libc::rlim_t,
rlim_max: v as libc::rlim_t,
};
if libc::setrlimit($resource, &limit) != 0 {
return Err(std::io::Error::last_os_error());
}
}
};
}
try_set!(libc::RLIMIT_CPU, cpu);
try_set!(libc::RLIMIT_AS, rss);
try_set!(libc::RLIMIT_NOFILE, fds);
Ok(())
});
}
}
#[cfg(not(unix))]
pub fn apply_to_command(
_cmd: &mut tokio::process::Command,
_limits: &crate::policy::ResourceLimits,
) {
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::time::Duration;
use tokio::process::Command;
use tokio::time::timeout;
#[tokio::test]
async fn default_limits_are_a_no_op() {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg("echo ok");
apply_to_command(&mut cmd, &ResourceLimits::default());
let out = cmd.output().await.expect("spawn ok");
assert!(out.status.success(), "child should succeed: {out:?}");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "ok");
}
#[tokio::test]
async fn nofile_limit_is_observable_in_child() {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg("ulimit -n");
apply_to_command(
&mut cmd,
&ResourceLimits {
max_open_fds: Some(64),
..Default::default()
},
);
let out = cmd.output().await.expect("spawn ok");
assert!(out.status.success(), "child should succeed: {out:?}");
let reported: u64 = String::from_utf8_lossy(&out.stdout)
.trim()
.parse()
.expect("ulimit prints a number");
assert_eq!(reported, 64, "child should see the FD cap we set");
}
#[tokio::test]
async fn cpu_limit_kills_busy_loop() {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg("while :; do :; done");
apply_to_command(
&mut cmd,
&ResourceLimits {
cpu_time_secs: Some(1),
..Default::default()
},
);
let out = timeout(Duration::from_secs(10), cmd.output())
.await
.expect("must finish within wall budget")
.expect("spawn ok");
assert!(
!out.status.success(),
"busy loop should be killed, got {out:?}"
);
use std::os::unix::process::ExitStatusExt as _;
let signal = out.status.signal();
assert!(
matches!(signal, Some(s) if s == libc::SIGXCPU || s == libc::SIGKILL),
"child should be killed by SIGXCPU or SIGKILL (kernel-dependent), got status {:?} (signal {signal:?})",
out.status,
);
}
#[tokio::test]
async fn unsatisfiable_limit_fails_at_spawn() {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg("echo unreachable");
apply_to_command(
&mut cmd,
&ResourceLimits {
max_open_fds: Some(u64::MAX),
..Default::default()
},
);
let result = cmd.output().await;
assert!(
result.is_err() || !result.as_ref().map(|o| o.status.success()).unwrap_or(true),
"unsatisfiable rlimit must fail-closed, got {result:?}"
);
}
}