use std::collections::HashSet;
use std::io::{Seek, SeekFrom, Write};
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::seccomp::notif::{read_child_cstr, write_child_mem, NotifAction, NotifPolicy};
use crate::seccomp::state::{NetworkState, ProcessIndex};
use crate::sys::structs::{SeccompNotif, EACCES};
use crate::sys::syscall;
const SENSITIVE_PATHS: &[&str] = &[
"/proc/kcore",
"/proc/kmsg",
"/proc/kallsyms",
"/proc/keys",
"/proc/key-users",
"/proc/sysrq-trigger",
"/sys/class/net",
"/sys/firmware",
"/sys/kernel/security",
];
pub(crate) fn is_sensitive_proc(path: &str) -> bool {
SENSITIVE_PATHS
.iter()
.any(|&sensitive| path == sensitive || path.starts_with(&format!("{}/", sensitive)))
}
fn extract_proc_pid(path: &str) -> Option<i32> {
let rest = path.strip_prefix("/proc/")?;
let component = rest.split('/').next()?;
component.parse::<i32>().ok()
}
pub(crate) fn generate_cpuinfo(num_cpus: u32) -> Vec<u8> {
let mut buf = String::new();
for i in 0..num_cpus {
if i > 0 {
buf.push('\n');
}
buf.push_str(&format!(
"processor\t: {}\nmodel name\t: Virtual CPU\ncpu MHz\t\t: 2400.000\n",
i
));
}
buf.into_bytes()
}
pub(crate) fn generate_uptime(elapsed_secs: f64) -> Vec<u8> {
format!("{:.2} 0.00\n", elapsed_secs.max(0.0)).into_bytes()
}
#[derive(Debug, Clone)]
pub struct LoadAvg {
pub avg_1: f64,
pub avg_5: f64,
pub avg_15: f64,
}
const EXP_1: f64 = 0.9200444146293232; const EXP_5: f64 = 0.9834714538216174; const EXP_15: f64 = 0.9944598480048967;
impl LoadAvg {
pub fn new() -> Self {
Self { avg_1: 0.0, avg_5: 0.0, avg_15: 0.0 }
}
pub fn sample(&mut self, running: u32) {
let r = running as f64;
self.avg_1 = self.avg_1 * EXP_1 + r * (1.0 - EXP_1);
self.avg_5 = self.avg_5 * EXP_5 + r * (1.0 - EXP_5);
self.avg_15 = self.avg_15 * EXP_15 + r * (1.0 - EXP_15);
}
}
pub(crate) fn generate_loadavg(load: &LoadAvg, running: u32, total: u32, last_pid: i32) -> Vec<u8> {
format!(
"{:.2} {:.2} {:.2} {}/{} {}\n",
load.avg_1, load.avg_5, load.avg_15,
running.max(1).min(total), total,
last_pid.max(0),
)
.into_bytes()
}
pub(crate) fn generate_meminfo(total_bytes: u64, used_bytes: u64) -> Vec<u8> {
let total_kb = total_bytes / 1024;
let used_kb = used_bytes.min(total_bytes) / 1024;
let free_kb = total_kb.saturating_sub(used_kb);
let avail_kb = free_kb;
format!(
"MemTotal: {} kB\n\
MemFree: {} kB\n\
MemAvailable: {} kB\n",
total_kb, free_kb, avail_kb,
)
.into_bytes()
}
fn detect_fstype(path: &std::path::Path) -> &'static str {
let c_path = match std::ffi::CString::new(path.as_os_str().as_encoded_bytes()) {
Ok(p) => p,
Err(_) => return "unknown",
};
let mut buf: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(c_path.as_ptr(), &mut buf) } != 0 {
return "unknown";
}
match buf.f_type {
0xEF53 => "ext4", 0x9123683E => "btrfs", 0x58465342 => "xfs", 0x01021994 => "tmpfs", 0x6969 => "nfs", 0x5346544E => "ntfs", 0x65735546 => "fuse", 0x28cd3d45 => "cramfs", 0x3153464A => "jfs", 0x52654973 => "reiserfs", 0xF2F52010 => "f2fs", 0x4244 => "hfs", 0x482B => "hfsplus", 0x1021997 => "v9fs", 0xFF534D42 => "cifs", 0x73717368 => "squashfs", 0x62656572 => "sysfs", 0x9FA0 => "proc", 0x61756673 => "aufs", 0x794C7630 => "overlayfs", 0x01161970 => "gfs2", 0x5A4F4653 => "zonefs", 0xCAFE001 => "bcachefs", _ => "unknown",
}
}
pub(crate) fn generate_proc_mounts(
chroot_root: Option<&std::path::Path>,
chroot_mounts: &[(std::path::PathBuf, std::path::PathBuf)],
) -> Vec<u8> {
let mut buf = String::new();
if let Some(root) = chroot_root {
let fstype = detect_fstype(root);
buf.push_str(&format!("sandlock / {} rw,relatime 0 0\n", fstype));
} else {
buf.push_str("rootfs / rootfs rw 0 0\n");
}
for (virtual_path, host_path) in chroot_mounts {
let vp = virtual_path.to_string_lossy();
let fstype = detect_fstype(host_path);
buf.push_str(&format!("sandlock {} {} rw,relatime 0 0\n", vp, fstype));
}
buf.into_bytes()
}
pub(crate) fn generate_proc_mountinfo(
chroot_root: Option<&std::path::Path>,
chroot_mounts: &[(std::path::PathBuf, std::path::PathBuf)],
) -> Vec<u8> {
let mut buf = String::new();
let mut mount_id: u32 = 20;
if let Some(root) = chroot_root {
let fstype = detect_fstype(root);
buf.push_str(&format!(
"{} 1 8:1 / / rw,relatime - {} sandlock rw\n", mount_id, fstype
));
} else {
buf.push_str(&format!(
"{} 1 0:1 / / rw - rootfs rootfs rw\n", mount_id
));
}
mount_id += 1;
for (virtual_path, host_path) in chroot_mounts {
let vp = virtual_path.to_string_lossy();
let fstype = detect_fstype(host_path);
buf.push_str(&format!(
"{} 20 8:1 / {} rw,relatime - {} sandlock rw\n", mount_id, vp, fstype
));
mount_id += 1;
}
buf.into_bytes()
}
pub(crate) fn generate_proc_net_dev() -> Vec<u8> {
concat!(
"Inter-| Receive | Transmit\n",
" face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n",
" lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
).as_bytes().to_vec()
}
pub(crate) fn generate_proc_net_if_inet6() -> Vec<u8> {
b"00000000000000000000000000000001 01 80 10 80 lo\n".to_vec()
}
pub(crate) fn generate_proc_net_tcp(bound_ports: &HashSet<u16>, is_v6: bool) -> Vec<u8> {
let path = if is_v6 { "/proc/net/tcp6" } else { "/proc/net/tcp" };
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let mut result = String::new();
for (i, line) in content.lines().enumerate() {
if i == 0 {
result.push_str(line);
result.push('\n');
continue;
}
if let Some(local_port) = parse_proc_net_tcp_port(line) {
if bound_ports.contains(&local_port) {
result.push_str(line);
result.push('\n');
}
}
}
result.into_bytes()
}
fn parse_proc_net_tcp_port(line: &str) -> Option<u16> {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 2 {
return None;
}
let local = fields[1];
let colon = local.rfind(':')?;
let port_hex = &local[colon + 1..];
u16::from_str_radix(port_hex, 16).ok()
}
fn inject_memfd(content: &[u8]) -> NotifAction {
let memfd = match syscall::memfd_create(
"sandlock",
(libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING) as u32,
) {
Ok(fd) => fd,
Err(_) => return NotifAction::Continue, };
let raw = memfd.as_raw_fd();
{
let mut file = unsafe { std::fs::File::from_raw_fd(raw) };
if file.write_all(content).is_err() || file.seek(SeekFrom::Start(0)).is_err() {
std::mem::forget(file);
return NotifAction::Continue;
}
std::mem::forget(file);
}
let seals = libc::F_SEAL_SEAL | libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK;
unsafe { libc::fcntl(raw, libc::F_ADD_SEALS, seals) };
NotifAction::InjectFdSend { srcfd: memfd, newfd_flags: libc::O_CLOEXEC as u32 }
}
fn read_path(notif: &SeccompNotif, addr: u64, notif_fd: RawFd) -> Option<String> {
read_child_cstr(notif_fd, notif.id, notif.pid, addr, 4096)
}
pub(crate) async fn handle_proc_open(
notif: &SeccompNotif,
processes: &Arc<ProcessIndex>,
resource: &Arc<Mutex<crate::seccomp::state::ResourceState>>,
network: &Arc<Mutex<NetworkState>>,
policy: &NotifPolicy,
notif_fd: RawFd,
) -> NotifAction {
let path_ptr = notif.data.args[1];
let path = match read_path(notif, path_ptr, notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
if is_sensitive_proc(&path) {
return NotifAction::Errno(EACCES);
}
if let Some(pid) = extract_proc_pid(&path) {
if !processes.contains(pid) {
return NotifAction::Errno(EACCES);
}
}
if path == "/proc/cpuinfo" {
if let Some(num_cpus) = policy.num_cpus {
let content = generate_cpuinfo(num_cpus);
return inject_memfd(&content);
}
}
if path == "/proc/meminfo" && policy.max_memory_bytes > 0 {
let rs = resource.lock().await;
let content = generate_meminfo(policy.max_memory_bytes, rs.mem_used);
return inject_memfd(&content);
}
if path == "/proc/uptime" && policy.has_time_start {
let rs = resource.lock().await;
let elapsed = rs.start_instant.elapsed().as_secs_f64();
let content = generate_uptime(elapsed);
return inject_memfd(&content);
}
if path == "/proc/loadavg" {
let total = processes.len() as u32;
let last_pid = processes.max_pid().unwrap_or(0);
let rs = resource.lock().await;
let running = rs.proc_count;
let content = generate_loadavg(&rs.load_avg, running, total, last_pid);
return inject_memfd(&content);
}
if path == "/proc/net/dev" {
return inject_memfd(&generate_proc_net_dev());
}
if path == "/proc/net/if_inet6" {
return inject_memfd(&generate_proc_net_if_inet6());
}
if policy.port_remap && (path == "/proc/net/tcp" || path == "/proc/net/tcp6") {
let is_v6 = path.ends_with('6');
let ns = network.lock().await;
let content = generate_proc_net_tcp(&ns.port_map.bound_ports, is_v6);
return inject_memfd(&content);
}
if path == "/proc/mounts" || path == "/proc/self/mounts" {
let content = generate_proc_mounts(
policy.chroot_root.as_deref(),
&policy.chroot_mounts,
);
return inject_memfd(&content);
}
if path == "/proc/self/mountinfo" {
let content = generate_proc_mountinfo(
policy.chroot_root.as_deref(),
&policy.chroot_mounts,
);
return inject_memfd(&content);
}
NotifAction::Continue
}
pub(crate) fn handle_sched_getaffinity(
notif: &SeccompNotif,
num_cpus: u32,
notif_fd: RawFd,
) -> NotifAction {
let cpusetsize = notif.data.args[1] as usize;
let mask_addr = notif.data.args[2];
if mask_addr == 0 || cpusetsize == 0 {
return NotifAction::Continue;
}
let mut mask = vec![0u8; cpusetsize];
for i in 0..num_cpus as usize {
let byte_idx = i / 8;
let bit_idx = i % 8;
if byte_idx < mask.len() {
mask[byte_idx] |= 1 << bit_idx;
}
}
match write_child_mem(notif_fd, notif.id, notif.pid, mask_addr, &mask) {
Ok(()) => NotifAction::ReturnValue(cpusetsize as i64),
Err(_) => NotifAction::Continue,
}
}
pub(crate) fn handle_uname(
notif: &SeccompNotif,
hostname: &str,
notif_fd: RawFd,
) -> NotifAction {
let buf_addr = notif.data.args[0];
if buf_addr == 0 {
return NotifAction::Continue;
}
let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
if unsafe { libc::uname(&mut uts) } != 0 {
return NotifAction::Continue;
}
let name_bytes = hostname.as_bytes();
let len = name_bytes.len().min(uts.nodename.len() - 1);
for (i, &b) in name_bytes[..len].iter().enumerate() {
uts.nodename[i] = b as libc::c_char;
}
uts.nodename[len] = 0;
let bytes = unsafe {
std::slice::from_raw_parts(
&uts as *const _ as *const u8,
std::mem::size_of::<libc::utsname>(),
)
};
match write_child_mem(notif_fd, notif.id, notif.pid, buf_addr, bytes) {
Ok(()) => NotifAction::ReturnValue(0),
Err(_) => NotifAction::Continue,
}
}
pub(crate) fn handle_hostname_open(
notif: &SeccompNotif,
hostname: &str,
notif_fd: RawFd,
) -> Option<NotifAction> {
let path_ptr = notif.data.args[1];
let path = read_path(notif, path_ptr, notif_fd)?;
if path != "/etc/hostname" {
return None;
}
let content = format!("{}\n", hostname);
Some(inject_memfd(content.as_bytes()))
}
pub(crate) fn handle_etc_hosts_open(
notif: &SeccompNotif,
etc_hosts_content: &str,
notif_fd: RawFd,
) -> Option<NotifAction> {
let path_ptr = notif.data.args[1];
let path = read_path(notif, path_ptr, notif_fd)?;
if path != "/etc/hosts" {
return None;
}
Some(inject_memfd(etc_hosts_content.as_bytes()))
}
pub(crate) async fn handle_sorted_getdents(
notif: &SeccompNotif,
processes: &Arc<ProcessIndex>,
notif_fd: RawFd,
) -> NotifAction {
let pid = notif.pid;
let child_fd = (notif.data.args[0] & 0xFFFF_FFFF) as u32;
let buf_addr = notif.data.args[1];
let buf_size = (notif.data.args[2] & 0xFFFF_FFFF) as usize;
let link_path = format!("/proc/{}/fd/{}", pid, child_fd);
let dir_path = match std::fs::read_link(&link_path) {
Ok(t) => t,
Err(_) => return NotifAction::Continue,
};
let entry = match processes.entry_for(pid as i32) {
Some(e) => e,
None => return NotifAction::Continue,
};
let cache_key = (child_fd, dir_path.to_string_lossy().into_owned());
let mut perproc = entry.1.lock().await;
if !perproc.procfs_dir_cache.contains_key(&cache_key) {
let dir = match std::fs::read_dir(&dir_path) {
Ok(d) => d,
Err(_) => return NotifAction::Continue,
};
let mut names: Vec<_> = Vec::new();
{
use std::os::unix::fs::MetadataExt;
let dot_ino = std::fs::symlink_metadata(&dir_path).map(|m| m.ino()).unwrap_or(0);
let dotdot_ino = dir_path
.parent()
.and_then(|p| std::fs::symlink_metadata(p).ok())
.map(|m| m.ino())
.unwrap_or(dot_ino);
names.push((".".to_string(), DT_DIR, dot_ino));
names.push(("..".to_string(), DT_DIR, dotdot_ino));
}
names.extend(dir
.filter_map(|e| e.ok())
.map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
let d_type = match e.file_type() {
Ok(ft) if ft.is_dir() => DT_DIR,
Ok(ft) if ft.is_symlink() => DT_LNK,
_ => DT_REG,
};
let d_ino = {
use std::os::linux::fs::MetadataExt;
e.metadata().map(|m| m.st_ino()).unwrap_or(0)
};
(name, d_type, d_ino)
}));
names.sort_by(|a, b| a.0.cmp(&b.0));
let entries: Vec<Vec<u8>> = names
.iter()
.enumerate()
.filter_map(|(i, (name, d_type, d_ino))| {
build_dirent64(*d_ino, (i + 1) as i64, *d_type, name)
})
.collect();
perproc.procfs_dir_cache.insert(cache_key.clone(), entries);
}
let entries = match perproc.procfs_dir_cache.get_mut(&cache_key) {
Some(e) => e,
None => return NotifAction::Continue,
};
if entries.is_empty() {
perproc.procfs_dir_cache.remove(&cache_key);
return NotifAction::ReturnValue(0);
}
let mut result = Vec::new();
let mut consumed = 0;
for entry in entries.iter() {
if result.len() + entry.len() > buf_size {
break;
}
result.extend_from_slice(entry);
consumed += 1;
}
if consumed > 0 {
entries.drain(..consumed);
}
drop(perproc);
if !result.is_empty() {
if write_child_mem(notif_fd, notif.id, pid, buf_addr, &result).is_err() {
return NotifAction::Continue;
}
}
NotifAction::ReturnValue(result.len() as i64)
}
pub(crate) const DT_DIR: u8 = 4;
pub(crate) const DT_REG: u8 = 8;
pub(crate) const DT_LNK: u8 = 10;
pub(crate) fn build_dirent64(d_ino: u64, d_off: i64, d_type: u8, name: &str) -> Option<Vec<u8>> {
const NAME_MAX: usize = 255;
let name_bytes = name.as_bytes();
if name_bytes.len() > NAME_MAX {
return None;
}
let reclen = ((19 + name_bytes.len() + 1) + 7) & !7; let mut buf = vec![0u8; reclen];
buf[0..8].copy_from_slice(&d_ino.to_ne_bytes());
buf[8..16].copy_from_slice(&d_off.to_ne_bytes());
buf[16..18].copy_from_slice(&(reclen as u16).to_ne_bytes());
buf[18] = d_type;
buf[19..19 + name_bytes.len()].copy_from_slice(name_bytes);
Some(buf)
}
fn build_filtered_dirents(sandbox_pids: &HashSet<i32>) -> Vec<Vec<u8>> {
let mut entries = Vec::new();
let mut d_off: i64 = 0;
let dir = match std::fs::read_dir("/proc") {
Ok(d) => d,
Err(_) => return entries,
};
for entry in dir {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let name = entry.file_name();
let name_str = name.to_string_lossy();
if let Ok(pid) = name_str.parse::<i32>() {
if !sandbox_pids.contains(&pid) {
continue;
}
}
d_off += 1;
let d_type = match entry.file_type() {
Ok(ft) if ft.is_dir() => DT_DIR,
Ok(ft) if ft.is_symlink() => DT_LNK,
_ => DT_REG,
};
let d_ino = {
use std::os::linux::fs::MetadataExt;
entry.metadata().map(|m| m.st_ino()).unwrap_or(0)
};
if let Some(rec) = build_dirent64(d_ino, d_off, d_type, &name_str) {
entries.push(rec);
}
}
entries
}
pub(crate) async fn handle_getdents(
notif: &SeccompNotif,
processes: &Arc<ProcessIndex>,
_policy: &NotifPolicy,
notif_fd: RawFd,
) -> NotifAction {
let pid = notif.pid; let child_fd = (notif.data.args[0] & 0xFFFF_FFFF) as u32;
let buf_addr = notif.data.args[1];
let buf_size = (notif.data.args[2] & 0xFFFF_FFFF) as usize;
let link_path = format!("/proc/{}/fd/{}", pid, child_fd);
let target = match std::fs::read_link(&link_path) {
Ok(t) => t,
Err(_) => return NotifAction::Continue,
};
if target.to_str() != Some("/proc") {
return NotifAction::Continue;
}
let entry = match processes.entry_for(pid as i32) {
Some(e) => e,
None => return NotifAction::Continue,
};
let cache_key = (child_fd, target.to_string_lossy().into_owned());
let mut perproc = entry.1.lock().await;
if !perproc.procfs_dir_cache.contains_key(&cache_key) {
let snapshot = processes.pids_snapshot();
let entries = build_filtered_dirents(&snapshot);
perproc.procfs_dir_cache.insert(cache_key.clone(), entries);
}
let entries = match perproc.procfs_dir_cache.get_mut(&cache_key) {
Some(e) => e,
None => return NotifAction::Continue,
};
let mut result = Vec::new();
let mut consumed = 0;
for entry in entries.iter() {
if result.len() + entry.len() > buf_size {
break;
}
result.extend_from_slice(entry);
consumed += 1;
}
if entries.is_empty() {
perproc.procfs_dir_cache.remove(&cache_key);
return NotifAction::ReturnValue(0);
}
if consumed > 0 {
entries.drain(..consumed);
}
drop(perproc);
if !result.is_empty() {
if write_child_mem(notif_fd, notif.id, pid, buf_addr, &result).is_err() {
return NotifAction::Continue;
}
}
NotifAction::ReturnValue(result.len() as i64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_sensitive_proc() {
assert!(is_sensitive_proc("/proc/kcore"));
assert!(is_sensitive_proc("/proc/kmsg"));
assert!(is_sensitive_proc("/proc/kallsyms"));
assert!(is_sensitive_proc("/proc/keys"));
assert!(is_sensitive_proc("/proc/key-users"));
assert!(is_sensitive_proc("/proc/sysrq-trigger"));
assert!(is_sensitive_proc("/sys/firmware"));
assert!(is_sensitive_proc("/sys/firmware/efi"));
assert!(is_sensitive_proc("/sys/kernel/security"));
assert!(is_sensitive_proc("/sys/kernel/security/apparmor"));
assert!(!is_sensitive_proc("/proc/cpuinfo"));
assert!(!is_sensitive_proc("/proc/meminfo"));
assert!(!is_sensitive_proc("/proc/1/status"));
assert!(is_sensitive_proc("/sys/class/net"));
assert!(is_sensitive_proc("/sys/class/net/eth0"));
}
#[test]
fn test_extract_proc_pid() {
assert_eq!(extract_proc_pid("/proc/123/cmdline"), Some(123));
assert_eq!(extract_proc_pid("/proc/1/status"), Some(1));
assert_eq!(extract_proc_pid("/proc/99999/fd"), Some(99999));
assert_eq!(extract_proc_pid("/proc/self/status"), None);
assert_eq!(extract_proc_pid("/proc/cpuinfo"), None);
assert_eq!(extract_proc_pid("/proc/meminfo"), None);
assert_eq!(extract_proc_pid("/proc/net/tcp"), None);
assert_eq!(extract_proc_pid("/etc/passwd"), None);
assert_eq!(extract_proc_pid("/proc/"), None);
}
#[test]
fn test_generate_cpuinfo_single() {
let info = generate_cpuinfo(1);
let text = String::from_utf8(info).unwrap();
assert!(text.contains("processor\t: 0"));
assert!(text.contains("model name\t: Virtual CPU"));
assert!(text.contains("cpu MHz\t\t: 2400.000"));
assert!(!text.contains("processor\t: 1"));
}
#[test]
fn test_generate_cpuinfo_multiple() {
let info = generate_cpuinfo(4);
let text = String::from_utf8(info).unwrap();
assert!(text.contains("processor\t: 0"));
assert!(text.contains("processor\t: 1"));
assert!(text.contains("processor\t: 2"));
assert!(text.contains("processor\t: 3"));
assert!(!text.contains("processor\t: 4"));
}
#[test]
fn test_generate_meminfo() {
let total = 1024 * 1024 * 1024u64;
let used = 256 * 1024 * 1024u64;
let info = generate_meminfo(total, used);
let text = String::from_utf8(info).unwrap();
let total_kb = total / 1024;
let used_kb = used / 1024;
let free_kb = total_kb - used_kb;
assert!(text.contains(&format!("MemTotal: {} kB", total_kb)));
assert!(text.contains(&format!("MemFree: {} kB", free_kb)));
assert!(text.contains(&format!("MemAvailable: {} kB", free_kb)));
}
#[test]
fn test_generate_meminfo_zero_used() {
let total = 512 * 1024 * 1024u64;
let info = generate_meminfo(total, 0);
let text = String::from_utf8(info).unwrap();
let total_kb = total / 1024;
assert!(text.contains(&format!("MemTotal: {} kB", total_kb)));
assert!(text.contains(&format!("MemFree: {} kB", total_kb)));
}
#[test]
fn test_generate_meminfo_over_used() {
let total = 100 * 1024u64;
let used = 200 * 1024u64;
let info = generate_meminfo(total, used);
let text = String::from_utf8(info).unwrap();
assert!(text.contains("MemFree: 0 kB"));
}
#[test]
fn test_generate_uptime() {
let info = generate_uptime(123.456);
let text = String::from_utf8(info).unwrap();
assert!(text.starts_with("123.46"));
assert!(text.contains("0.00"));
}
#[test]
fn test_generate_uptime_zero() {
let info = generate_uptime(0.0);
let text = String::from_utf8(info).unwrap();
assert!(text.starts_with("0.00"));
}
#[test]
fn test_generate_uptime_negative_clamped() {
let info = generate_uptime(-5.0);
let text = String::from_utf8(info).unwrap();
assert!(text.starts_with("0.00"));
}
#[test]
fn test_loadavg_ewma() {
let mut la = LoadAvg::new();
assert_eq!(la.avg_1, 0.0);
assert_eq!(la.avg_5, 0.0);
assert_eq!(la.avg_15, 0.0);
for _ in 0..12 {
la.sample(4);
}
assert!(la.avg_1 > la.avg_5);
assert!(la.avg_5 > la.avg_15);
assert!(la.avg_1 > 2.0); }
#[test]
fn test_loadavg_ewma_decay() {
let mut la = LoadAvg::new();
for _ in 0..60 {
la.sample(10);
}
let peak = la.avg_1;
for _ in 0..60 {
la.sample(0);
}
assert!(la.avg_1 < peak * 0.1, "1-min avg should decay quickly");
}
#[test]
fn test_generate_loadavg() {
let la = LoadAvg { avg_1: 1.23, avg_5: 0.45, avg_15: 0.12 };
let info = generate_loadavg(&la, 3, 10, 42);
let text = String::from_utf8(info).unwrap();
assert!(text.contains("1.23"));
assert!(text.contains("0.45"));
assert!(text.contains("0.12"));
assert!(text.contains("3/10"));
assert!(text.contains("42"));
}
#[test]
fn test_generate_loadavg_zero_procs() {
let la = LoadAvg::new();
let info = generate_loadavg(&la, 0, 0, 0);
let text = String::from_utf8(info).unwrap();
assert!(text.contains("0/0"));
}
#[test]
fn test_detect_fstype_root() {
let fstype = detect_fstype(std::path::Path::new("/"));
assert_ne!(fstype, "unknown", "root fs should have a known type");
}
#[test]
fn test_detect_fstype_nonexistent() {
let fstype = detect_fstype(std::path::Path::new("/no/such/path"));
assert_eq!(fstype, "unknown");
}
#[test]
fn test_generate_proc_mounts_chroot() {
let tmp = std::env::temp_dir();
let mounts = vec![
(std::path::PathBuf::from("/work"), tmp.clone()),
(std::path::PathBuf::from("/data"), tmp.clone()),
];
let content = generate_proc_mounts(Some(tmp.as_path()), &mounts);
let text = String::from_utf8(content).unwrap();
assert!(text.starts_with("sandlock / "), "Should start with root entry, got: {}", text);
assert!(text.contains("sandlock /work "));
assert!(text.contains("sandlock /data "));
assert!(!text.contains(tmp.to_str().unwrap()));
let root_line = text.lines().next().unwrap();
assert!(!root_line.contains("unknown"), "root fstype should be detected, got: {}", root_line);
}
#[test]
fn test_generate_proc_mounts_no_chroot() {
let mounts: Vec<(std::path::PathBuf, std::path::PathBuf)> = vec![];
let content = generate_proc_mounts(None, &mounts);
let text = String::from_utf8(content).unwrap();
assert!(text.contains("rootfs / rootfs rw 0 0"));
assert_eq!(text.lines().count(), 1);
}
#[test]
fn test_generate_proc_mountinfo_chroot() {
let tmp = std::env::temp_dir();
let mounts = vec![
(std::path::PathBuf::from("/work"), tmp.clone()),
];
let content = generate_proc_mountinfo(Some(tmp.as_path()), &mounts);
let text = String::from_utf8(content).unwrap();
assert!(text.contains("/ / rw,relatime -"));
assert!(text.contains("/ /work rw,relatime -"));
assert!(!text.contains(tmp.to_str().unwrap()));
assert_eq!(text.lines().count(), 2);
}
#[test]
fn test_generate_proc_mountinfo_no_chroot() {
let mounts: Vec<(std::path::PathBuf, std::path::PathBuf)> = vec![];
let content = generate_proc_mountinfo(None, &mounts);
let text = String::from_utf8(content).unwrap();
assert!(text.contains("/ / rw - rootfs rootfs rw"));
assert_eq!(text.lines().count(), 1);
}
#[test]
fn test_build_dirent64() {
let entry = build_dirent64(12345, 1, DT_DIR, "1234").unwrap();
assert_eq!(entry.len(), 24); let d_ino = u64::from_ne_bytes(entry[0..8].try_into().unwrap());
assert_eq!(d_ino, 12345);
let d_reclen = u16::from_ne_bytes(entry[16..18].try_into().unwrap());
assert_eq!(d_reclen, 24);
assert_eq!(entry[18], DT_DIR);
assert_eq!(&entry[19..23], b"1234");
assert_eq!(entry[23], 0);
}
#[test]
fn test_build_dirent64_alignment() {
let entry = build_dirent64(1, 1, DT_REG, "ab").unwrap();
assert_eq!(entry.len(), 24);
}
#[test]
fn test_build_dirent64_rejects_oversize_name() {
let name = "x".repeat(256);
assert!(build_dirent64(1, 1, DT_REG, &name).is_none());
}
#[test]
fn test_build_filtered_dirents() {
use std::collections::HashSet;
let mut sandbox_pids = HashSet::new();
sandbox_pids.insert(1_i32);
let entries = build_filtered_dirents(&sandbox_pids);
assert!(!entries.is_empty());
}
}