use crate::config::Config;
use crate::output;
use nix::sys::resource::{Resource, getrlimit, setrlimit};
#[cfg(target_os = "linux")]
const NPROC_NORMAL: u64 = 4096;
const NOFILE_NORMAL: u64 = 65536;
#[cfg(target_os = "linux")]
const NPROC_LOCKDOWN: u64 = 1024;
const NOFILE_LOCKDOWN: u64 = 4096;
struct Limit {
resource: Resource,
soft: u64,
name: &'static str,
}
fn limits_for(config: &Config) -> Vec<Limit> {
let lockdown = config.lockdown_enabled();
let limits = vec![
Limit {
resource: Resource::RLIMIT_NOFILE,
soft: if lockdown {
NOFILE_LOCKDOWN
} else {
NOFILE_NORMAL
},
name: "NOFILE",
},
Limit {
resource: Resource::RLIMIT_CORE,
soft: 0,
name: "CORE",
},
];
limits
}
pub fn apply(config: &Config, verbose: bool) {
if !config.rlimits_enabled() {
if verbose {
output::verbose("Resource limits: disabled");
}
return;
}
for lim in limits_for(config) {
let Ok((_, hard)) = getrlimit(lim.resource) else {
output::warn(&format!(
"Failed to read RLIMIT_{}, skipping",
lim.name
));
continue;
};
let effective = lim.soft.min(hard);
if let Err(e) = setrlimit(lim.resource, effective, hard) {
output::warn(&format!("Failed to set RLIMIT_{}: {e}", lim.name));
} else if verbose {
output::verbose(&format!(
"RLIMIT_{}: {} (hard: {})",
lim.name, effective, hard
));
}
}
}
#[cfg(target_os = "linux")]
pub fn apply_nproc(config: &Config, verbose: bool) {
if !config.rlimits_enabled() {
return;
}
let soft = if config.lockdown_enabled() {
NPROC_LOCKDOWN
} else {
NPROC_NORMAL
};
let Ok((_, hard)) = getrlimit(Resource::RLIMIT_NPROC) else {
return;
};
let effective = soft.min(hard);
if let Err(e) = setrlimit(Resource::RLIMIT_NPROC, effective, effective) {
output::warn(&format!("Failed to set RLIMIT_NPROC: {e}"));
} else if verbose {
output::verbose(&format!(
"RLIMIT_NPROC: {effective} (hard: {effective})"
));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_sets_core_to_zero() {
let config = Config::default();
apply(&config, false);
let (soft, _) = getrlimit(Resource::RLIMIT_CORE).unwrap();
assert_eq!(soft, 0);
}
#[test]
fn apply_respects_disabled() {
let config = Config {
no_rlimits: Some(true),
..Config::default()
};
apply(&config, true);
}
#[test]
fn limits_lockdown_tighter_than_normal() {
let normal = Config::default();
let lockdown = Config {
lockdown: Some(true),
..Config::default()
};
let normal_limits = limits_for(&normal);
let lockdown_limits = limits_for(&lockdown);
for (n, l) in normal_limits.iter().zip(lockdown_limits.iter()) {
assert!(
l.soft <= n.soft,
"Lockdown {} ({}) should be <= normal ({})",
n.name,
l.soft,
n.soft
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn apply_nproc_pins_hard_equal_to_soft() {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.arg("--exact")
.arg(
"sandbox::rlimits::tests::apply_nproc_pins_hard_equal_to_soft_child",
)
.arg("--ignored")
.env("AI_JAIL_RLIMIT_CHILD", "1")
.output()
.expect("spawn child rlimit test");
assert!(
output.status.success(),
"child rlimit test failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(target_os = "linux")]
#[test]
#[ignore]
fn apply_nproc_pins_hard_equal_to_soft_child() {
if std::env::var_os("AI_JAIL_RLIMIT_CHILD").is_none() {
return;
}
let (_, hard_before) =
getrlimit(Resource::RLIMIT_NPROC).expect("getrlimit");
let config = Config::default();
apply_nproc(&config, false);
let (soft_after, hard_after) =
getrlimit(Resource::RLIMIT_NPROC).expect("getrlimit");
assert_eq!(
soft_after, hard_after,
"apply_nproc must pin hard == soft so the sandbox can't raise it"
);
let expected = NPROC_NORMAL.min(hard_before);
assert_eq!(
soft_after, expected,
"soft limit should match NPROC_NORMAL clamped to current hard"
);
}
}