use std::fs;
use std::io;
use std::os::fd::OwnedFd;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
const CGROUP_V2_ROOT: &str = "/sys/fs/cgroup";
const CONTROLLERS_FILE: &str = "cgroup.controllers";
const SUBTREE_CONTROL_FILE: &str = "cgroup.subtree_control";
const PROCS_FILE: &str = "cgroup.procs";
const KILL_FILE: &str = "cgroup.kill";
const PIDS_MAX_FILE: &str = "pids.max";
const PIDS_PEAK_FILE: &str = "pids.peak";
const MEMORY_MAX_FILE: &str = "memory.max";
static PROBE_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct CgroupLimits {
pub pids_max: Option<u64>,
pub memory_max: Option<u64>,
}
impl CgroupLimits {
#[must_use]
pub fn with_pids_max(pids_max: u64) -> Self {
Self {
pids_max: Some(pids_max),
memory_max: None,
}
}
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn and_memory_max(self, memory_max: u64) -> Self {
Self {
memory_max: Some(memory_max),
..self
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct CgroupSetup {
pub pids_enforced: bool,
pub memory_enforced: bool,
}
#[derive(Debug)]
pub struct CgroupLeaf {
dir: PathBuf,
setup: CgroupSetup,
present: bool,
}
impl CgroupLeaf {
pub fn create(base: &Path, name: &str, limits: CgroupLimits) -> io::Result<Self> {
if name.is_empty() || name.contains('/') {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cgroup leaf name must be a single non-empty path component",
));
}
let dir = base.join(name);
fs::create_dir(&dir)?;
let mut leaf = Self {
dir,
setup: CgroupSetup::default(),
present: true,
};
match leaf.apply_limits(base, limits) {
Ok(setup) => {
leaf.setup = setup;
Ok(leaf)
}
Err(e) => {
let _ = fs::remove_dir(&leaf.dir);
leaf.present = false;
Err(e)
}
}
}
fn apply_limits(&self, base: &Path, limits: CgroupLimits) -> io::Result<CgroupSetup> {
let delegated = read_subtree_control(base)?;
if limits.pids_max.is_some() {
require_delegated(&delegated, "pids")?;
}
if limits.memory_max.is_some() {
require_delegated(&delegated, "memory")?;
}
let mut setup = CgroupSetup::default();
if let Some(pids_max) = limits.pids_max {
write_limit(&self.dir, PIDS_MAX_FILE, pids_max)?;
setup.pids_enforced = true;
}
if let Some(memory_max) = limits.memory_max {
write_limit(&self.dir, MEMORY_MAX_FILE, memory_max)?;
setup.memory_enforced = true;
}
Ok(setup)
}
pub fn dir(&self) -> io::Result<&Path> {
if self.present {
Ok(&self.dir)
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
"cgroup leaf has already been removed",
))
}
}
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn setup(&self) -> CgroupSetup {
self.setup
}
pub fn dir_fd(&self) -> io::Result<OwnedFd> {
let dir = self.dir()?;
let file = fs::File::open(dir)?;
Ok(OwnedFd::from(file))
}
pub fn member_pids(&self) -> io::Result<Vec<i32>> {
let path = self.dir()?.join(PROCS_FILE);
let text = fs::read_to_string(&path)?;
parse_procs(&text)
}
pub fn peak_pids(&self) -> io::Result<Option<u64>> {
let path = self.dir()?.join(PIDS_PEAK_FILE);
match fs::read_to_string(&path) {
Ok(text) => {
let peak = text.trim().parse::<u64>().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("pids.peak was not a number: {text:?}"),
)
})?;
Ok(Some(peak))
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
pub fn kill(&self) -> io::Result<()> {
let path = self.dir()?.join(KILL_FILE);
if !path.exists() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"cgroup.kill absent (kernel < 5.14): cannot atomically kill the subtree",
));
}
fs::write(&path, b"1")
}
pub fn wait_until_empty(&self, max_attempts: u32, poll_interval: Duration) -> io::Result<bool> {
for attempt in 0..max_attempts {
if self.member_pids()?.is_empty() {
return Ok(true);
}
if attempt + 1 < max_attempts {
std::thread::sleep(poll_interval);
}
}
Ok(self.member_pids()?.is_empty())
}
pub fn remove(&mut self) -> io::Result<()> {
if !self.present {
return Ok(());
}
fs::remove_dir(&self.dir)?;
self.present = false;
Ok(())
}
}
impl Drop for CgroupLeaf {
fn drop(&mut self) {
if self.present {
let _ = fs::remove_dir(&self.dir);
}
}
}
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn probe_cgroup_delegation() -> Option<PathBuf> {
let root = Path::new(CGROUP_V2_ROOT);
if !root.join(CONTROLLERS_FILE).exists() {
return None;
}
let own = own_v2_cgroup_dir(root)?;
if base_is_writable(&own) {
return Some(own);
}
let parent = own.parent()?;
if parent.starts_with(root) && base_is_writable(parent) {
return Some(parent.to_path_buf());
}
None
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct CgroupLeafCaps {
pub atomic_kill: bool,
pub pids_peak: bool,
}
#[must_use]
pub fn probe_leaf_caps(base: &Path) -> CgroupLeafCaps {
let suffix = PROBE_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let probe = base.join(format!(".bvisor-caps-probe-{pid}-{suffix}"));
if fs::create_dir(&probe).is_err() {
return CgroupLeafCaps::default();
}
let caps = CgroupLeafCaps {
atomic_kill: probe.join(KILL_FILE).exists(),
pids_peak: probe.join(PIDS_PEAK_FILE).exists(),
};
let _ = fs::remove_dir(&probe);
caps
}
#[must_use]
pub fn probe_controller_base(required: &[&str]) -> Option<PathBuf> {
let root = Path::new(CGROUP_V2_ROOT);
if !root.join(CONTROLLERS_FILE).exists() {
return None;
}
let own = own_v2_cgroup_dir(root)?;
find_delegating_base(root, &own, required)
}
fn find_delegating_base(root: &Path, start: &Path, required: &[&str]) -> Option<PathBuf> {
let mut current = Some(start);
while let Some(dir) = current {
if !dir.starts_with(root) {
break;
}
let delegated = read_subtree_control(dir).unwrap_or_default();
if delegates_all(&delegated, required) && base_is_writable(dir) {
return Some(dir.to_path_buf());
}
if dir == root {
break;
}
current = dir.parent();
}
None
}
fn delegates_all(delegated: &[String], required: &[&str]) -> bool {
required.iter().all(|r| delegated.iter().any(|d| d == r))
}
fn own_v2_cgroup_dir(root: &Path) -> Option<PathBuf> {
let text = fs::read_to_string("/proc/self/cgroup").ok()?;
let rel = own_v2_relative_path(&text)?;
let rel = rel.trim_start_matches('/');
Some(root.join(rel))
}
fn own_v2_relative_path(proc_cgroup: &str) -> Option<String> {
for line in proc_cgroup.lines() {
let mut parts = line.splitn(3, ':');
let hierarchy = parts.next()?;
let controllers = parts.next()?;
let path = parts.next()?;
if hierarchy == "0" && controllers.is_empty() {
return Some(path.to_string());
}
}
None
}
fn base_is_writable(base: &Path) -> bool {
let suffix = PROBE_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let probe = base.join(format!(".bvisor-probe-{pid}-{suffix}"));
match fs::create_dir(&probe) {
Ok(()) => fs::remove_dir(&probe).is_ok(),
Err(_) => false,
}
}
fn read_subtree_control(base: &Path) -> io::Result<Vec<String>> {
let path = base.join(SUBTREE_CONTROL_FILE);
match fs::read_to_string(&path) {
Ok(text) => Ok(text.split_whitespace().map(str::to_string).collect()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
fn require_delegated(delegated: &[String], controller: &str) -> io::Result<()> {
if delegated.iter().any(|c| c == controller) {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"the `{controller}` controller is not delegated (absent from \
cgroup.subtree_control); refusing to claim an unenforceable limit"
),
))
}
}
fn write_limit(dir: &Path, file: &str, value: u64) -> io::Result<()> {
fs::write(dir.join(file), value.to_string().as_bytes())
}
fn parse_procs(text: &str) -> io::Result<Vec<i32>> {
let mut pids = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let pid = line.parse::<i32>().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("cgroup.procs contained a non-numeric line: {line:?}"),
)
})?;
pids.push(pid);
}
Ok(pids)
}
#[cfg(test)]
#[path = "cgroup_tests.rs"]
mod tests;
#[cfg(test)]
mod topology_tests {
use super::*;
#[test]
fn parse_v2_relative_path_picks_the_unified_line() {
let sample = "12:pids:/user.slice\n0::/user.slice/user-1000.slice/session.scope\n";
assert_eq!(
own_v2_relative_path(sample).as_deref(),
Some("/user.slice/user-1000.slice/session.scope")
);
}
#[test]
fn parse_v2_relative_path_none_when_no_unified_line() {
assert_eq!(own_v2_relative_path("3:cpu:/foo\n2:memory:/bar\n"), None);
}
#[test]
fn delegates_all_requires_every_controller() {
let delegated = vec!["cpu".to_string(), "pids".to_string(), "memory".to_string()];
assert!(delegates_all(&delegated, &["pids"]));
assert!(delegates_all(&delegated, &["pids", "memory"]));
assert!(
delegates_all(&delegated, &[]),
"empty required ⇒ vacuously true"
);
assert!(
!delegates_all(&delegated, &["pids", "io"]),
"io is absent ⇒ not all delegated"
);
}
#[test]
fn find_delegating_base_walks_up_past_a_nondelegating_leaf() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
fs::write(root.join(CONTROLLERS_FILE), "cpu io memory pids").expect("root controllers");
let parent = root.join("delegating-parent"); let leaf = parent.join("nondelegating-leaf"); fs::create_dir_all(&leaf).expect("ancestor chain");
fs::write(parent.join(SUBTREE_CONTROL_FILE), "cpu io memory pids").expect("parent subtree");
fs::write(leaf.join(SUBTREE_CONTROL_FILE), "").expect("leaf subtree empty");
let base = find_delegating_base(root, &leaf, &["pids"])
.expect("a pids-delegating writable ancestor exists");
assert_eq!(
base, parent,
"must skip the nondelegating leaf, return the parent"
);
let littered = fs::read_dir(&leaf)
.expect("read leaf")
.filter_map(Result::ok)
.any(|e| {
e.file_name()
.to_string_lossy()
.starts_with(".bvisor-probe-")
});
assert!(!littered, "no probe dir under a non-delegating cgroup");
}
#[test]
fn find_delegating_base_is_none_when_no_ancestor_delegates() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
fs::write(root.join(CONTROLLERS_FILE), "cpu pids").expect("root controllers");
let leaf = root.join("a").join("b");
fs::create_dir_all(&leaf).expect("chain");
fs::write(root.join("a").join(SUBTREE_CONTROL_FILE), "cpu").expect("a subtree");
fs::write(leaf.join(SUBTREE_CONTROL_FILE), "").expect("b subtree");
assert!(
find_delegating_base(root, &leaf, &["pids"]).is_none(),
"no pids-delegating ancestor ⇒ honest None, never a guess"
);
}
#[test]
fn probe_leaf_caps_are_false_when_no_cgroup_files_materialise() {
let tmp = tempfile::tempdir().expect("tempdir");
let caps = probe_leaf_caps(tmp.path());
assert!(
!caps.atomic_kill && !caps.pids_peak,
"no cgroup interface files under a non-cgroup dir ⇒ all capabilities unavailable"
);
}
#[test]
fn require_delegated_distinguishes_present_from_absent() {
let delegated = vec!["pids".to_string(), "memory".to_string()];
assert!(require_delegated(&delegated, "pids").is_ok());
assert_eq!(
require_delegated(&delegated, "io")
.expect_err("io not delegated")
.kind(),
io::ErrorKind::Unsupported
);
}
}