use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use crate::resources::HostResourceLimits;
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const DEFAULT_CGROUP2_MOUNT: &str = "/sys/fs/cgroup";
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const CPU_MAX_PERIOD_US: u64 = 100_000;
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const REQUIRED_CONTROLLERS: &[&str] = &["memory", "pids", "cpu"];
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const SUPERVISOR_LEAF: &str = "murmur-supervisor";
#[derive(Debug)]
pub(crate) struct CgroupScope {
path: PathBuf,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
procs_fd: Option<std::os::fd::OwnedFd>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct CgroupEventCounters {
pub(crate) oom_kill: u64,
pub(crate) pids_max: u64,
}
impl CgroupEventCounters {
pub(crate) fn attribution_since(&self, before: Self) -> Option<&'static str> {
if self.oom_kill > before.oom_kill {
return Some("cgroup_memory_bytes");
}
if self.pids_max > before.pids_max {
return Some("cgroup_pids_max");
}
None
}
}
pub(crate) fn prepare_scope(
required: bool,
limits: &HostResourceLimits,
session_id: &str,
workdir: &Path,
) -> Result<Option<Arc<CgroupScope>>, String> {
if !required {
return Ok(None);
}
#[cfg(target_os = "linux")]
{
let scope = CgroupScope::create(limits, session_id, workdir)?;
Ok(Some(Arc::new(scope)))
}
#[cfg(not(target_os = "linux"))]
{
let _ = (limits, session_id, workdir);
Ok(None)
}
}
pub(crate) fn requires_process_bounding(
policy: &crate::types::CapabilityPolicy,
has_native_artifact: bool,
) -> bool {
!policy.shell_allow.is_empty() || !policy.spawn_allow.is_empty() || has_native_artifact
}
impl CgroupScope {
#[allow(dead_code)]
pub(crate) fn path(&self) -> &Path {
&self.path
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[allow(unsafe_code)]
pub(crate) fn join_current_process(&self) -> std::io::Result<()> {
use std::os::fd::AsRawFd;
let Some(fd) = self.procs_fd.as_ref() else {
return Ok(());
};
let pid = unsafe { libc::getpid() };
let mut buf = [0u8; 24];
let written = format_pid(pid, &mut buf);
let mut offset = 0;
while offset < written {
let rc = unsafe {
libc::write(
fd.as_raw_fd(),
buf[offset..written].as_ptr().cast::<libc::c_void>(),
written - offset,
)
};
if rc < 0 {
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(error);
}
offset += rc as usize;
}
Ok(())
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) fn event_counters(&self) -> CgroupEventCounters {
CgroupEventCounters {
oom_kill: read_event_counter(&self.path.join("memory.events"), "oom_kill"),
pids_max: read_event_counter(&self.path.join("pids.events"), "max"),
}
}
}
impl Drop for CgroupScope {
fn drop(&mut self) {
self.procs_fd = None;
if self.path.as_os_str().is_empty() {
return;
}
if let Err(error) = std::fs::remove_dir(&self.path) {
eprintln!(
"[capsule-runtime] note: could not remove cgroup scope {} ({error}); \
it will be reclaimed once its last task exits",
self.path.display()
);
}
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn format_pid(pid: i32, buf: &mut [u8; 24]) -> usize {
let mut digits = [0u8; 20];
let mut count = 0;
let mut value = pid.unsigned_abs() as u64;
if value == 0 {
digits[0] = b'0';
count = 1;
}
while value > 0 {
digits[count] = b'0' + (value % 10) as u8;
value /= 10;
count += 1;
}
let mut written = 0;
for index in (0..count).rev() {
buf[written] = digits[index];
written += 1;
}
buf[written] = b'\n';
written + 1
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn read_event_counter(path: &Path, key: &str) -> u64 {
let Ok(contents) = std::fs::read_to_string(path) else {
return 0;
};
parse_event_counter(&contents, key)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parse_event_counter(contents: &str, key: &str) -> u64 {
for line in contents.lines() {
let mut parts = line.split_whitespace();
if parts.next() == Some(key) {
return parts
.next()
.and_then(|value| value.parse().ok())
.unwrap_or(0);
}
}
0
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn scope_dir_name(session_id: &str) -> String {
static SERIAL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let sanitized: String = session_id
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect();
let pid = std::process::id();
if sanitized.is_empty() {
format!("murmur-{pid}-{serial}")
} else {
format!("murmur-{sanitized}-{pid}-{serial}")
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parse_unified_cgroup_path(contents: &str) -> Option<String> {
for line in contents.lines() {
let mut parts = line.splitn(3, ':');
let (Some("0"), Some(""), Some(path)) = (parts.next(), parts.next(), parts.next()) else {
continue;
};
if path.starts_with('/') {
return Some(path.to_string());
}
}
None
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parse_cgroup2_mount(contents: &str) -> String {
for line in contents.lines() {
let mut parts = line.split_whitespace();
let (Some(_source), Some(target), Some(fstype)) =
(parts.next(), parts.next(), parts.next())
else {
continue;
};
if fstype == "cgroup2" {
return target.to_string();
}
}
DEFAULT_CGROUP2_MOUNT.to_string()
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn missing_controllers(subtree_control: &str, wanted: &[&str]) -> Vec<String> {
let enabled: Vec<&str> = subtree_control.split_whitespace().collect();
wanted
.iter()
.filter(|controller| !enabled.contains(*controller))
.map(|controller| (*controller).to_string())
.collect()
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn device_major_minor(dev: u64) -> (u64, u64) {
let major = ((dev >> 8) & 0xfff) | ((dev >> 32) & !0xfff);
let minor = (dev & 0xff) | ((dev >> 12) & !0xff);
(major, minor)
}
#[cfg(target_os = "linux")]
impl CgroupScope {
fn create(
limits: &HostResourceLimits,
session_id: &str,
workdir: &Path,
) -> Result<Self, String> {
let base = delegated_base()?;
let path = base.join(scope_dir_name(session_id));
if let Err(error) = std::fs::create_dir(&path) {
if error.kind() != std::io::ErrorKind::AlreadyExists {
return Err(format!(
"could not create cgroup scope at {}: {error}",
path.display()
));
}
}
let scope = Self {
path,
procs_fd: None,
};
scope.write_limit("memory.max", &limits.cgroup_memory_bytes.to_string())?;
scope.write_limit("pids.max", &limits.cgroup_pids_max.to_string())?;
scope.write_limit(
"cpu.max",
&format!(
"{} {CPU_MAX_PERIOD_US}",
u64::from(limits.cgroup_cpu_percent) * (CPU_MAX_PERIOD_US / 100)
),
)?;
scope.apply_io_max(limits.cgroup_io_bytes_per_sec, workdir);
let procs_fd = open_write_only(&scope.path.join("cgroup.procs")).map_err(|error| {
format!(
"could not open {}/cgroup.procs for subprocess placement: {error}",
scope.path.display()
)
})?;
Ok(Self {
path: scope.take_path(),
procs_fd: Some(procs_fd),
})
}
fn take_path(mut self) -> PathBuf {
let path = std::mem::take(&mut self.path);
path
}
fn write_limit(&self, file: &str, value: &str) -> Result<(), String> {
let path = self.path.join(file);
std::fs::write(&path, format!("{value}\n"))
.map_err(|error| format!("could not write {} to {}: {error}", value, path.display()))
}
fn apply_io_max(&self, bytes_per_sec: u64, workdir: &Path) {
use std::os::linux::fs::MetadataExt;
let Ok(metadata) = workdir.metadata() else {
eprintln!(
"[capsule-runtime] note: cgroup io.max not applied — could not stat workdir {}",
workdir.display()
);
return;
};
let (major, minor) = device_major_minor(metadata.st_dev());
let value = format!("{major}:{minor} rbps={bytes_per_sec} wbps={bytes_per_sec}");
if let Err(error) = std::fs::write(self.path.join("io.max"), format!("{value}\n")) {
eprintln!(
"[capsule-runtime] note: cgroup io.max not applied ({error}); memory.max, \
pids.max and cpu.max are still enforced on this scope"
);
}
}
}
#[cfg(target_os = "linux")]
fn delegated_base() -> Result<PathBuf, String> {
static BASE: std::sync::Mutex<Option<PathBuf>> = std::sync::Mutex::new(None);
let mut cached = BASE.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(base) = cached.as_ref() {
return Ok(base.clone());
}
move_self_into_own_systemd_scope();
let base = probe_delegation()?;
enable_controllers(&base)?;
*cached = Some(base.clone());
Ok(base)
}
#[cfg(target_os = "linux")]
fn move_self_into_own_systemd_scope() {
static ATTEMPTED: std::sync::Once = std::sync::Once::new();
ATTEMPTED.call_once(|| {
let _ = request_transient_scope(&transient_scope_unit_name());
});
}
#[cfg(target_os = "linux")]
fn transient_scope_unit_name() -> String {
static SERIAL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("murmur-{}-{serial}.scope", std::process::id())
}
#[cfg(target_os = "linux")]
fn request_transient_scope(unit: &str) -> Result<(), String> {
let owned_unit = unit.to_string();
std::thread::Builder::new()
.name("murmur-cgroup-scope".to_string())
.spawn(move || start_transient_scope(&owned_unit))
.map_err(|error| format!("could not spawn the D-Bus thread: {error}"))?
.join()
.map_err(|_| "the D-Bus thread panicked".to_string())??;
await_scope_membership(unit)
}
#[cfg(target_os = "linux")]
fn start_transient_scope(unit: &str) -> Result<(), String> {
use zbus::zvariant::Value;
let connection = zbus::blocking::Connection::session()
.map_err(|error| format!("no systemd session bus: {error}"))?;
let properties: Vec<(&str, Value<'_>)> = vec![
(
"Description",
Value::from("Murmur capsule subprocess scope"),
),
("PIDs", Value::from(vec![std::process::id()])),
("Delegate", Value::from(true)),
("CollectMode", Value::from("inactive-or-failed")),
];
let aux: Vec<(&str, Vec<(&str, Value<'_>)>)> = Vec::new();
connection
.call_method(
Some("org.freedesktop.systemd1"),
"/org/freedesktop/systemd1",
Some("org.freedesktop.systemd1.Manager"),
"StartTransientUnit",
&(unit, "fail", properties, aux),
)
.map(|_| ())
.map_err(|error| format!("systemd refused to start {unit}: {error}"))
}
#[cfg(target_os = "linux")]
fn await_scope_membership(unit: &str) -> Result<(), String> {
let deadline = std::time::Instant::now() + Duration::from_secs(2);
loop {
let current = std::fs::read_to_string("/proc/self/cgroup").unwrap_or_default();
if let Some(path) = parse_unified_cgroup_path(¤t) {
if path.rsplit('/').next() == Some(unit) {
return Ok(());
}
}
if std::time::Instant::now() >= deadline {
return Err(format!(
"systemd accepted the request for {unit} but this process is not in it"
));
}
std::thread::sleep(Duration::from_millis(20));
}
}
#[cfg(target_os = "linux")]
fn probe_delegation() -> Result<PathBuf, String> {
let self_cgroup = std::fs::read_to_string("/proc/self/cgroup")
.map_err(|error| format!("could not read /proc/self/cgroup: {error}"))?;
let relative = parse_unified_cgroup_path(&self_cgroup).ok_or_else(|| {
"this host has no cgroup v2 unified hierarchy (no `0::` line in /proc/self/cgroup); \
cgroup v1-only and hybrid hosts cannot be delegated a v2 scope"
.to_string()
})?;
let mount = std::fs::read_to_string("/proc/mounts")
.map(|contents| parse_cgroup2_mount(&contents))
.unwrap_or_else(|_| DEFAULT_CGROUP2_MOUNT.to_string());
let base = ascend_out_of_supervisor_leaves(
PathBuf::from(mount).join(relative.trim_start_matches('/')),
);
if !base.is_dir() {
return Err(format!(
"delegated cgroup directory {} does not exist",
base.display()
));
}
let controllers =
std::fs::read_to_string(base.join("cgroup.controllers")).map_err(|error| {
format!(
"could not read {}/cgroup.controllers: {error}",
base.display()
)
})?;
let unavailable = missing_controllers(&controllers, REQUIRED_CONTROLLERS);
if !unavailable.is_empty() {
return Err(format!(
"cgroup controllers [{}] are not delegated to {}; the unit `mur` runs under needs \
`Delegate=yes` (or `Delegate=memory pids cpu io`)",
unavailable.join(", "),
base.display()
));
}
let probe_dir = base.join(format!("murmur-delegation-probe-{}", std::process::id()));
let _ = std::fs::remove_dir(&probe_dir);
std::fs::create_dir(&probe_dir).map_err(|error| {
format!(
"cannot create a child cgroup under {} ({error}); the unit `mur` runs under needs \
`Delegate=yes` for memory, pids, cpu and io",
base.display()
)
})?;
let _ = std::fs::remove_dir(&probe_dir);
Ok(base)
}
#[cfg(target_os = "linux")]
fn enable_controllers(base: &Path) -> Result<(), String> {
let subtree_control = base.join("cgroup.subtree_control");
let current = std::fs::read_to_string(&subtree_control).unwrap_or_default();
let available = std::fs::read_to_string(base.join("cgroup.controllers")).unwrap_or_default();
let mut wanted: Vec<&str> = REQUIRED_CONTROLLERS.to_vec();
if available.split_whitespace().any(|c| c == "io") {
wanted.push("io");
}
let missing = missing_controllers(¤t, &wanted);
if missing.is_empty() {
return Ok(());
}
let directive = missing
.iter()
.map(|controller| format!("+{controller}"))
.collect::<Vec<_>>()
.join(" ");
if std::fs::write(&subtree_control, format!("{directive}\n")).is_ok() {
return Ok(());
}
move_self_to_supervisor_leaf(base)?;
let deadline = std::time::Instant::now() + Duration::from_secs(2);
let mut last_error;
loop {
let current = std::fs::read_to_string(&subtree_control).unwrap_or_default();
if missing_controllers(¤t, &wanted).is_empty() {
return Ok(());
}
match std::fs::write(&subtree_control, format!("{directive}\n")) {
Ok(()) => return Ok(()),
Err(error) => last_error = Some(error),
}
if std::time::Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(20));
}
Err(format!(
"could not enable controllers [{}] in {} ({}); the unit `mur` runs under needs \
`Delegate=yes` for memory, pids, cpu and io",
missing.join(", "),
subtree_control.display(),
last_error.map_or_else(|| "still occupied".to_string(), |error| error.to_string()),
))
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn ascend_out_of_supervisor_leaves(mut base: PathBuf) -> PathBuf {
while base.file_name().and_then(|name| name.to_str()) == Some(SUPERVISOR_LEAF) {
base.pop();
}
base
}
#[cfg(target_os = "linux")]
fn move_self_to_supervisor_leaf(base: &Path) -> Result<(), String> {
let leaf = base.join(SUPERVISOR_LEAF);
if let Err(error) = std::fs::create_dir(&leaf) {
if error.kind() != std::io::ErrorKind::AlreadyExists {
return Err(format!(
"could not create {} to vacate the delegated cgroup: {error}",
leaf.display()
));
}
}
std::fs::write(
leaf.join("cgroup.procs"),
format!("{}\n", std::process::id()),
)
.map_err(|error| {
format!(
"could not move this process into {} to vacate the delegated cgroup: {error}",
leaf.display()
)
})
}
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
fn open_write_only(path: &Path) -> std::io::Result<std::os::fd::OwnedFd> {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.custom_flags(libc::O_CLOEXEC)
.open(path)
.map(std::os::fd::OwnedFd::from)
}
pub fn cgroup_delegation_available() -> bool {
#[cfg(not(target_os = "linux"))]
{
true
}
#[cfg(target_os = "linux")]
{
delegated_base().is_ok()
}
}
pub fn skip_without_host_support(test_name: &str) -> bool {
if crate::network_namespace::skip_without_egress_namespace(test_name) {
return true;
}
if !cgroup_delegation_available() {
eprintln!(
"[SKIP-HOST] {test_name}: this host cannot delegate a cgroup v2 scope, so a capsule \
that can spawn native subprocesses refuses to launch with E-RUN-012 before anything \
this test observes happens -- see \
docs/content/reference/resource-limits-manual-verification.md"
);
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::CapabilityPolicy;
#[test]
fn scope_is_required_by_any_route_to_a_native_subprocess() {
let shell = CapabilityPolicy {
shell_allow: vec!["bash".to_string()],
..CapabilityPolicy::default()
};
let spawn = CapabilityPolicy {
spawn_allow: vec!["child".to_string()],
..CapabilityPolicy::default()
};
let bare = CapabilityPolicy::default();
assert!(requires_process_bounding(&shell, false));
assert!(requires_process_bounding(&spawn, false));
assert!(
requires_process_bounding(&bare, true),
"a native artifact alone is enough"
);
assert!(
!requires_process_bounding(&bare, false),
"a WASM-only capsule has no process tree to bound and must not be blocked"
);
}
#[test]
fn prepare_scope_returns_none_when_bounding_is_not_required() {
let temp = tempfile::tempdir().unwrap();
let scope = prepare_scope(
false,
&HostResourceLimits::default(),
"ses_test",
temp.path(),
)
.expect("a capsule with no subprocess capability must never be refused");
assert!(scope.is_none());
}
#[cfg(not(target_os = "linux"))]
#[test]
fn prepare_scope_is_never_an_error_off_linux() {
let temp = tempfile::tempdir().unwrap();
let scope = prepare_scope(
true,
&HostResourceLimits::default(),
"ses_test",
temp.path(),
)
.expect("a non-Linux host must not refuse to launch for a missing cgroup");
assert!(scope.is_none());
}
#[cfg(target_os = "linux")]
#[test]
fn a_required_scope_is_created_whatever_cgroup_this_process_inherited() {
if skip_without_host_support(
"a_required_scope_is_created_whatever_cgroup_this_process_inherited",
) {
return;
}
let temp = tempfile::tempdir().unwrap();
let scope = prepare_scope(
true,
&HostResourceLimits::default(),
"ses_selftest",
temp.path(),
)
.expect("a Linux host with a systemd user session must be able to bound a subprocess tree")
.expect("a required scope on Linux is never `None`");
for file in ["memory.max", "pids.max", "cpu.max"] {
let contents = std::fs::read_to_string(scope.path().join(file))
.unwrap_or_else(|error| panic!("{file} unreadable in the scope: {error}"));
assert!(!contents.trim().is_empty(), "{file} was left unset");
assert_ne!(contents.trim(), "max", "{file} carries no ceiling");
}
}
#[cfg(target_os = "linux")]
#[test]
fn transient_scope_unit_names_are_murmur_prefixed_and_unique() {
let first = transient_scope_unit_name();
let second = transient_scope_unit_name();
assert!(first.starts_with(&format!("murmur-{}-", std::process::id())));
assert!(first.ends_with(".scope"));
assert_ne!(first, second);
}
#[test]
fn unified_cgroup_path_is_read_from_the_zero_hierarchy_line() {
let v2_only = "0::/user.slice/user-1000.slice/user@1000.service/app.slice/mur.scope\n";
assert_eq!(
parse_unified_cgroup_path(v2_only).as_deref(),
Some("/user.slice/user-1000.slice/user@1000.service/app.slice/mur.scope")
);
let hybrid = "12:pids:/user.slice\n11:memory:/user.slice\n0::/user.slice/session.scope\n";
assert_eq!(
parse_unified_cgroup_path(hybrid).as_deref(),
Some("/user.slice/session.scope")
);
let v1_only = "12:pids:/user.slice\n11:memory:/user.slice\n";
assert_eq!(parse_unified_cgroup_path(v1_only), None);
}
#[test]
fn cgroup2_mount_falls_back_to_the_conventional_path() {
let mounts = "cgroup2 /custom/cgroup cgroup2 rw,nsdelegate 0 0\n";
assert_eq!(parse_cgroup2_mount(mounts), "/custom/cgroup");
assert_eq!(
parse_cgroup2_mount("proc /proc proc rw 0 0\n"),
"/sys/fs/cgroup"
);
}
#[test]
fn missing_controllers_reports_only_what_is_absent() {
assert_eq!(
missing_controllers("cpu memory", REQUIRED_CONTROLLERS),
vec!["pids".to_string()]
);
assert!(missing_controllers("cpu io memory pids", REQUIRED_CONTROLLERS).is_empty());
assert_eq!(
missing_controllers("", REQUIRED_CONTROLLERS),
vec!["memory".to_string(), "pids".to_string(), "cpu".to_string()]
);
}
#[test]
fn event_counters_parse_the_named_key_only() {
let memory_events = "low 0\nhigh 0\nmax 12\noom 3\noom_kill 2\n";
assert_eq!(parse_event_counter(memory_events, "oom_kill"), 2);
assert_eq!(parse_event_counter(memory_events, "max"), 12);
assert_eq!(parse_event_counter("max 7\n", "max"), 7);
assert_eq!(parse_event_counter("", "oom_kill"), 0);
assert_eq!(parse_event_counter("oom_kill\n", "oom_kill"), 0);
}
#[test]
fn attribution_uses_the_delta_across_one_call() {
let before = CgroupEventCounters {
oom_kill: 1,
pids_max: 4,
};
assert_eq!(before.attribution_since(before), None);
assert_eq!(
CgroupEventCounters {
oom_kill: 2,
pids_max: 4
}
.attribution_since(before),
Some("cgroup_memory_bytes")
);
assert_eq!(
CgroupEventCounters {
oom_kill: 1,
pids_max: 5
}
.attribution_since(before),
Some("cgroup_pids_max")
);
}
#[test]
fn scope_dir_name_is_sanitized_and_never_empty() {
assert!(scope_dir_name("ses_abc123").starts_with("murmur-ses_abc123-"));
assert!(scope_dir_name("../escape").starts_with("murmur-.._escape-"));
assert!(!scope_dir_name("../escape").contains('/'));
assert!(scope_dir_name("").starts_with("murmur-"));
}
#[test]
fn delegated_base_ascends_out_of_inherited_supervisor_leaves() {
let base = PathBuf::from("/sys/fs/cgroup/user.slice/app.slice/mur.scope");
assert_eq!(ascend_out_of_supervisor_leaves(base.clone()), base);
assert_eq!(
ascend_out_of_supervisor_leaves(base.join(SUPERVISOR_LEAF)),
base
);
assert_eq!(
ascend_out_of_supervisor_leaves(base.join(SUPERVISOR_LEAF).join(SUPERVISOR_LEAF)),
base,
"a base already nested by an earlier build must still resolve to the real root"
);
}
#[test]
fn scope_dir_name_is_unique_per_call_for_one_id() {
let first = scope_dir_name("p");
let second = scope_dir_name("p");
assert_ne!(
first, second,
"same plan id must not reuse a scope directory"
);
}
#[test]
fn pid_is_formatted_without_allocating() {
let mut buf = [0u8; 24];
let len = format_pid(4321, &mut buf);
assert_eq!(&buf[..len], b"4321\n");
let len = format_pid(7, &mut buf);
assert_eq!(&buf[..len], b"7\n");
}
#[test]
fn device_numbers_use_the_wide_glibc_encoding() {
assert_eq!(device_major_minor(0x800), (8, 0));
assert_eq!(device_major_minor((259 << 8) | 3), (259, 3));
}
}