pub mod config;
use crate::backend::{Backend, SandboxBackend};
use crate::lifecycle::{ExecResult, SandboxConfig};
use crate::policy::SandboxPolicy;
use config::FirecrackerConfig;
#[derive(Debug)]
pub struct FirecrackerBackend {
sandbox_config: SandboxConfig,
fc_config: FirecrackerConfig,
}
impl FirecrackerBackend {
pub fn new(config: &SandboxConfig) -> crate::Result<Self> {
if !Backend::Firecracker.is_available() {
return Err(crate::KavachError::BackendUnavailable(
"firecracker not found in PATH".into(),
));
}
let fc_config = FirecrackerConfig::from_sandbox_config(config);
Ok(Self {
sandbox_config: config.clone(),
fc_config,
})
}
fn jailer_available() -> bool {
crate::backend::which_exists("jailer")
}
fn build_args(config_path: &std::path::Path) -> Vec<String> {
vec![
"--no-api".into(),
"--config-file".into(),
config_path.to_string_lossy().into_owned(),
"--log-path".into(),
"/dev/null".into(),
"--level".into(),
"Warning".into(),
]
}
fn build_jailer_args(
vm_id: &str,
fc_path: &str,
config_path: &std::path::Path,
workdir: &std::path::Path,
) -> Vec<String> {
let uid = uid_gid().0;
let gid = uid_gid().1;
let mut args = vec![
"--id".into(),
vm_id.into(),
"--exec-file".into(),
fc_path.into(),
"--uid".into(),
uid.to_string(),
"--gid".into(),
gid.to_string(),
"--chroot-base-dir".into(),
workdir.to_string_lossy().into_owned(),
];
if crate::backend::capabilities::cgroup_is_v2() {
args.extend(["--cgroup-version".into(), "2".into()]);
}
args.push("--".into());
args.extend(Self::build_args(config_path));
args
}
}
#[async_trait::async_trait]
impl SandboxBackend for FirecrackerBackend {
fn backend_type(&self) -> Backend {
Backend::Firecracker
}
async fn exec(&self, command: &str, policy: &SandboxPolicy) -> crate::Result<ExecResult> {
let vm_id = format!("kavach-fc-{}", uuid::Uuid::new_v4().as_simple());
let workdir = tempfile::tempdir()
.map_err(|e| crate::KavachError::CreationFailed(format!("FC workdir: {e}")))?;
let task_script = workdir.path().join("task.sh");
std::fs::write(&task_script, format!("#!/bin/sh\n{command}\n"))
.map_err(|e| crate::KavachError::CreationFailed(format!("write task script: {e}")))?;
let config_path = self.fc_config.write_config(workdir.path())?;
let _ = policy;
let (program, args) = if Self::jailer_available() {
let fc_path = find_binary("firecracker").unwrap_or("firecracker".into());
(
"jailer".to_string(),
Self::build_jailer_args(&vm_id, &fc_path, &config_path, workdir.path()),
)
} else {
("firecracker".to_string(), Self::build_args(&config_path))
};
let mut cmd = tokio::process::Command::new(&program);
cmd.args(&args);
for (k, v) in &self.sandbox_config.env {
cmd.env(k, v);
}
crate::backend::exec_util::execute_with_timeout(
&mut cmd,
self.sandbox_config.timeout_ms,
&program,
)
.await
}
async fn health_check(&self) -> crate::Result<bool> {
let output = tokio::process::Command::new("firecracker")
.arg("--version")
.output()
.await
.map_err(|e| crate::KavachError::ExecFailed(format!("FC health: {e}")))?;
Ok(output.status.success())
}
async fn destroy(&self) -> crate::Result<()> {
Ok(())
}
}
fn uid_gid() -> (u32, u32) {
#[cfg(target_os = "linux")]
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
let mut uid = 0u32;
let mut gid = 0u32;
for line in status.lines() {
if let Some(val) = line.strip_prefix("Uid:")
&& let Some(first) = val.split_whitespace().next()
{
uid = first.parse().unwrap_or(0);
}
if let Some(val) = line.strip_prefix("Gid:")
&& let Some(first) = val.split_whitespace().next()
{
gid = first.parse().unwrap_or(0);
}
}
return (uid, gid);
}
(1000, 1000)
}
fn find_binary(name: &str) -> Option<String> {
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(':') {
let full = std::path::Path::new(dir).join(name);
if full.exists() {
return Some(full.to_string_lossy().into_owned());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_args_basic() {
let args = FirecrackerBackend::build_args(std::path::Path::new("/tmp/fc.json"));
assert!(args.contains(&"--no-api".to_string()));
assert!(args.contains(&"--config-file".to_string()));
assert!(args.contains(&"/tmp/fc.json".to_string()));
}
#[test]
fn find_binary_exists() {
assert!(find_binary("echo").is_some() || find_binary("sh").is_some());
}
#[test]
fn find_binary_not_found() {
assert!(find_binary("nonexistent_binary_xyz_123").is_none());
}
}