mod common;
use assert_cmd::Command;
use boxlite_test_utils::home::PerTestBoxHome;
use std::path::{Path, PathBuf};
use std::time::Duration;
fn boxlite(home: &Path, args: &[&str], timeout: Duration) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_boxlite"))
.arg("--home")
.arg(home)
.arg("--config")
.arg(common::write_registry_config(home))
.args(args)
.timeout(timeout)
.output()
.expect("spawn boxlite")
}
struct BoxCleanup {
home: PathBuf,
id: String,
}
impl Drop for BoxCleanup {
fn drop(&mut self) {
let _ = boxlite(&self.home, &["rm", "-f", &self.id], Duration::from_secs(30));
}
}
struct SubtreeRestore {
subtree: PathBuf,
controllers: Vec<String>,
}
impl Drop for SubtreeRestore {
fn drop(&mut self) {
for controller in &self.controllers {
let _ = std::fs::write(&self.subtree, format!("+{controller}"));
}
}
}
fn uid() -> u32 {
std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse().ok())
.unwrap_or(u32::MAX)
}
fn boxlite_cgroup_parent() -> Option<PathBuf> {
let root = Path::new("/sys/fs/cgroup");
if !root.join("cgroup.controllers").exists() {
return None; }
if uid() == 0 {
return Some(root.join("boxlite"));
}
let user_scope = root
.join("user.slice")
.join(format!("user-{}.slice", uid()))
.join(format!("user@{}.service", uid()));
user_scope
.join("cgroup.controllers")
.exists()
.then(|| user_scope.join("boxlite"))
}
fn delegated_to(parent: &Path) -> Vec<String> {
let grandparent = parent.parent().unwrap_or(Path::new("/sys/fs/cgroup"));
std::fs::read_to_string(grandparent.join("cgroup.subtree_control"))
.unwrap_or_default()
.split_whitespace()
.map(str::to_string)
.collect()
}
fn cgroup_parent_or_skip(what: &str) -> Option<PathBuf> {
let Some(parent) = boxlite_cgroup_parent() else {
eprintln!("SKIP {what}: no cgroup v2 (or no systemd user manager for this uid)");
return None;
};
let delegated = delegated_to(&parent);
if !delegated.iter().any(|c| c == "pids") {
eprintln!(
"SKIP {what}: `pids` is not delegated to {} (delegated: {delegated:?}), so there is \
no pids.max to assert on",
parent.display()
);
return None;
}
let probe = parent.with_file_name(format!("boxlite-writeprobe-{}", std::process::id()));
match std::fs::create_dir(&probe) {
Ok(()) => {
let _ = std::fs::remove_dir(&probe);
Some(parent)
}
Err(e) => {
eprintln!(
"SKIP {what}: cannot create cgroups under {}: {e}",
parent.display()
);
None
}
}
}
fn start_box(home: &PerTestBoxHome) -> String {
let out = boxlite(
home.path.as_path(),
&[
"run",
"-d",
"--memory",
"128",
"alpine:latest",
"sleep",
"600",
],
Duration::from_secs(300),
);
assert!(
out.status.success(),
"box start failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn box_cgroup_has_the_default_pids_limit_applied() {
let Some(parent) = cgroup_parent_or_skip("box_cgroup_has_the_default_pids_limit_applied")
else {
return;
};
let home = PerTestBoxHome::new();
let box_id = start_box(&home);
let _cleanup = BoxCleanup {
home: home.path.clone(),
id: box_id.clone(),
};
let pids_max = parent.join(&box_id).join("pids.max");
let value = std::fs::read_to_string(&pids_max).unwrap_or_else(|e| {
panic!(
"{} must exist and be readable: {e}. An absent file means the parent's \
cgroup.subtree_control never got `pids`, so the kernel never created the \
interface file and the limit was never enforced.",
pids_max.display()
)
});
assert_eq!(
value.trim(),
"1024",
"default host pids cap must be applied to the box cgroup at {}",
pids_max.display()
);
}
#[test]
fn a_parent_left_undelegated_is_repaired_by_the_next_box() {
let Some(parent) =
cgroup_parent_or_skip("a_parent_left_undelegated_is_repaired_by_the_next_box")
else {
return;
};
if let Ok(entries) = std::fs::read_dir(&parent)
&& entries.filter_map(|e| e.ok()).any(|e| e.path().is_dir())
{
eprintln!(
"SKIP a_parent_left_undelegated_is_repaired_by_the_next_box: {} already has box \
cgroups; resetting its delegation would disturb them",
parent.display()
);
return;
}
if !parent.exists() {
std::fs::create_dir(&parent).expect("create boxlite parent cgroup");
}
let subtree = parent.join("cgroup.subtree_control");
let before: Vec<String> = std::fs::read_to_string(&subtree)
.unwrap_or_default()
.split_whitespace()
.map(str::to_string)
.collect();
let under_test: Vec<String> = before
.iter()
.filter(|c| matches!(c.as_str(), "cpu" | "memory" | "pids"))
.cloned()
.collect();
for controller in &under_test {
let _ = std::fs::write(&subtree, format!("-{controller}"));
}
let _restore = SubtreeRestore {
subtree: subtree.clone(),
controllers: under_test.clone(),
};
let still_on = std::fs::read_to_string(&subtree).unwrap_or_default();
if still_on
.split_whitespace()
.any(|c| matches!(c, "cpu" | "memory" | "pids"))
{
eprintln!(
"SKIP a_parent_left_undelegated_is_repaired_by_the_next_box: could not undelegate \
cpu/memory/pids (still {still_on:?})"
);
return;
}
let home = PerTestBoxHome::new();
let box_id = start_box(&home);
let _cleanup = BoxCleanup {
home: home.path.clone(),
id: box_id.clone(),
};
let pids_max = parent.join(&box_id).join("pids.max");
let value = std::fs::read_to_string(&pids_max).unwrap_or_else(|e| {
panic!(
"{} must exist after starting a box under an undelegated parent: {e}. \
This is the regression: delegation ran only at parent-creation time, so a \
parent left behind undelegated was never repaired and every later box ran \
unconfined.",
pids_max.display()
)
});
assert_eq!(
value.trim(),
"1024",
"the box started under a stuck parent must still get its limits at {}",
pids_max.display()
);
}