#[cfg(target_os = "linux")]
pub const SYSTEM_ROOTS: &[&str] = &[
"/bin", "/sbin", "/lib", "/lib64", "/usr", "/etc", "/dev", "/proc",
];
pub const DEFAULT_RLIMIT_AS_BYTES: u64 = 256 * 1024 * 1024;
pub const DEFAULT_RLIMIT_NPROC: u64 = 128;
pub const DEFAULT_RLIMIT_CPU_SECS: u64 = 5;
pub const DEFAULT_RLIMIT_FSIZE_BYTES: u64 = 64 * 1024 * 1024;
pub const SWEEP_BUDGET_MS: u64 = crate::proctree::MAX_EXTINCTION_DEADLINE_MS;
pub fn apply_child_plan(
plan: &super::sandbox_backend::ChildEnforcementPlan,
) -> std::io::Result<()> {
#[cfg(target_os = "linux")]
{
apply_child_plan_linux(plan)
}
#[cfg(not(target_os = "linux"))]
{
let _ = plan;
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"linux enforcement plan requires Linux",
))
}
}
#[cfg(target_os = "linux")]
pub(crate) fn is_child_subreaper() -> bool {
let mut flag: libc::c_int = 0;
let rc = unsafe {
libc::prctl(
libc::PR_GET_CHILD_SUBREAPER,
&mut flag as *mut libc::c_int as libc::c_ulong,
0,
0,
0,
)
};
rc == 0 && flag == 1
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ExpectedLimits {
pub rlimit_as: Option<u64>,
pub rlimit_nproc: Option<u64>,
pub rlimit_cpu: Option<u64>,
pub rlimit_fsize: Option<u64>,
pub cgroup_memory_max: Option<String>,
pub cgroup_pids_max: Option<String>,
pub cgroup_cpu_max: Option<String>,
pub cgroup_swap_max: Option<String>,
}
impl ExpectedLimits {
pub fn harness_defaults() -> Self {
Self {
rlimit_as: Some(DEFAULT_RLIMIT_AS_BYTES),
rlimit_nproc: Some(DEFAULT_RLIMIT_NPROC),
rlimit_cpu: Some(DEFAULT_RLIMIT_CPU_SECS),
rlimit_fsize: Some(DEFAULT_RLIMIT_FSIZE_BYTES),
cgroup_memory_max: None,
cgroup_pids_max: None,
cgroup_cpu_max: None,
cgroup_swap_max: None,
}
}
pub fn from_policy(policy: &crate::policy::Policy) -> Self {
let (cg_mem, cg_pids, cg_cpu, cg_swap) = match &policy.cgroup {
Some(cg) => (
cg.memory_max.clone(),
cg.pids_max.clone(),
cg.cpu_max.clone(),
cg.swap_max.clone(),
),
None => (None, None, None, None),
};
let cg_cpu = cg_cpu.or_else(|| policy.cpu_max.clone());
Self {
rlimit_as: policy.limits.address_space_bytes,
rlimit_nproc: policy.limits.processes,
rlimit_cpu: policy.limits.cpu_seconds,
rlimit_fsize: policy.limits.file_size_bytes,
cgroup_memory_max: cg_mem,
cgroup_pids_max: cg_pids,
cgroup_cpu_max: cg_cpu,
cgroup_swap_max: cg_swap,
}
}
}
pub fn verify_child_host(pid: u32) -> super::sandbox_backend::HostVerification {
#[cfg(target_os = "linux")]
{
verify_child_host_linux(pid, None)
}
#[cfg(not(target_os = "linux"))]
{
let _ = pid;
super::sandbox_backend::HostVerification::none()
}
}
pub fn verify_child_host_with_limits(
pid: u32,
expected: &ExpectedLimits,
) -> super::sandbox_backend::HostVerification {
#[cfg(target_os = "linux")]
{
verify_child_host_linux(pid, Some(expected))
}
#[cfg(not(target_os = "linux"))]
{
let _ = (pid, expected);
super::sandbox_backend::HostVerification::none()
}
}
pub fn verify_child_host_policy(
pid: u32,
policy: &crate::policy::Policy,
) -> super::sandbox_backend::HostVerification {
verify_child_host_with_limits(pid, &ExpectedLimits::from_policy(policy))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SweepOutcome {
pub clean: bool,
pub killed: usize,
pub residual: Vec<i32>,
pub subreaper: bool,
pub blind: bool,
}
pub fn sweep_tree_by_nonce(nonce: &str, root_pid: u32) -> Option<SweepOutcome> {
#[cfg(target_os = "linux")]
{
Some(sweep_tree_by_nonce_linux(nonce, root_pid))
}
#[cfg(not(target_os = "linux"))]
{
let _ = (nonce, root_pid);
None
}
}
#[cfg(target_os = "linux")]
fn apply_child_plan_linux(
plan: &super::sandbox_backend::ChildEnforcementPlan,
) -> std::io::Result<()> {
if plan.new_pgroup {
if unsafe { libc::setpgid(0, 0) } != 0 {
return Err(std::io::Error::last_os_error());
}
}
set_rlimit_if_some(libc::RLIMIT_AS, plan.rlimit_as)?;
set_rlimit_if_some(libc::RLIMIT_NPROC, plan.rlimit_nproc)?;
set_rlimit_if_some(libc::RLIMIT_CPU, plan.rlimit_cpu)?;
set_rlimit_if_some(libc::RLIMIT_FSIZE, plan.rlimit_fsize)?;
if plan.landlock {
let mut write_roots = vec![plan.exec_root.clone()];
write_roots.extend(plan.extra_rw.iter().cloned());
let read_roots: Vec<std::path::PathBuf> = plan
.system_ro
.iter()
.map(std::path::PathBuf::from)
.collect();
crate::sandbox::linux::landlock::apply_policy(
&write_roots,
&read_roots,
plan.strip_read_on_write,
)
.map_err(|e| std::io::Error::other(format!("{e:?}")))?;
} else {
if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
return Err(std::io::Error::last_os_error());
}
}
if plan.harden_syscalls {
let socket_policy = if plan.net_deny {
crate::sandbox::linux::seccomp_netblock::SocketPolicy::UnixOnly
} else {
crate::sandbox::linux::seccomp_netblock::SocketPolicy::UnixAndIp
};
crate::sandbox::linux::seccomp_netblock::install_for_profile(
socket_policy,
crate::policy::SeccompProfile::AgentMin,
)
.map_err(|e| std::io::Error::other(format!("{e:?}")))?;
}
Ok(())
}
#[cfg(target_os = "linux")]
fn set_rlimit_if_some(
resource: libc::__rlimit_resource_t,
value: Option<u64>,
) -> std::io::Result<()> {
let Some(value) = value else {
return Ok(());
};
let limit = libc::rlimit {
rlim_cur: value as libc::rlim_t,
rlim_max: value as libc::rlim_t,
};
if unsafe { libc::setrlimit(resource, &limit) } != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(target_os = "linux")]
fn verify_child_host_linux(
pid: u32,
expected: Option<&ExpectedLimits>,
) -> super::sandbox_backend::HostVerification {
use super::sandbox_backend::HostVerification;
use std::time::{Duration, Instant};
let deadline = Instant::now() + Duration::from_secs(2);
let mut out = HostVerification::none();
loop {
let status = read_proc_file(pid, "status");
if let Some(body) = status.as_deref() {
if proc_field_is(body, "Seccomp:", "2") {
out.seccomp_filter = true;
}
if proc_field_is(body, "NoNewPrivs:", "1") {
out.no_new_privs = true;
}
}
let pgid = unsafe { libc::getpgid(pid as libc::pid_t) };
if pgid == pid as libc::pid_t {
out.pgroup_separate = true;
}
if let (Ok(child_netns), Ok(host_netns)) = (
std::fs::read_link(format!("/proc/{pid}/ns/net")),
std::fs::read_link("/proc/self/ns/net"),
) {
if child_netns != host_netns {
out.netns_isolated = true;
}
}
if let Some(limits) = read_proc_file(pid, "limits").as_deref() {
if let Some(exp) = expected {
if let Some(val) = exp.rlimit_as {
out.rlimit_as_ok = limits_field_is(limits, "Max address space", val);
} else if limits_field_is(limits, "Max address space", DEFAULT_RLIMIT_AS_BYTES) {
out.rlimit_as_ok = true;
}
if let Some(val) = exp.rlimit_nproc {
out.rlimit_nproc_ok = limits_field_is(limits, "Max processes", val);
} else if limits_field_is(limits, "Max processes", DEFAULT_RLIMIT_NPROC) {
out.rlimit_nproc_ok = true;
}
if let Some(val) = exp.rlimit_cpu {
out.rlimit_cpu_ok = limits_field_is(limits, "Max cpu time", val);
} else if limits_field_is(limits, "Max cpu time", DEFAULT_RLIMIT_CPU_SECS) {
out.rlimit_cpu_ok = true;
}
if let Some(val) = exp.rlimit_fsize {
out.rlimit_fsize_ok = limits_field_is(limits, "Max file size", val);
} else if limits_field_is(limits, "Max file size", DEFAULT_RLIMIT_FSIZE_BYTES) {
out.rlimit_fsize_ok = true;
}
} else {
if limits_field_is(limits, "Max address space", DEFAULT_RLIMIT_AS_BYTES) {
out.rlimit_as_ok = true;
} else if let Some((soft, hard)) =
parse_proc_limits_value(limits, "Max address space")
{
if soft == hard && soft > 0 {
out.rlimit_as_ok = true;
}
}
if limits_field_is(limits, "Max processes", DEFAULT_RLIMIT_NPROC) {
out.rlimit_nproc_ok = true;
} else if let Some((soft, hard)) = parse_proc_limits_value(limits, "Max processes")
{
if soft == hard && soft > 0 {
out.rlimit_nproc_ok = true;
}
}
if limits_field_is(limits, "Max cpu time", DEFAULT_RLIMIT_CPU_SECS) {
out.rlimit_cpu_ok = true;
} else if let Some((soft, hard)) = parse_proc_limits_value(limits, "Max cpu time") {
if soft == hard && soft > 0 {
out.rlimit_cpu_ok = true;
}
}
if limits_field_is(limits, "Max file size", DEFAULT_RLIMIT_FSIZE_BYTES) {
out.rlimit_fsize_ok = true;
} else if let Some((soft, hard)) = parse_proc_limits_value(limits, "Max file size")
{
if soft == hard && soft > 0 {
out.rlimit_fsize_ok = true;
}
}
}
}
if let Some(cg) = inspect_child_cgroup(pid) {
if let Some(exp) = expected {
if let Some(want_mem) = &exp.cgroup_memory_max {
if let Some(actual) = &cg.memory_max {
out.cgroup_memory_ok = cgroup_memory_matches(actual, want_mem);
}
} else if let Some(actual) = &cg.memory_max {
if actual != "max" && !actual.is_empty() {
out.cgroup_memory_ok = true;
}
}
if let Some(want_pids) = &exp.cgroup_pids_max {
if let Some(actual) = &cg.pids_max {
out.cgroup_pids_ok = cgroup_pids_matches(actual, want_pids);
}
} else if let Some(actual) = &cg.pids_max {
if actual != "max" && !actual.is_empty() {
out.cgroup_pids_ok = true;
}
}
if let Some(want_cpu) = &exp.cgroup_cpu_max {
if let Some(actual) = &cg.cpu_max {
out.cgroup_cpu_ok = cgroup_cpu_matches(actual, want_cpu);
}
} else if let Some(actual) = &cg.cpu_max {
if actual != "max 100000" && !actual.starts_with("max") && !actual.is_empty() {
out.cgroup_cpu_ok = true;
}
}
} else {
if let Some(actual) = &cg.memory_max {
if actual != "max" && !actual.is_empty() {
out.cgroup_memory_ok = true;
}
}
if let Some(actual) = &cg.pids_max {
if actual != "max" && !actual.is_empty() {
out.cgroup_pids_ok = true;
}
}
if let Some(actual) = &cg.cpu_max {
if actual != "max 100000" && !actual.starts_with("max") && !actual.is_empty() {
out.cgroup_cpu_ok = true;
}
}
}
}
out.subreaper_ok = is_child_subreaper();
let zombie = match status.as_deref() {
Some(body) => pid_is_zombie(body),
None => false,
};
if out.all_observed() || Instant::now() >= deadline || !pid_alive(pid) || zombie {
return out;
}
std::thread::sleep(Duration::from_millis(25));
}
}
#[cfg(target_os = "linux")]
fn sweep_tree_by_nonce_linux(nonce: &str, root_pid: u32) -> SweepOutcome {
use std::time::{Duration, Instant};
let subreaper = is_child_subreaper();
let mut outcome = SweepOutcome {
clean: false,
killed: 0,
residual: Vec::new(),
subreaper,
blind: false,
};
if !subreaper {
outcome.blind = true;
return outcome;
}
let me = unsafe { libc::getpid() } as u32;
let me_uid = unsafe { libc::geteuid() };
let needle = nonce.as_bytes();
let deadline = Instant::now() + Duration::from_millis(SWEEP_BUDGET_MS);
loop {
let (matched, blind) = scan_nonce_pids(needle, root_pid, me, me_uid);
if blind {
outcome.blind = true;
}
if matched.is_empty() {
if !outcome.blind {
outcome.clean = true;
}
return outcome;
}
for pid in &matched {
if unsafe { libc::kill(*pid, libc::SIGKILL) } == 0 {
outcome.killed += 1;
}
let mut status = 0i32;
unsafe { libc::waitpid(*pid, &mut status, libc::WNOHANG) };
}
if Instant::now() >= deadline {
if blind {
outcome.blind = true;
}
outcome.residual = last_nonce_pids(nonce, root_pid, me);
return outcome;
}
std::thread::sleep(Duration::from_millis(10));
}
}
#[cfg(target_os = "linux")]
fn scan_nonce_pids(needle: &[u8], root_pid: u32, me: u32, me_uid: libc::uid_t) -> (Vec<i32>, bool) {
let mut matched = Vec::new();
let mut blind = false;
let Ok(entries) = std::fs::read_dir("/proc") else {
return (matched, true);
};
for entry in entries.flatten() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
let Ok(pid) = name.parse::<i32>() else {
continue;
};
if pid <= 0 || pid as u32 == root_pid || pid as u32 == me {
continue;
}
let status = match std::fs::read_to_string(format!("/proc/{pid}/status")) {
Ok(s) => s,
Err(e)
if e.raw_os_error() == Some(libc::ENOENT)
|| e.raw_os_error() == Some(libc::ESRCH) =>
{
continue;
}
Err(_) => continue,
};
if let Some(uid) = status_uid(&status) {
if uid != me_uid {
continue;
}
}
let env = match std::fs::read(format!("/proc/{pid}/environ")) {
Ok(env) => env,
Err(e)
if e.raw_os_error() == Some(libc::ENOENT)
|| e.raw_os_error() == Some(libc::ESRCH) =>
{
continue;
}
Err(_) => {
if pid_is_zombie(&status) || !pid_alive(pid as u32) {
continue;
}
if let Ok(latest_status) = std::fs::read_to_string(format!("/proc/{pid}/status")) {
if pid_is_zombie(&latest_status) {
continue;
}
} else {
continue;
}
if crate::sandbox::linux::proctrack::ppid_from_status(&status) == Some(me) {
blind = true;
}
continue;
}
};
if contains_slice(&env, needle) {
matched.push(pid);
} else if (env.is_empty() || !contains_slice(&env, needle))
&& pid_alive(pid as u32)
&& !pid_is_zombie(&status)
&& crate::sandbox::linux::proctrack::ppid_from_status(&status) == Some(me)
{
let my_sid = crate::sandbox::linux::proctrack::session_of(0);
let their_sid = crate::sandbox::linux::proctrack::session_of(pid);
if match (my_sid, their_sid) {
(Some(mine), Some(theirs)) => mine != theirs,
_ => false,
} {
blind = true;
matched.push(pid);
}
}
}
(matched, blind)
}
#[cfg(target_os = "linux")]
fn last_nonce_pids(nonce: &str, root_pid: u32, me: u32) -> Vec<i32> {
let mut out = Vec::new();
let needle = nonce.as_bytes();
if let Ok(entries) = std::fs::read_dir("/proc") {
for entry in entries.flatten() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
let Ok(pid) = name.parse::<i32>() else {
continue;
};
if pid <= 0 || pid as u32 == root_pid || pid as u32 == me {
continue;
}
let Ok(env) = std::fs::read(format!("/proc/{pid}/environ")) else {
continue;
};
if contains_slice(&env, needle) {
out.push(pid);
}
}
}
out.sort_unstable();
out.truncate(8);
out
}
#[cfg(target_os = "linux")]
fn pid_is_zombie(status: &str) -> bool {
for line in status.lines() {
if let Some(rest) = line.trim_start().strip_prefix("State:") {
let s = rest.trim_start();
return s.starts_with('Z') || s.starts_with('X');
}
}
false
}
#[cfg(target_os = "linux")]
fn pid_alive(pid: u32) -> bool {
unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
}
#[cfg(target_os = "linux")]
fn read_proc_file(pid: u32, file: &str) -> Option<String> {
std::fs::read_to_string(format!("/proc/{pid}/{file}")).ok()
}
pub fn proc_field_is(status_body: &str, field: &str, expected: &str) -> bool {
for line in status_body.lines() {
if let Some(rest) = line.trim_start().strip_prefix(field) {
return rest.trim() == expected;
}
}
false
}
pub fn limits_field_is(limits_body: &str, row: &str, expected: u64) -> bool {
for line in limits_body.lines() {
if let Some(idx) = line.find(row) {
let after = line[idx + row.len()..].trim_start();
let mut cols = after.split_whitespace();
let soft = cols.next().unwrap_or("");
let hard = cols.next().unwrap_or("");
let want = expected.to_string();
return soft == want && hard == want;
}
}
false
}
pub fn parse_proc_limits_value(limits_body: &str, row: &str) -> Option<(u64, u64)> {
for line in limits_body.lines() {
if let Some(idx) = line.find(row) {
let after = line[idx + row.len()..].trim_start();
let mut cols = after.split_whitespace();
let soft_str = cols.next()?;
let hard_str = cols.next()?;
if soft_str.eq_ignore_ascii_case("unlimited")
|| hard_str.eq_ignore_ascii_case("unlimited")
{
return None;
}
let soft = soft_str.parse::<u64>().ok()?;
let hard = hard_str.parse::<u64>().ok()?;
return Some((soft, hard));
}
}
None
}
pub fn child_cgroup_dir(pid: u32) -> Option<std::path::PathBuf> {
let content = std::fs::read_to_string(format!("/proc/{pid}/cgroup")).ok()?;
for line in content.lines() {
if let Some(path_part) = line.strip_prefix("0::") {
let rel = path_part.trim().trim_start_matches('/');
let cgroup_dir = std::path::Path::new("/sys/fs/cgroup").join(rel);
if cgroup_dir.exists() && cgroup_dir.join("cgroup.procs").exists() {
return Some(cgroup_dir);
}
}
}
None
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CgroupHostInspection {
pub cgroup_dir: std::path::PathBuf,
pub pid_in_procs: bool,
pub memory_max: Option<String>,
pub pids_max: Option<String>,
pub cpu_max: Option<String>,
pub swap_max: Option<String>,
}
pub fn inspect_child_cgroup(pid: u32) -> Option<CgroupHostInspection> {
let dir = child_cgroup_dir(pid)?;
let mut inspection = CgroupHostInspection {
cgroup_dir: dir.clone(),
pid_in_procs: false,
memory_max: None,
pids_max: None,
cpu_max: None,
swap_max: None,
};
if let Ok(procs) = std::fs::read_to_string(dir.join("cgroup.procs")) {
let pid_s = pid.to_string();
inspection.pid_in_procs = procs.lines().any(|l| l.trim() == pid_s);
}
if let Ok(val) = std::fs::read_to_string(dir.join("memory.max")) {
inspection.memory_max = Some(val.trim().to_string());
}
if let Ok(val) = std::fs::read_to_string(dir.join("pids.max")) {
inspection.pids_max = Some(val.trim().to_string());
}
if let Ok(val) = std::fs::read_to_string(dir.join("cpu.max")) {
inspection.cpu_max = Some(val.trim().to_string());
}
if let Ok(val) = std::fs::read_to_string(dir.join("memory.swap.max")) {
inspection.swap_max = Some(val.trim().to_string());
}
Some(inspection)
}
fn parse_memory_bytes(input: &str) -> Option<String> {
let s = input.trim();
if s.is_empty() || s.eq_ignore_ascii_case("max") {
return Some("max".to_string());
}
let (num_part, unit_part) = match s.find(|c: char| !c.is_ascii_digit() && c != '.') {
Some(idx) => (&s[..idx], s[idx..].trim().to_uppercase()),
None => (s, String::new()),
};
let num: f64 = num_part.parse().ok()?;
let multiplier: f64 = match unit_part.as_str() {
"" | "B" => 1.0,
"K" | "KB" | "KIB" => 1024.0,
"M" | "MB" | "MIB" => 1024.0 * 1024.0,
"G" | "GB" | "GIB" => 1024.0 * 1024.0 * 1024.0,
"T" | "TB" | "TIB" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
_ => return None,
};
let bytes = (num * multiplier) as u64;
Some(bytes.to_string())
}
fn parse_cpu_max(input: &str) -> Option<String> {
let s = input.trim();
if s.is_empty() || s.eq_ignore_ascii_case("max") {
return Some("max 100000".to_string());
}
if s.ends_with('%') {
let pct_str = s.trim_end_matches('%').trim();
let pct: f64 = pct_str.parse().ok()?;
let period = 100_000u64;
let quota = ((pct / 100.0) * period as f64) as u64;
return Some(format!("{quota} {period}"));
}
if s.contains(' ') {
return Some(s.to_string());
}
if let Ok(quota) = s.parse::<u64>() {
return Some(format!("{quota} 100000"));
}
None
}
pub fn cgroup_memory_matches(actual_bytes_str: &str, expected: &str) -> bool {
let actual = actual_bytes_str.trim();
if actual == expected.trim() {
return true;
}
if let Some(parsed) = parse_memory_bytes(expected) {
if parsed == actual {
return true;
}
}
false
}
pub fn cgroup_cpu_matches(actual_cpu_str: &str, expected: &str) -> bool {
let actual = actual_cpu_str.trim();
if actual == expected.trim() {
return true;
}
if let Some(parsed) = parse_cpu_max(expected) {
if parsed == actual {
return true;
}
}
false
}
pub fn cgroup_pids_matches(actual_pids_str: &str, expected: &str) -> bool {
actual_pids_str.trim() == expected.trim()
}
pub fn contains_slice(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || needle.len() > haystack.len() {
return false;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}
pub fn status_uid(status_body: &str) -> Option<u32> {
for line in status_body.lines() {
if let Some(rest) = line.trim_start().strip_prefix("Uid:") {
let first = rest.split_whitespace().next().unwrap_or("");
return first.parse::<u32>().ok();
}
}
None
}
#[cfg(test)]
mod linux_enforce_tests {
use super::*;
#[test]
fn proc_field_matches_status_shapes() {
let body = "Name:\tsh\nState:\tS (sleeping)\nSeccomp:\t2\nNoNewPrivs:\t1\n";
assert!(proc_field_is(body, "Seccomp:", "2"));
assert!(proc_field_is(body, "NoNewPrivs:", "1"));
assert!(!proc_field_is(body, "Seccomp:", "0"));
assert!(!proc_field_is(body, "Missing:", "2"));
assert!(!proc_field_is("", "Seccomp:", "2"));
}
#[test]
fn limits_field_matches_both_columns() {
let body = "Limit Soft Limit Hard Limit Units\n\
Max cpu time 5 5 seconds\n\
Max file size 67108864 67108864 bytes\n\
Max processes 128 128 processes\n\
Max address space 268435456 268435456 bytes\n\
Max open files 1024 1024 files\n";
assert!(limits_field_is(body, "Max cpu time", 5));
assert!(limits_field_is(body, "Max file size", 67_108_864));
assert!(limits_field_is(body, "Max processes", 128));
assert!(limits_field_is(body, "Max address space", 268_435_456));
assert!(!limits_field_is(body, "Max cpu time", 6));
assert!(!limits_field_is(body, "Max open files", 512));
assert!(!limits_field_is(body, "No such row", 0));
}
#[test]
fn limits_field_rejects_unlimited_and_split() {
let body = "Max cpu time unlimited unlimited seconds\n\
Max processes 128 64 processes\n";
assert!(!limits_field_is(body, "Max cpu time", 5));
assert!(!limits_field_is(body, "Max processes", 128));
}
#[test]
fn contains_slice_finds_nonce_in_environ() {
let env = b"PATH=/bin\x00VETTO_VNG_NONCE=abc123\x00HOME=/x\x00";
assert!(contains_slice(env, b"abc123"));
assert!(contains_slice(env, b"VETTO_VNG_NONCE"));
assert!(!contains_slice(env, b"other"));
assert!(!contains_slice(env, b""));
assert!(!contains_slice(b"short", b"much-longer-needle"));
}
#[test]
fn status_uid_parses_first_column() {
let body = "Name:\tsleep\nState:\tS (sleeping)\nUid:\t1000\t1000\t1000\t1000\n";
assert_eq!(status_uid(body), Some(1000));
assert_eq!(status_uid("Uid:\t0\t0\t0\t0\n"), Some(0));
assert_eq!(status_uid("Name:\tx\nState:\tR (running)\n"), None);
assert_eq!(status_uid(""), None);
assert_eq!(status_uid("Uid:\tnot-a-number\n"), None);
}
#[cfg(target_os = "linux")]
#[test]
fn subreaper_query_is_deterministic() {
let a = is_child_subreaper();
let b = is_child_subreaper();
assert_eq!(a, b);
}
#[test]
fn parse_proc_limits_value_parses_finite_and_rejects_unlimited() {
let body = "Limit Soft Limit Hard Limit Units\n\
Max cpu time 5 5 seconds\n\
Max file size unlimited unlimited bytes\n\
Max processes 128 128 processes\n";
assert_eq!(parse_proc_limits_value(body, "Max cpu time"), Some((5, 5)));
assert_eq!(parse_proc_limits_value(body, "Max file size"), None);
assert_eq!(
parse_proc_limits_value(body, "Max processes"),
Some((128, 128))
);
assert_eq!(parse_proc_limits_value(body, "Max memory"), None);
}
#[test]
fn cgroup_matching_handles_units_and_percentages() {
assert!(cgroup_memory_matches("268435456", "256M"));
assert!(cgroup_memory_matches("104857600", "100MB"));
assert!(cgroup_memory_matches("1000", "1000"));
assert!(!cgroup_memory_matches("268435456", "512M"));
assert!(cgroup_cpu_matches("50000 100000", "50%"));
assert!(cgroup_cpu_matches("100000 100000", "100%"));
assert!(cgroup_cpu_matches("50000 100000", "50000 100000"));
assert!(!cgroup_cpu_matches("50000 100000", "100%"));
assert!(cgroup_pids_matches("128", "128"));
assert!(!cgroup_pids_matches("128", "256"));
}
#[test]
fn expected_limits_from_policy_maps_fields() {
let mut policy = crate::policy::Policy::default();
policy.limits.address_space_bytes = Some(268435456);
policy.limits.processes = Some(128);
policy.cgroup = Some(crate::policy::CgroupConfig {
memory_max: Some("256M".to_string()),
pids_max: Some("128".to_string()),
swap_max: None,
cpu_max: Some("50%".to_string()),
});
let expected = ExpectedLimits::from_policy(&policy);
assert_eq!(expected.rlimit_as, Some(268435456));
assert_eq!(expected.rlimit_nproc, Some(128));
assert_eq!(expected.cgroup_memory_max.as_deref(), Some("256M"));
assert_eq!(expected.cgroup_pids_max.as_deref(), Some("128"));
assert_eq!(expected.cgroup_cpu_max.as_deref(), Some("50%"));
}
}