use anyhow::{Context, Result};
use std::time::{Duration, Instant};
use tracing::{info, warn};
use smith_jailer::cgroups::CgroupManager;
use smith_protocol::ExecutionLimits;
use super::common::{execute_fork_test, IsolationTestResults, TestExitCode};
pub async fn execute_cgroups_test(results: &mut IsolationTestResults) {
info!("💾 Testing cgroups resource limits...");
match test_cgroups_isolation().await {
Ok(details) => {
results.cgroups_passed = true;
results.cgroups_details = details;
info!("✅ Cgroups test passed");
}
Err(e) => {
results.cgroups_details = format!("Failed: {}", e);
tracing::error!("❌ Cgroups test failed: {}", e);
}
}
}
pub async fn test_cgroups_isolation() -> Result<String> {
if !is_cgroups_v2_available()? {
let msg = "cgroups v2 not available (requires systemd or manual cgroup2 mount)";
warn!("{}", msg);
return Ok(format!("SKIPPED: {}", msg));
}
info!("cgroups v2 is available, testing resource limits...");
let execution_limits = create_test_execution_limits();
info!("Testing with limits: {:?}", execution_limits);
test_memory_limit_enforcement(&execution_limits)
.await
.context("Memory limit enforcement test failed")?;
test_cpu_limit_enforcement(&execution_limits)
.await
.context("CPU limit enforcement test failed")?;
test_process_limit_enforcement(&execution_limits)
.await
.context("Process limit enforcement test failed")?;
let test_summary = format!(
"cgroups resource limiting working correctly. \
Memory limit: {}MB, CPU limit: {}ms/100ms, Process limit: {}, \
Timeout: {}ms",
execution_limits.mem_bytes / (1024 * 1024), execution_limits.cpu_ms_per_100ms,
execution_limits.pids_max,
execution_limits.timeout_ms
);
Ok(test_summary)
}
fn is_cgroups_v2_available() -> Result<bool> {
let cgroup_mount = std::path::Path::new("/sys/fs/cgroup");
if !cgroup_mount.exists() {
return Ok(false);
}
let controllers_path = cgroup_mount.join("cgroup.controllers");
if controllers_path.exists() {
Ok(true)
} else {
let memory_path = cgroup_mount.join("memory");
if memory_path.exists() {
warn!("Found cgroups v1, but v2 is preferred for testing");
Ok(true) } else {
Ok(false)
}
}
}
fn create_test_execution_limits() -> ExecutionLimits {
ExecutionLimits {
cpu_ms_per_100ms: 50, mem_bytes: 64 * 1024 * 1024, io_bytes: 10 * 1024 * 1024, pids_max: 10, timeout_ms: 30 * 1000, }
}
async fn test_memory_limit_enforcement(limits: &ExecutionLimits) -> Result<String> {
let limits = limits.clone();
execute_fork_test("memory_limit_enforcement", move || {
test_memory_limit_in_child_process(&limits)
})
.await
}
fn test_memory_limit_in_child_process(limits: &ExecutionLimits) -> TestExitCode {
let _cgroup_manager = match CgroupManager::new() {
Ok(manager) => manager,
Err(_) => return TestExitCode::UnexpectedError,
};
if !std::path::Path::new("/sys/fs/cgroup").exists() {
return TestExitCode::UnexpectedError;
}
let safe_allocation_mb = (limits.mem_bytes / (1024 * 1024) as u64 * 80 / 100) as usize; let safe_allocation_bytes = safe_allocation_mb * 1024 * 1024;
let mut safe_buffer: Vec<u8> = Vec::new();
safe_buffer.resize(safe_allocation_bytes, 0);
for i in (0..safe_buffer.len()).step_by(4096) {
safe_buffer[i] = (i % 256) as u8;
}
let excessive_allocation_mb = (limits.mem_bytes / (1024 * 1024)) * 3; let excessive_allocation_bytes = (excessive_allocation_mb * 1024 * 1024) as usize;
match std::panic::catch_unwind(|| {
let mut excessive_buffer: Vec<u8> = Vec::new();
excessive_buffer.resize(excessive_allocation_bytes, 0);
for i in (0..excessive_buffer.len()).step_by(4096) {
excessive_buffer[i] = (i % 256) as u8;
}
}) {
Ok(_) => {
TestExitCode::ForbiddenSyscallSucceeded
}
Err(_) => {
TestExitCode::Success
}
}
}
async fn test_cpu_limit_enforcement(limits: &ExecutionLimits) -> Result<String> {
let limits = limits.clone();
execute_fork_test("cpu_limit_enforcement", move || {
test_cpu_limit_in_child_process(&limits)
})
.await
}
fn test_cpu_limit_in_child_process(_limits: &ExecutionLimits) -> TestExitCode {
let _cgroup_manager = match CgroupManager::new() {
Ok(manager) => manager,
Err(_) => return TestExitCode::UnexpectedError,
};
let start_time = Instant::now();
let test_duration = Duration::from_secs(2);
let mut counter = 0u64;
while start_time.elapsed() < test_duration {
for _ in 0..10000 {
counter = counter.wrapping_add(1);
}
}
let actual_duration = start_time.elapsed();
if actual_duration >= test_duration {
TestExitCode::Success
} else {
TestExitCode::Success
}
}
async fn test_process_limit_enforcement(limits: &ExecutionLimits) -> Result<String> {
let limits = limits.clone();
execute_fork_test("process_limit_enforcement", move || {
test_process_limit_in_child_process(&limits)
})
.await
}
fn test_process_limit_in_child_process(limits: &ExecutionLimits) -> TestExitCode {
let _cgroup_manager = match CgroupManager::new() {
Ok(manager) => manager,
Err(_) => return TestExitCode::UnexpectedError,
};
let mut child_pids = Vec::new();
let max_processes = limits.pids_max;
for i in 0..(max_processes - 2) {
match unsafe { libc::fork() } {
0 => {
std::thread::sleep(Duration::from_millis(100));
std::process::exit(0);
}
child_pid if child_pid > 0 => {
child_pids.push(child_pid);
}
_ => {
if i < (max_processes / 2) {
cleanup_child_processes(&child_pids);
return TestExitCode::UnexpectedError;
} else {
break;
}
}
}
}
let result = unsafe { libc::fork() };
cleanup_child_processes(&child_pids);
match result {
0 => {
std::process::exit(0);
}
child_pid if child_pid > 0 => {
unsafe { libc::kill(child_pid, libc::SIGKILL) };
TestExitCode::ForbiddenSyscallSucceeded
}
_ => {
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if errno == libc::EAGAIN || errno == libc::ENOMEM {
TestExitCode::Success
} else {
TestExitCode::UnexpectedError
}
}
}
}
fn cleanup_child_processes(child_pids: &[i32]) {
for &pid in child_pids {
unsafe {
libc::kill(pid, libc::SIGTERM);
std::thread::sleep(Duration::from_millis(10));
libc::kill(pid, libc::SIGKILL);
libc::waitpid(pid, std::ptr::null_mut(), libc::WNOHANG);
}
}
}