use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::process::Command;
use tokio::time::{timeout, Duration};
use super::{ExecutionResult, SandboxRunner};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NativeConfig {
pub executable: String,
pub working_directory: PathBuf,
pub enforce_resource_limits: bool,
pub max_memory_mb: Option<u64>,
pub max_cpu_seconds: Option<u64>,
pub max_execution_time: Duration,
pub allowed_executables: Vec<String>,
}
impl Default for NativeConfig {
fn default() -> Self {
Self {
executable: "bash".to_string(),
working_directory: PathBuf::from("/tmp/symbiont-native"),
enforce_resource_limits: true,
max_memory_mb: Some(2048),
max_cpu_seconds: Some(300),
max_execution_time: Duration::from_secs(300),
allowed_executables: vec![
"bash".to_string(),
"sh".to_string(),
"python3".to_string(),
"python".to_string(),
"node".to_string(),
],
}
}
}
impl NativeConfig {
pub fn validate(&self) -> Result<(), anyhow::Error> {
if !self.allowed_executables.is_empty() {
let exec_name = self
.executable
.split('/')
.next_back()
.unwrap_or(&self.executable);
if !self
.allowed_executables
.iter()
.any(|allowed| allowed == &self.executable || allowed == exec_name)
{
anyhow::bail!(
"Executable '{}' not in allowed list: {:?}",
self.executable,
self.allowed_executables
);
}
}
if !self.working_directory.is_absolute() {
anyhow::bail!(
"Working directory must be absolute path: {}",
self.working_directory.display()
);
}
Ok(())
}
}
pub struct NativeRunner {
config: NativeConfig,
}
impl NativeRunner {
pub fn new(config: NativeConfig) -> Result<Self, anyhow::Error> {
if let Ok(env) = std::env::var("SYMBIONT_ENV") {
if env.to_lowercase() == "production" {
let allow_native = std::env::var("SYMBIONT_ALLOW_NATIVE_EXECUTION")
.unwrap_or_default()
.to_lowercase();
if allow_native != "true" && allow_native != "yes" && allow_native != "1" {
anyhow::bail!(
"SECURITY: Native execution is disabled in production environments. \
Set SYMBIONT_ALLOW_NATIVE_EXECUTION=true to override (not recommended)."
);
}
tracing::error!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
tracing::error!("⚠️ CRITICAL SECURITY WARNING");
tracing::error!("⚠️ Native execution enabled in PRODUCTION environment!");
tracing::error!("⚠️ This provides ZERO isolation and is NOT recommended.");
tracing::error!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
eprintln!("\n⚠️ CRITICAL: Native execution in production!");
eprintln!("⚠️ NO sandboxing - full host access granted to code.\n");
}
}
tracing::warn!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
tracing::warn!("⚠️ Native Sandbox: NO ISOLATION");
tracing::warn!("⚠️ Executable: {}", config.executable);
tracing::warn!("⚠️ Working dir: {}", config.working_directory.display());
tracing::warn!("⚠️ Code will run directly on host system");
tracing::warn!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
config.validate()?;
if !config.working_directory.exists() {
tracing::info!(
"Creating working directory: {}",
config.working_directory.display()
);
std::fs::create_dir_all(&config.working_directory)?;
}
Ok(Self { config })
}
pub fn with_defaults() -> Result<Self, anyhow::Error> {
Self::new(NativeConfig::default())
}
#[cfg(unix)]
fn apply_resource_limits(&self, command: &mut Command) -> Result<(), anyhow::Error> {
if !self.config.enforce_resource_limits {
return Ok(());
}
let mut limit_args = Vec::new();
if let Some(max_mem_mb) = self.config.max_memory_mb {
limit_args.push(format!("-v {}", max_mem_mb * 1024));
}
if let Some(max_cpu_sec) = self.config.max_cpu_seconds {
limit_args.push(format!("-t {}", max_cpu_sec));
}
if !limit_args.is_empty() {
let original_program = command.as_std().get_program().to_string_lossy().to_string();
let original_args: Vec<String> = command
.as_std()
.get_args()
.map(|s| s.to_string_lossy().to_string())
.collect();
let wrapper_cmd = format!(
"ulimit {} && {} {}",
limit_args.join(" "),
original_program,
original_args.join(" ")
);
*command = Command::new("sh");
command.arg("-c");
command.arg(wrapper_cmd);
}
Ok(())
}
#[cfg(not(unix))]
fn apply_resource_limits(&self, _command: &mut Command) -> Result<(), anyhow::Error> {
if self.config.enforce_resource_limits {
tracing::warn!(
"Resource limits are not supported on this platform, ignoring enforce_resource_limits setting"
);
}
Ok(())
}
}
#[async_trait]
impl SandboxRunner for NativeRunner {
async fn execute(
&self,
code: &str,
env: HashMap<String, String>,
) -> Result<ExecutionResult, anyhow::Error> {
tracing::warn!("⚠️ EXECUTING CODE WITHOUT ISOLATION - Native execution mode is active");
tracing::debug!(
"Native execution: executable={}, working_dir={}",
self.config.executable,
self.config.working_directory.display()
);
let mut command = Command::new(&self.config.executable);
command.current_dir(&self.config.working_directory);
command.envs(env);
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
match self
.config
.executable
.split('/')
.next_back()
.unwrap_or(&self.config.executable)
{
"python" | "python3" => {
command.arg("-c");
command.arg(code);
}
"node" => {
command.arg("-e");
command.arg(code);
}
"bash" | "sh" => {
command.arg("-c");
command.arg(code);
}
_ => {
command.arg("-c");
command.arg(code);
}
}
self.apply_resource_limits(&mut command)?;
let start = std::time::Instant::now();
let output_result = timeout(self.config.max_execution_time, command.output()).await;
let execution_time = start.elapsed();
match output_result {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
let success = output.status.success();
tracing::debug!(
"Native execution completed: exit_code={}, success={}, duration={:?}",
exit_code,
success,
execution_time
);
Ok(ExecutionResult {
stdout,
stderr,
exit_code,
success,
execution_time_ms: execution_time.as_millis() as u64,
})
}
Ok(Err(e)) => {
tracing::error!("Native execution failed: {}", e);
Err(anyhow::anyhow!("Process execution failed: {}", e))
}
Err(_) => {
tracing::error!(
"Native execution timed out after {:?}",
self.config.max_execution_time
);
Err(anyhow::anyhow!(
"Execution timed out after {:?}",
self.config.max_execution_time
))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_native_runner_creation() {
let config = NativeConfig::default();
let runner = NativeRunner::new(config);
assert!(runner.is_ok());
}
#[tokio::test]
async fn test_native_runner_with_defaults() {
let runner = NativeRunner::with_defaults();
assert!(runner.is_ok());
}
#[tokio::test]
async fn test_native_python_execution() {
let config = NativeConfig {
executable: "python3".to_string(),
..Default::default()
};
let runner = match NativeRunner::new(config) {
Ok(r) => r,
Err(_) => {
return;
}
};
let result = runner
.execute("print('Hello from native!')", HashMap::new())
.await;
if let Ok(output) = result {
assert!(output.success);
assert!(output.stdout.contains("Hello from native!"));
}
}
#[tokio::test]
async fn test_native_bash_execution() {
let config = NativeConfig {
executable: "bash".to_string(),
..Default::default()
};
let runner = NativeRunner::new(config).unwrap();
let result = runner
.execute("echo 'Testing native execution'", HashMap::new())
.await
.unwrap();
assert!(result.success);
assert!(result.stdout.contains("Testing native execution"));
}
#[tokio::test]
async fn test_native_execution_with_env_vars() {
let config = NativeConfig {
executable: "bash".to_string(),
..Default::default()
};
let runner = NativeRunner::new(config).unwrap();
let mut env = HashMap::new();
env.insert("TEST_VAR".to_string(), "test_value".to_string());
let result = runner.execute("echo $TEST_VAR", env).await.unwrap();
assert!(result.success);
assert!(result.stdout.contains("test_value"));
}
#[tokio::test]
async fn test_native_execution_timeout() {
let config = NativeConfig {
executable: "bash".to_string(),
max_execution_time: Duration::from_secs(1),
..Default::default()
};
let runner = NativeRunner::new(config).unwrap();
let result = runner.execute("sleep 5", HashMap::new()).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("timed out"));
}
#[tokio::test]
async fn test_executable_validation() {
let config = NativeConfig {
executable: "malicious_exe".to_string(),
allowed_executables: vec!["bash".to_string(), "python3".to_string()],
..Default::default()
};
let result = NativeRunner::new(config);
assert!(result.is_err());
}
#[tokio::test]
async fn test_working_directory_validation() {
let config = NativeConfig {
working_directory: PathBuf::from("relative/path"),
..Default::default()
};
let result = NativeRunner::new(config);
assert!(result.is_err());
}
}