use std::path::{Path, PathBuf};
use std::process::Command;
use super::{NetworkPolicy, Sandbox, SandboxCommand, SandboxError, SandboxOutput, SandboxSpec};
#[derive(Debug, Clone)]
pub struct VfkitSandbox {
binary: PathBuf,
kernel: PathBuf,
initrd: PathBuf,
cmdline: String,
memory_mib: u32,
cpus: u32,
}
impl VfkitSandbox {
pub fn new(
binary: impl Into<PathBuf>,
kernel: impl Into<PathBuf>,
initrd: impl Into<PathBuf>,
cmdline: impl Into<String>,
) -> Self {
Self {
binary: binary.into(),
kernel: kernel.into(),
initrd: initrd.into(),
cmdline: cmdline.into(),
memory_mib: 512,
cpus: 1,
}
}
#[must_use]
pub fn with_memory_mib(mut self, mib: u32) -> Self {
self.memory_mib = mib;
self
}
#[must_use]
pub fn with_cpus(mut self, cpus: u32) -> Self {
self.cpus = cpus;
self
}
fn device_arg(host_path: &Path, mount_tag: &str) -> String {
format!(
"virtio-fs,sharedDir={},mountTag={mount_tag}",
host_path.display()
)
}
fn build_argv(
&self,
spec: &SandboxSpec,
command: &SandboxCommand,
) -> Result<Vec<String>, SandboxError> {
if let Some(path) = spec.read_only_paths.first() {
return Err(SandboxError::UnsupportedReadOnlyShare { path: path.clone() });
}
let payload = std::iter::once(command.program.as_str())
.chain(command.args.iter().map(String::as_str))
.collect::<Vec<_>>()
.join(" ");
let cmdline = format!("{} -- {payload}", self.cmdline);
let mut argv = vec![
"--bootloader".to_string(),
format!(
"linux,kernel={},initrd={},cmdline=\"{}\"",
self.kernel.display(),
self.initrd.display(),
cmdline
),
"--memory".to_string(),
self.memory_mib.to_string(),
"--cpus".to_string(),
self.cpus.to_string(),
];
for (index, path) in spec.writable_paths.iter().enumerate() {
argv.push("--device".to_string());
argv.push(Self::device_arg(path, &format!("share{index}")));
}
if spec.network == NetworkPolicy::Deny && !spec.writable_paths.is_empty() {
return Err(SandboxError::Unsupported("a network-denying policy"));
}
argv.push("--device".to_string());
argv.push("virtio-net,nat".to_string());
Ok(argv)
}
}
impl Sandbox for VfkitSandbox {
fn name(&self) -> &'static str {
"vfkit"
}
fn is_isolated(&self) -> bool {
true
}
fn exec(
&self,
spec: &SandboxSpec,
command: &SandboxCommand,
) -> Result<SandboxOutput, SandboxError> {
spec.validate()?;
let argv = self.build_argv(spec, command)?;
let output = Command::new(&self.binary)
.args(&argv)
.stdin(std::process::Stdio::null())
.output()
.map_err(SandboxError::Spawn)?;
Ok(SandboxOutput {
status: output.status.code().unwrap_or(-1),
stdout: output.stdout,
stderr: output.stderr,
})
}
}
pub fn which() -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|dir| dir.join("vfkit"))
.find(|candidate| candidate.is_file())
}
#[cfg(test)]
mod tests {
use super::*;
fn backend() -> VfkitSandbox {
VfkitSandbox::new("vfkit", "bzImage", "initrd", "console=ttyS0")
}
#[test]
fn device_string_matches_vfkit_grammar() {
assert_eq!(
VfkitSandbox::device_arg(Path::new("/srv/data"), "share0"),
"virtio-fs,sharedDir=/srv/data,mountTag=share0"
);
}
#[test]
fn reports_real_isolation() {
assert!(backend().is_isolated());
assert_eq!(backend().name(), "vfkit");
}
#[test]
fn read_only_grant_is_refused_not_widened() {
let spec = SandboxSpec::deny_all().with_read_only("/usr");
let err = backend()
.build_argv(&spec, &SandboxCommand::new("/bin/true"))
.expect_err("must refuse a read-only grant");
assert!(matches!(err, SandboxError::UnsupportedReadOnlyShare { .. }));
assert!(err.to_string().contains("read-only"));
}
#[test]
fn writable_share_becomes_a_virtio_fs_device() {
let spec = SandboxSpec::deny_all()
.with_writable("/srv/data")
.with_network(NetworkPolicy::Allow);
let argv = backend()
.build_argv(&spec, &SandboxCommand::new("/bin/true"))
.expect("should build");
assert!(argv.iter().any(|a| a == "--device"));
assert!(argv
.iter()
.any(|a| a == "virtio-fs,sharedDir=/srv/data,mountTag=share0"));
}
#[test]
fn command_payload_lands_in_the_bootloader_cmdline() {
let spec = SandboxSpec::deny_all().with_network(NetworkPolicy::Allow);
let cmd = SandboxCommand::new("/bin/echo").arg("hello");
let argv = backend().build_argv(&spec, &cmd).expect("should build");
let bootloader = argv.get(1).expect("bootloader arg");
assert!(bootloader.starts_with("linux,"), "{bootloader}");
assert!(
bootloader.contains("cmdline=\"console=ttyS0 -- /bin/echo hello\""),
"{bootloader}"
);
for arg in &argv {
if arg.starts_with("--") {
assert!(
matches!(
arg.as_str(),
"--bootloader" | "--memory" | "--cpus" | "--device"
),
"unexpected flag {arg}"
);
}
}
}
#[test]
fn network_denial_is_refused_rather_than_approximated() {
let spec = SandboxSpec::deny_all().with_writable("/srv/data");
let err = backend()
.build_argv(&spec, &SandboxCommand::new("/bin/true"))
.expect_err("must refuse");
assert!(matches!(err, SandboxError::Unsupported(_)));
}
}