use super::common;
use super::error::JailerError;
use crate::runtime::advanced_options::ResourceLimits;
use std::fs;
use std::path::{Path, PathBuf};
const CGROUP_ROOT: &str = "/sys/fs/cgroup";
const BOXLITE_CGROUP: &str = "boxlite";
#[cfg(target_os = "linux")]
fn is_root() -> bool {
unsafe { libc::getuid() == 0 }
}
#[cfg(not(target_os = "linux"))]
fn is_root() -> bool {
false
}
#[cfg(target_os = "linux")]
fn get_user_cgroup_base() -> Option<PathBuf> {
let uid = unsafe { libc::getuid() };
let path = PathBuf::from(format!(
"/sys/fs/cgroup/user.slice/user-{}.slice/user@{}.service",
uid, uid
));
if path.exists() {
Some(path)
} else {
None
}
}
#[cfg(not(target_os = "linux"))]
fn get_user_cgroup_base() -> Option<PathBuf> {
None
}
fn get_cgroup_base() -> PathBuf {
if is_root() {
PathBuf::from(CGROUP_ROOT)
} else {
get_user_cgroup_base().unwrap_or_else(|| PathBuf::from(CGROUP_ROOT))
}
}
#[derive(Debug, Clone, Default)]
pub struct CgroupConfig {
pub memory_max: Option<u64>,
pub memory_high: Option<u64>,
pub cpu_weight: Option<u32>,
pub cpu_max: Option<(u64, u64)>,
pub pids_max: Option<u64>,
}
pub fn is_cgroup_v2_available() -> bool {
let cgroup_root = Path::new(CGROUP_ROOT);
if !cgroup_root.exists() {
return false;
}
let controllers = cgroup_root.join("cgroup.controllers");
controllers.exists()
}
pub fn cgroup_path(box_id: &str) -> PathBuf {
get_cgroup_base().join(BOXLITE_CGROUP).join(box_id)
}
pub fn setup_cgroup(box_id: &str, config: &CgroupConfig) -> Result<PathBuf, JailerError> {
if !is_cgroup_v2_available() {
tracing::warn!("Cgroup v2 not available, skipping cgroup setup");
return Err(JailerError::Cgroup("Cgroup v2 not available".to_string()));
}
let cgroup_base = get_cgroup_base();
let boxlite_cgroup = cgroup_base.join(BOXLITE_CGROUP);
let box_cgroup = boxlite_cgroup.join(box_id);
tracing::debug!(
cgroup_base = %cgroup_base.display(),
is_root = is_root(),
"Using cgroup base path"
);
if !boxlite_cgroup.exists() {
fs::create_dir(&boxlite_cgroup).map_err(|e| {
JailerError::Cgroup(format!(
"Failed to create boxlite cgroup at {}: {}",
boxlite_cgroup.display(),
e
))
})?;
enable_controllers(&boxlite_cgroup)?;
}
if !box_cgroup.exists() {
fs::create_dir(&box_cgroup).map_err(|e| {
JailerError::Cgroup(format!(
"Failed to create box cgroup at {}: {}",
box_cgroup.display(),
e
))
})?;
}
apply_limits(&box_cgroup, config)?;
tracing::debug!(
box_id = %box_id,
path = %box_cgroup.display(),
"Cgroup created"
);
Ok(box_cgroup)
}
fn enable_controllers(cgroup_path: &Path) -> Result<(), JailerError> {
let subtree_control = cgroup_path.join("cgroup.subtree_control");
write_file(&subtree_control, "+cpu +memory +pids")?;
Ok(())
}
fn apply_limits(cgroup_path: &Path, config: &CgroupConfig) -> Result<(), JailerError> {
if let Some(memory_max) = config.memory_max {
write_file(&cgroup_path.join("memory.max"), &memory_max.to_string())?;
}
if let Some(memory_high) = config.memory_high {
write_file(&cgroup_path.join("memory.high"), &memory_high.to_string())?;
}
if let Some(cpu_weight) = config.cpu_weight {
write_file(&cgroup_path.join("cpu.weight"), &cpu_weight.to_string())?;
}
if let Some((quota, period)) = config.cpu_max {
write_file(
&cgroup_path.join("cpu.max"),
&format!("{} {}", quota, period),
)?;
}
if let Some(pids_max) = config.pids_max {
write_file(&cgroup_path.join("pids.max"), &pids_max.to_string())?;
}
Ok(())
}
#[allow(dead_code)]
pub fn add_process(box_id: &str, pid: u32) -> Result<(), JailerError> {
let cgroup_path = cgroup_path(box_id);
let procs_file = cgroup_path.join("cgroup.procs");
write_file(&procs_file, &pid.to_string())?;
tracing::debug!(
box_id = %box_id,
pid = pid,
"Process added to cgroup"
);
Ok(())
}
#[allow(dead_code)]
pub fn remove_cgroup(box_id: &str) -> Result<(), JailerError> {
let cgroup_path = cgroup_path(box_id);
if cgroup_path.exists() {
fs::remove_dir(&cgroup_path).map_err(|e| {
JailerError::Cgroup(format!(
"Failed to remove cgroup at {}: {}",
cgroup_path.display(),
e
))
})?;
tracing::debug!(
box_id = %box_id,
"Cgroup removed"
);
}
Ok(())
}
fn write_file(path: &Path, content: &str) -> Result<(), JailerError> {
fs::write(path, content)
.map_err(|e| JailerError::Cgroup(format!("Failed to write to {}: {}", path.display(), e)))
}
impl From<&ResourceLimits> for CgroupConfig {
fn from(limits: &ResourceLimits) -> Self {
Self {
memory_max: limits.max_memory,
memory_high: limits.max_memory.map(|m| m * 9 / 10), cpu_weight: None, cpu_max: limits.max_cpu_time.map(|t| {
(t * 1_000_000, 1_000_000)
}),
pids_max: limits.max_processes,
}
}
}
#[cfg(target_os = "linux")]
pub fn add_self_to_cgroup_raw(cgroup_procs_path: &std::ffi::CStr) -> Result<(), i32> {
let pid = unsafe { libc::getpid() };
let mut pid_buf = [0u8; 16];
let pid_len = {
let mut n = pid as u32;
let mut len = 0;
let mut temp = [0u8; 16];
if n == 0 {
temp[0] = b'0';
len = 1;
} else {
while n > 0 {
temp[len] = b'0' + (n % 10) as u8;
n /= 10;
len += 1;
}
}
for i in 0..len {
pid_buf[i] = temp[len - 1 - i];
}
pid_buf[len] = b'\n';
len + 1
};
let fd = unsafe { libc::open(cgroup_procs_path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Err(common::get_errno());
}
let result = unsafe { libc::write(fd, pid_buf.as_ptr() as *const libc::c_void, pid_len) };
unsafe { libc::close(fd) };
if result < 0 {
return Err(common::get_errno());
}
Ok(())
}
#[cfg(target_os = "linux")]
pub fn build_cgroup_procs_path(box_id: &str) -> Option<std::ffi::CString> {
if !is_cgroup_v2_available() {
return None;
}
let path = cgroup_path(box_id).join("cgroup.procs");
std::ffi::CString::new(path.to_string_lossy().as_bytes()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cgroup_path() {
let path = cgroup_path("test-box-123");
let expected_base = get_cgroup_base();
let expected = expected_base.join("boxlite").join("test-box-123");
assert_eq!(path, expected);
assert!(path.ends_with("boxlite/test-box-123"));
}
#[test]
fn test_cgroup_v2_detection() {
let available = is_cgroup_v2_available();
println!("Cgroup v2 available: {}", available);
}
#[test]
fn test_cgroup_config_from_limits() {
let limits = ResourceLimits {
max_memory: Some(1024 * 1024 * 1024), max_processes: Some(100),
max_cpu_time: Some(60), ..Default::default()
};
let config = CgroupConfig::from(&limits);
assert_eq!(config.memory_max, Some(1024 * 1024 * 1024));
assert_eq!(config.pids_max, Some(100));
assert!(config.cpu_max.is_some());
}
}