#![allow(unsafe_code)]
use anyhow::Result;
use std::process::Command;
#[cfg(unix)]
use std::process::Stdio;
#[cfg(unix)]
use tracing::debug;
#[cfg(not(unix))]
use tracing::warn;
#[derive(Debug, Clone)]
pub struct IsolationConfig {
pub drop_to_uid: Option<u32>,
pub drop_to_gid: Option<u32>,
pub max_memory_bytes: Option<usize>,
pub max_cpu_seconds: Option<u64>,
pub max_processes: Option<usize>,
}
impl Default for IsolationConfig {
fn default() -> Self {
Self {
drop_to_uid: None,
drop_to_gid: None,
max_memory_bytes: Some(128 * 1024 * 1024), max_cpu_seconds: Some(5), max_processes: Some(1), }
}
}
pub fn apply_isolation(
#[cfg_attr(not(unix), allow(unused_mut))] mut cmd: Command,
config: &IsolationConfig,
) -> Result<Command> {
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let mut ulimit_args = Vec::new();
if let Some(max_mem) = config.max_memory_bytes {
let max_mem_kb = max_mem / 1024;
ulimit_args.push(format!("-v {}", max_mem_kb));
}
if let Some(max_cpu) = config.max_cpu_seconds {
ulimit_args.push(format!("-t {}", max_cpu));
}
if let Some(max_proc) = config.max_processes {
ulimit_args.push(format!("-u {}", max_proc));
}
ulimit_args.push("-f 0".to_string());
ulimit_args.push("-c 0".to_string());
debug!("Applying ulimit restrictions: {:?}", ulimit_args);
if !ulimit_args.is_empty() {
let program = cmd.get_program().to_string_lossy().to_string();
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_string_lossy().to_string())
.collect();
let mut wrapped = Command::new("sh");
wrapped.arg("-c");
let ulimit_cmd = ulimit_args.join("; ulimit ");
let exec_cmd = format!(
"{} {}",
program,
args.iter()
.map(|a| shell_escape(a))
.collect::<Vec<_>>()
.join(" ")
);
let full_cmd = format!("ulimit {}; {}", ulimit_cmd, exec_cmd);
wrapped.arg(full_cmd);
wrapped.stdin(Stdio::null());
wrapped.stdout(Stdio::piped());
wrapped.stderr(Stdio::piped());
cmd = wrapped;
}
if let Some(uid) = config.drop_to_uid {
debug!("Dropping privileges to UID: {}", uid);
let _gid = config.drop_to_gid;
unsafe {
cmd.pre_exec(move || {
#[cfg(target_os = "linux")]
{
use libc::{setgid, setuid};
if let Some(gid_val) = _gid {
if setgid(gid_val) != 0 {
return Err(std::io::Error::last_os_error());
}
}
if setuid(uid) != 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
}
}
}
#[cfg(not(unix))]
{
warn!("Process isolation not fully supported on this platform");
let _ = config; }
Ok(cmd)
}
#[cfg(unix)]
fn shell_escape(arg: &str) -> String {
format!("'{}'", arg.replace('\'', "'\\''"))
}
pub fn is_running_as_root() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(not(unix))]
{
false
}
}
pub fn current_uid() -> Option<u32> {
#[cfg(unix)]
{
Some(unsafe { libc::getuid() })
}
#[cfg(not(unix))]
{
None
}
}
pub fn current_gid() -> Option<u32> {
#[cfg(unix)]
{
Some(unsafe { libc::getgid() })
}
#[cfg(not(unix))]
{
None
}
}
pub fn recommend_safe_uid() -> Option<(u32, u32)> {
if is_running_as_root() {
Some((65534, 65534))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_isolation_config_default() {
let config = IsolationConfig::default();
assert!(config.max_memory_bytes.is_some());
assert!(config.max_cpu_seconds.is_some());
assert_eq!(config.max_processes, Some(1));
}
#[test]
#[cfg(unix)]
fn test_shell_escape() {
assert_eq!(shell_escape("simple"), "'simple'");
assert_eq!(shell_escape("with spaces"), "'with spaces'");
assert_eq!(shell_escape("with'quote"), "'with'\\''quote'");
assert_eq!(
shell_escape("complex'test'string"),
"'complex'\\''test'\\''string'"
);
}
#[test]
fn test_current_uid_gid() {
#[cfg(unix)]
{
assert!(current_uid().is_some());
assert!(current_gid().is_some());
}
#[cfg(not(unix))]
{
assert!(current_uid().is_none());
assert!(current_gid().is_none());
}
}
#[test]
fn test_recommend_safe_uid() {
let recommendation = recommend_safe_uid();
if is_running_as_root() {
assert!(recommendation.is_some());
assert_eq!(recommendation.unwrap(), (65534, 65534));
} else {
assert!(recommendation.is_none());
}
}
#[test]
fn test_apply_isolation_basic() {
let config = IsolationConfig::default();
let cmd = Command::new("echo");
let result = apply_isolation(cmd, &config);
assert!(result.is_ok());
}
}