use std::{ffi::CString, net::Ipv4Addr, path::PathBuf, ptr};
use getset::Getters;
use ipnetwork::Ipv4Network;
use microsandbox_utils::SupportedPathType;
use typed_path::Utf8UnixPathBuf;
use crate::{
config::{EnvPair, NetworkScope, PathPair, PortPair},
utils, InvalidMicroVMConfigError, MicrosandboxError, MicrosandboxResult,
};
use super::{ffi, LinuxRlimit, MicroVmBuilder, MicroVmConfigBuilder};
pub const VIRTIOFS_TAG_PREFIX: &str = "virtiofs";
#[derive(Debug, Getters)]
pub struct MicroVm {
ctx_id: u32,
#[get = "pub with_prefix"]
config: MicroVmConfig,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Rootfs {
Native(PathBuf),
Overlayfs(Vec<PathBuf>),
}
#[derive(Debug)]
pub struct MicroVmConfig {
pub log_level: LogLevel,
pub rootfs: Rootfs,
pub num_vcpus: u8,
pub memory_mib: u32,
pub mapped_dirs: Vec<PathPair>,
pub port_map: Vec<PortPair>,
pub scope: NetworkScope,
pub ip: Option<Ipv4Addr>,
pub subnet: Option<Ipv4Network>,
pub rlimits: Vec<LinuxRlimit>,
pub workdir_path: Option<Utf8UnixPathBuf>,
pub exec_path: Utf8UnixPathBuf,
pub args: Vec<String>,
pub env: Vec<EnvPair>,
pub console_output: Option<Utf8UnixPathBuf>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(u8)]
pub enum LogLevel {
#[default]
Off = 0,
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
Trace = 5,
}
impl MicroVm {
pub fn from_config(config: MicroVmConfig) -> MicrosandboxResult<Self> {
let ctx_id = Self::create_ctx();
config.validate()?;
Self::apply_config(ctx_id, &config);
Ok(Self { ctx_id, config })
}
pub fn builder() -> MicroVmBuilder<(), ()> {
MicroVmBuilder::default()
}
pub fn start(&self) -> MicrosandboxResult<i32> {
let ctx_id = self.ctx_id;
let status = unsafe { ffi::krun_start_enter(ctx_id) };
if status < 0 {
tracing::error!("failed to start microvm: {}", status);
return Err(MicrosandboxError::StartVmFailed(status));
}
tracing::info!("microvm exited with status: {}", status);
Ok(status)
}
fn create_ctx() -> u32 {
let ctx_id = unsafe { ffi::krun_create_ctx() };
assert!(ctx_id >= 0, "failed to create microvm context: {}", ctx_id);
ctx_id as u32
}
fn apply_config(ctx_id: u32, config: &MicroVmConfig) {
unsafe {
let status = ffi::krun_set_log_level(config.log_level as u32);
assert!(status >= 0, "failed to set log level: {}", status);
}
unsafe {
let status = ffi::krun_set_vm_config(ctx_id, config.num_vcpus, config.memory_mib);
assert!(status >= 0, "failed to set VM config: {}", status);
}
match &config.rootfs {
Rootfs::Native(path) => {
let c_path = CString::new(path.to_str().unwrap().as_bytes()).unwrap();
unsafe {
let status = ffi::krun_set_root(ctx_id, c_path.as_ptr());
assert!(status >= 0, "failed to set rootfs: {}", status);
}
}
Rootfs::Overlayfs(paths) => {
tracing::debug!("setting overlayfs rootfs: {:?}", paths);
let c_paths: Vec<_> = paths
.iter()
.map(|p| CString::new(p.to_str().unwrap().as_bytes()).unwrap())
.collect();
let c_paths_ptrs = utils::to_null_terminated_c_array(&c_paths);
unsafe {
let status = ffi::krun_set_overlayfs_root(ctx_id, c_paths_ptrs.as_ptr());
assert!(status >= 0, "failed to set rootfs: {}", status);
}
}
}
tracing::debug!("applying config: {:#?}", config);
let mapped_dirs = &config.mapped_dirs;
for (idx, dir) in mapped_dirs.iter().enumerate() {
let tag = CString::new(format!("{}_{}", VIRTIOFS_TAG_PREFIX, idx)).unwrap();
tracing::debug!("adding virtiofs mount for {}", tag.to_string_lossy());
let host_path_buf = PathBuf::from(dir.get_host().as_str());
let canonical_host_path = match host_path_buf.canonicalize() {
Ok(path) => path,
Err(e) => {
tracing::error!("failed to canonicalize host path: {}", e);
panic!("failed to canonicalize host path: {}", e);
}
};
let host_path = CString::new(canonical_host_path.to_string_lossy().as_bytes()).unwrap();
tracing::debug!("canonical host path: {}", host_path.to_string_lossy());
unsafe {
let status = ffi::krun_add_virtiofs(ctx_id, tag.as_ptr(), host_path.as_ptr());
assert!(status >= 0, "failed to add mapped directory: {}", status);
}
}
let c_port_map: Vec<_> = config
.port_map
.iter()
.map(|p| CString::new(p.to_string()).unwrap())
.collect();
let c_port_map_ptrs = utils::to_null_terminated_c_array(&c_port_map);
unsafe {
let status = ffi::krun_set_port_map(ctx_id, c_port_map_ptrs.as_ptr());
assert!(status >= 0, "failed to set port map: {}", status);
}
unsafe {
let status =
ffi::krun_set_tsi_scope(ctx_id, ptr::null(), ptr::null(), config.scope as u8);
assert!(status >= 0, "failed to set network scope: {}", status);
}
if !config.rlimits.is_empty() {
let c_rlimits: Vec<_> = config
.rlimits
.iter()
.map(|s| CString::new(s.to_string()).unwrap())
.collect();
let c_rlimits_ptrs = utils::to_null_terminated_c_array(&c_rlimits);
unsafe {
let status = ffi::krun_set_rlimits(ctx_id, c_rlimits_ptrs.as_ptr());
assert!(status >= 0, "failed to set resource limits: {}", status);
}
}
if let Some(workdir) = &config.workdir_path {
let c_workdir = CString::new(workdir.to_string().as_bytes()).unwrap();
unsafe {
let status = ffi::krun_set_workdir(ctx_id, c_workdir.as_ptr());
assert!(status >= 0, "Failed to set working directory: {}", status);
}
}
let c_exec_path = CString::new(config.exec_path.to_string().as_bytes()).unwrap();
let c_argv: Vec<_> = config
.args
.iter()
.map(|s| CString::new(s.as_str()).unwrap())
.collect();
let c_argv_ptrs = utils::to_null_terminated_c_array(&c_argv);
let c_env: Vec<_> = config
.env
.iter()
.map(|s| CString::new(s.to_string()).unwrap())
.collect();
let c_env_ptrs = utils::to_null_terminated_c_array(&c_env);
unsafe {
let status = ffi::krun_set_exec(
ctx_id,
c_exec_path.as_ptr(),
c_argv_ptrs.as_ptr(),
c_env_ptrs.as_ptr(),
);
assert!(
status >= 0,
"Failed to set executable configuration: {}",
status
);
}
if let Some(console_output) = &config.console_output {
let c_console_output = CString::new(console_output.to_string().as_bytes()).unwrap();
unsafe {
let status = ffi::krun_set_console_output(ctx_id, c_console_output.as_ptr());
assert!(status >= 0, "Failed to set console output: {}", status);
}
}
}
}
impl MicroVmConfig {
pub fn builder() -> MicroVmConfigBuilder<(), ()> {
MicroVmConfigBuilder::default()
}
fn validate_guest_paths(mapped_dirs: &[PathPair]) -> MicrosandboxResult<()> {
if mapped_dirs.len() <= 1 {
return Ok(());
}
let normalized_paths: Vec<_> = mapped_dirs
.iter()
.map(|dir| {
microsandbox_utils::normalize_path(
dir.get_guest().as_str(),
SupportedPathType::Absolute,
)
.map_err(Into::into)
})
.collect::<MicrosandboxResult<Vec<_>>>()?;
for i in 0..normalized_paths.len() {
let path1 = &normalized_paths[i];
for path2 in &normalized_paths[i + 1..] {
if utils::paths_overlap(path1, path2) {
return Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::ConflictingGuestPaths(
path1.to_string(),
path2.to_string(),
),
));
}
}
}
Ok(())
}
pub fn validate(&self) -> MicrosandboxResult<()> {
match &self.rootfs {
Rootfs::Native(path) => {
if !path.exists() {
return Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::RootPathDoesNotExist(
path.to_str().unwrap().into(),
),
));
}
}
Rootfs::Overlayfs(paths) => {
for path in paths {
if !path.exists() {
return Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::RootPathDoesNotExist(
path.to_str().unwrap().into(),
),
));
}
}
}
}
for dir in &self.mapped_dirs {
let host_path = PathBuf::from(dir.get_host().as_str());
if !host_path.exists() {
return Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::HostPathDoesNotExist(
host_path.to_str().unwrap().into(),
),
));
}
}
if self.num_vcpus == 0 {
return Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::NumVCPUsIsZero,
));
}
if self.memory_mib == 0 {
return Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::MemoryIsZero,
));
}
Self::validate_command_line(self.exec_path.as_ref())?;
for arg in &self.args {
Self::validate_command_line(arg)?;
}
Self::validate_guest_paths(&self.mapped_dirs)?;
Ok(())
}
pub fn validate_command_line(s: &str) -> MicrosandboxResult<()> {
fn valid_char(c: char) -> bool {
matches!(c, ' '..='~')
}
if s.chars().all(valid_char) {
Ok(())
} else {
Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::InvalidCommandLineString(s.to_string()),
))
}
}
}
impl Drop for MicroVm {
fn drop(&mut self) {
unsafe { ffi::krun_free_ctx(self.ctx_id) };
}
}
impl TryFrom<u8> for LogLevel {
type Error = MicrosandboxError;
fn try_from(value: u8) -> Result<Self, MicrosandboxError> {
match value {
0 => Ok(LogLevel::Off),
1 => Ok(LogLevel::Error),
2 => Ok(LogLevel::Warn),
3 => Ok(LogLevel::Info),
4 => Ok(LogLevel::Debug),
5 => Ok(LogLevel::Trace),
_ => Err(MicrosandboxError::InvalidLogLevel(value)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use microsandbox_utils::DEFAULT_NUM_VCPUS;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_microvm_config_builder() {
let config = MicroVmConfig::builder()
.log_level(LogLevel::Info)
.rootfs(Rootfs::Native(PathBuf::from("/tmp")))
.memory_mib(512)
.exec_path("/bin/echo")
.build();
assert!(config.log_level == LogLevel::Info);
assert_eq!(config.rootfs, Rootfs::Native(PathBuf::from("/tmp")));
assert_eq!(config.memory_mib, 512);
assert_eq!(config.num_vcpus, DEFAULT_NUM_VCPUS);
}
#[test]
fn test_microvm_config_validation_success() {
let temp_dir = TempDir::new().unwrap();
let config = MicroVmConfig::builder()
.log_level(LogLevel::Info)
.rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
.exec_path("/bin/echo")
.build();
assert!(config.validate().is_ok());
}
#[test]
fn test_microvm_config_validation_failure_root_path() {
let config = MicroVmConfig::builder()
.log_level(LogLevel::Info)
.rootfs(Rootfs::Native(PathBuf::from("/non/existent/path")))
.memory_mib(512)
.exec_path("/bin/echo")
.build();
assert!(matches!(
config.validate(),
Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::RootPathDoesNotExist(_)
))
));
}
#[test]
fn test_microvm_config_validation_failure_zero_ram() {
let temp_dir = TempDir::new().unwrap();
let config = MicroVmConfig::builder()
.log_level(LogLevel::Info)
.rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
.memory_mib(0)
.exec_path("/bin/echo")
.build();
assert!(matches!(
config.validate(),
Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::MemoryIsZero
))
));
}
#[test]
fn test_validate_command_line_valid_strings() {
assert!(MicroVmConfig::validate_command_line("hello").is_ok());
assert!(MicroVmConfig::validate_command_line("hello world").is_ok());
assert!(MicroVmConfig::validate_command_line("Hello, World!").is_ok());
assert!(MicroVmConfig::validate_command_line(" ").is_ok()); assert!(MicroVmConfig::validate_command_line("~").is_ok());
assert!(MicroVmConfig::validate_command_line("!@#$%^&*()").is_ok());
assert!(MicroVmConfig::validate_command_line("path/to/file").is_ok());
assert!(MicroVmConfig::validate_command_line("user-name_123").is_ok());
}
#[test]
fn test_validate_command_line_invalid_strings() {
assert!(MicroVmConfig::validate_command_line("\n").is_err()); assert!(MicroVmConfig::validate_command_line("\t").is_err()); assert!(MicroVmConfig::validate_command_line("\r").is_err()); assert!(MicroVmConfig::validate_command_line("\x1B").is_err());
assert!(MicroVmConfig::validate_command_line("hello🌎").is_err()); assert!(MicroVmConfig::validate_command_line("über").is_err()); assert!(MicroVmConfig::validate_command_line("café").is_err()); assert!(MicroVmConfig::validate_command_line("ä½ å¥½").is_err());
assert!(MicroVmConfig::validate_command_line("hello\nworld").is_err());
assert!(MicroVmConfig::validate_command_line("path/to/file\0").is_err()); assert!(MicroVmConfig::validate_command_line("hello\x7F").is_err()); }
#[test]
fn test_validate_command_line_in_config() {
let temp_dir = TempDir::new().unwrap();
let config = MicroVmConfig::builder()
.rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
.memory_mib(512)
.exec_path("/bin/hello\nworld")
.build();
assert!(matches!(
config.validate(),
Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::InvalidCommandLineString(_)
))
));
let config = MicroVmConfig::builder()
.rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
.memory_mib(512)
.exec_path("/bin/echo")
.args(["hello\tworld"])
.build();
assert!(matches!(
config.validate(),
Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::InvalidCommandLineString(_)
))
));
}
#[test]
fn test_validate_guest_paths() -> anyhow::Result<()> {
let valid_paths = vec![
"/app".parse::<PathPair>()?,
"/data".parse()?,
"/var/log".parse()?,
"/etc/config".parse()?,
];
assert!(MicroVmConfig::validate_guest_paths(&valid_paths).is_ok());
let conflicting_paths = vec![
"/app".parse()?,
"/data".parse()?,
"/app".parse()?, ];
assert!(MicroVmConfig::validate_guest_paths(&conflicting_paths).is_err());
let subset_paths = vec![
"/app".parse()?,
"/app/data".parse()?, "/var/log".parse()?,
];
assert!(MicroVmConfig::validate_guest_paths(&subset_paths).is_err());
let parent_paths = vec![
"/var/log".parse()?,
"/var".parse()?, "/etc".parse()?,
];
assert!(MicroVmConfig::validate_guest_paths(&parent_paths).is_err());
let unnormalized_paths = vec![
"/app/./data".parse()?,
"/var/log".parse()?,
"/etc//config".parse()?,
];
assert!(MicroVmConfig::validate_guest_paths(&unnormalized_paths).is_ok());
let normalized_conflicts = vec![
"/app/./data".parse()?,
"/app/data/".parse()?, "/var/log".parse()?,
];
assert!(MicroVmConfig::validate_guest_paths(&normalized_conflicts).is_err());
Ok(())
}
#[test]
fn test_microvm_config_validation_with_guest_paths() -> anyhow::Result<()> {
use tempfile::TempDir;
let temp_dir = TempDir::new()?;
let host_dir1 = temp_dir.path().join("dir1");
let host_dir2 = temp_dir.path().join("dir2");
std::fs::create_dir_all(&host_dir1)?;
std::fs::create_dir_all(&host_dir2)?;
let valid_config = MicroVmConfig::builder()
.rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
.memory_mib(1024)
.exec_path("/bin/echo")
.mapped_dirs([
format!("{}:/app", host_dir1.display()).parse()?,
format!("{}:/data", host_dir2.display()).parse()?,
])
.build();
assert!(valid_config.validate().is_ok());
let invalid_config = MicroVmConfig::builder()
.rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
.memory_mib(1024)
.exec_path("/bin/echo")
.mapped_dirs([
format!("{}:/app/data", host_dir1.display()).parse()?,
format!("{}:/app", host_dir2.display()).parse()?,
])
.build();
assert!(matches!(
invalid_config.validate(),
Err(MicrosandboxError::InvalidMicroVMConfig(
InvalidMicroVMConfigError::ConflictingGuestPaths(_, _)
))
));
Ok(())
}
}