Skip to main content
Glama

microsandbox

by microsandbox
vm.rs32.3 kB
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}; //-------------------------------------------------------------------------------------------------- // Constants //-------------------------------------------------------------------------------------------------- /// The prefix used for virtio-fs tags when mounting shared directories pub const VIRTIOFS_TAG_PREFIX: &str = "virtiofs"; //-------------------------------------------------------------------------------------------------- // Types //-------------------------------------------------------------------------------------------------- /// A lightweight Linux virtual machine. /// /// MicroVm provides a secure, isolated environment for running applications with their own /// filesystem, network, and resource constraints. /// /// ## Examples /// /// ```no_run /// use microsandbox_core::vm::{MicroVm, Rootfs}; /// use tempfile::TempDir; /// /// # fn main() -> anyhow::Result<()> { /// let temp_dir = TempDir::new()?; /// let vm = MicroVm::builder() /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf())) /// .ram_mib(1024) /// .exec_path("/bin/echo") /// .args(["Hello, World!"]) /// .build()?; /// /// // Start the MicroVm /// vm.start()?; // This would actually run the VM /// # Ok(()) /// # } /// ``` #[derive(Debug, Getters)] pub struct MicroVm { /// The context ID for the MicroVm configuration. ctx_id: u32, /// The configuration for the MicroVm. #[get = "pub with_prefix"] config: MicroVmConfig, } /// The type of rootfs to use for the MicroVm. /// /// This enum represents the different types of rootfss that can be used for the MicroVm. /// /// ## Variants /// /// * `Native(PathBuf)` - A native rootfs using a single path. /// * `Overlayfs(Vec<PathBuf>)` - An overlayfs rootfs using a list of paths. /// /// ## Examples /// /// ```rust /// use microsandbox_core::vm::Rootfs; /// use std::path::PathBuf; /// /// let native_root = Rootfs::Native(PathBuf::from("/path/to/root")); /// let overlayfs_root = Rootfs::Overlayfs(vec![PathBuf::from("/path/to/root1"), PathBuf::from("/path/to/root2")]); /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Rootfs { /// A rootfs using underlying native filesystem. Native(PathBuf), /// An overlayfs rootfs using a list of paths. Overlayfs(Vec<PathBuf>), } /// Configuration for a MicroVm instance. /// /// This struct holds all the settings needed to create and run a MicroVm, /// including system resources, filesystem configuration, networking, and /// process execution details. /// /// Rather than creating this directly, use `MicroVmConfigBuilder` or /// `MicroVmBuilder` for a more ergonomic interface. /// /// ## Examples /// /// ```rust /// use microsandbox_core::vm::{MicroVm, MicroVmConfig, Rootfs}; /// use tempfile::TempDir; /// /// # fn main() -> anyhow::Result<()> { /// let temp_dir = TempDir::new()?; /// let config = MicroVmConfig::builder() /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf())) /// .memory_mib(1024) /// .exec_path("/bin/echo") /// .build(); /// /// let vm = MicroVm::from_config(config)?; /// # Ok(()) /// # } /// ``` #[derive(Debug)] pub struct MicroVmConfig { /// The log level to use for the MicroVm. pub log_level: LogLevel, /// The rootfs for the MicroVm. pub rootfs: Rootfs, /// The number of vCPUs to use for the MicroVm. pub num_vcpus: u8, /// The amount of memory in MiB to use for the MicroVm. pub memory_mib: u32, /// The directories to mount in the MicroVm using virtio-fs. /// Each PathPair represents a host:guest path mapping. pub mapped_dirs: Vec<PathPair>, /// The port map to use for the MicroVm. pub port_map: Vec<PortPair>, /// The network scope to use for the MicroVm. pub scope: NetworkScope, /// The IP address to use for the MicroVm. pub ip: Option<Ipv4Addr>, /// The subnet to use for the MicroVm. pub subnet: Option<Ipv4Network>, /// The resource limits to use for the MicroVm. pub rlimits: Vec<LinuxRlimit>, /// The working directory path to use for the MicroVm. pub workdir_path: Option<Utf8UnixPathBuf>, /// The executable path to use for the MicroVm. pub exec_path: Utf8UnixPathBuf, /// The arguments to pass to the executable. pub args: Vec<String>, /// The environment variables to set for the executable. pub env: Vec<EnvPair>, /// The console output path to use for the MicroVm. pub console_output: Option<Utf8UnixPathBuf>, } /// The log level to use for the MicroVm. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(u8)] pub enum LogLevel { /// No logging. #[default] Off = 0, /// Error messages. Error = 1, /// Warning messages. Warn = 2, /// Informational messages. Info = 3, /// Debug messages. Debug = 4, /// Trace messages. Trace = 5, } //-------------------------------------------------------------------------------------------------- // Methods //-------------------------------------------------------------------------------------------------- impl MicroVm { /// Creates a new MicroVm from the given configuration. /// /// This is a low-level constructor - prefer using `MicroVm::builder()` /// for a more ergonomic interface. /// /// ## Errors /// Returns an error if: /// - The configuration is invalid /// - Required resources cannot be allocated /// - The system lacks required capabilities 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 }) } /// Creates a builder for configuring a new MicroVm instance. /// /// This is the recommended way to create a new MicroVm. /// /// ## Examples /// /// ```rust /// use microsandbox_core::vm::{MicroVm, Rootfs}; /// use tempfile::TempDir; /// /// # fn main() -> anyhow::Result<()> { /// let temp_dir = TempDir::new()?; /// let vm = MicroVm::builder() /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf())) /// .ram_mib(1024) /// .exec_path("/bin/echo") /// .build()?; /// # Ok(()) /// # } /// ``` pub fn builder() -> MicroVmBuilder<(), ()> { MicroVmBuilder::default() } /// Starts the MicroVm and waits for it to complete. /// /// This function will block until the MicroVm exits. The exit status /// of the guest process is returned. /// /// ## Examples /// /// ```rust,no_run /// use microsandbox_core::vm::{MicroVm, Rootfs}; /// use tempfile::TempDir; /// /// # fn main() -> anyhow::Result<()> { /// let temp_dir = TempDir::new()?; /// let vm = MicroVm::builder() /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf())) /// .ram_mib(1024) /// .exec_path("/usr/bin/python3") /// .args(["-c", "print('Hello from MicroVm!')"]) /// .build()?; /// /// // let status = vm.start()?; /// // assert_eq!(status, 0); // Process exited successfully /// # Ok(()) /// # } /// ``` /// /// ## Notes /// - This function takes control of stdin/stdout /// - The MicroVm is automatically cleaned up when this returns /// - A non-zero status indicates the guest process failed 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) } /// Creates a new MicroVm context. 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 } /// Applies the configuration to the MicroVm context. /// /// This method configures all aspects of the MicroVm including: /// - Basic VM settings (vCPUs, RAM) /// - Root filesystem /// - Directory mappings via virtio-fs /// - Port mappings /// - Resource limits /// - Working directory /// - Executable and arguments /// - Environment variables /// - Console output /// - Network settings /// /// ## Arguments /// * `ctx_id` - The MicroVm context ID to configure /// * `config` - The configuration to apply /// /// ## Panics /// Panics if: /// - Any libkrun API call fails /// - Cannot update the rootfs fstab file fn apply_config(ctx_id: u32, config: &MicroVmConfig) { // Set log level unsafe { let status = ffi::krun_set_log_level(config.log_level as u32); assert!(status >= 0, "failed to set log level: {}", status); } // Set basic VM configuration 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); } // Set rootfs. 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); // Add mapped directories using virtio-fs 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()); // Canonicalize the host path 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); } } // Set port map 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); } // Set network scope 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); } // Set resource limits 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); } } // Set working directory 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); } } // Set executable path, arguments, and environment variables 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 ); } // Set console output 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 { /// Creates a builder for configuring a new MicroVm configuration. /// /// This is the recommended way to create a new MicroVmConfig instance. The builder pattern /// provides a more ergonomic interface and ensures all required fields are set. /// /// ## Examples /// /// ```rust /// use microsandbox_core::vm::{MicroVmConfig, Rootfs}; /// use tempfile::TempDir; /// /// # fn main() -> anyhow::Result<()> { /// let temp_dir = TempDir::new()?; /// let config = MicroVmConfig::builder() /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf())) /// .memory_mib(1024) /// .exec_path("/bin/echo") /// .build(); /// # Ok(()) /// # } /// ``` pub fn builder() -> MicroVmConfigBuilder<(), ()> { MicroVmConfigBuilder::default() } /// Validates that guest paths are not subsets of each other. /// /// For example, these paths would conflict: /// - /app and /app/data /// - /var/log and /var /// - /data and /data /// /// ## Arguments /// * `mapped_dirs` - The mapped directories to validate /// /// ## Returns /// - Ok(()) if no paths are subsets of each other /// - Err with details about conflicting paths fn validate_guest_paths(mapped_dirs: &[PathPair]) -> MicrosandboxResult<()> { // Early return if we have 0 or 1 paths - no conflicts possible if mapped_dirs.len() <= 1 { return Ok(()); } // Pre-normalize all paths once to avoid repeated normalization 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<_>>>()?; // Compare each path with every other path only once // Using windows of size 2 would miss some comparisons since we need to check both directions for i in 0..normalized_paths.len() { let path1 = &normalized_paths[i]; // Only need to check paths after this one since previous comparisons were already done for path2 in &normalized_paths[i + 1..] { // Check both directions for prefix relationship if utils::paths_overlap(path1, path2) { return Err(MicrosandboxError::InvalidMicroVMConfig( InvalidMicroVMConfigError::ConflictingGuestPaths( path1.to_string(), path2.to_string(), ), )); } } } Ok(()) } /// Validates the MicroVm configuration. /// /// Performs a series of checks to ensure the configuration is valid: /// - Verifies the root path exists and is accessible /// - Verifies all host paths in mapped_dirs exist and are accessible /// - Ensures number of vCPUs is non-zero /// - Ensures memory allocation is non-zero /// - Validates executable path and arguments contain only printable ASCII characters /// - Validates guest paths don't overlap or conflict with each other /// /// ## Returns /// - `Ok(())` if the configuration is valid /// - `Err(MicrosandboxError::InvalidMicroVMConfig)` with details about what failed /// /// ## Examples /// ```rust /// use microsandbox_core::vm::{MicroVmConfig, Rootfs}; /// use tempfile::TempDir; /// /// # fn main() -> anyhow::Result<()> { /// let temp_dir = TempDir::new()?; /// let config = MicroVmConfig::builder() /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf())) /// .memory_mib(1024) /// .exec_path("/bin/echo") /// .build(); /// /// assert!(config.validate().is_ok()); /// # Ok(()) /// # } /// ``` pub fn validate(&self) -> MicrosandboxResult<()> { // Check that paths specified in rootfs exist 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(), ), )); } } } } // Check all host paths in mapped_dirs exist 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, )); } // Validate memory_mib is not zero 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)?; } // Validate guest paths are not subsets of each other Self::validate_guest_paths(&self.mapped_dirs)?; Ok(()) } /// Validates that a command line string contains only allowed characters. /// /// Command line strings (executable paths and arguments) must contain only printable ASCII /// characters in the range from space (0x20) to tilde (0x7E). This excludes: /// - Control characters (newlines, tabs, etc.) /// - Non-ASCII Unicode characters /// - Null bytes /// /// ## Arguments /// * `s` - The string to validate /// /// ## Returns /// - `Ok(())` if the string contains only valid characters /// - `Err(MicrosandboxError::InvalidMicroVMConfig)` if invalid characters are found /// /// ## Examples /// ```rust /// use microsandbox_core::vm::MicroVmConfig; /// /// // Valid strings /// assert!(MicroVmConfig::validate_command_line("/bin/echo").is_ok()); /// assert!(MicroVmConfig::validate_command_line("Hello, World!").is_ok()); /// /// // Invalid strings /// assert!(MicroVmConfig::validate_command_line("/bin/echo\n").is_err()); // newline /// assert!(MicroVmConfig::validate_command_line("hello🌎").is_err()); // emoji /// ``` 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()), )) } } } //-------------------------------------------------------------------------------------------------- // Trait Implementations //-------------------------------------------------------------------------------------------------- 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)), } } } //-------------------------------------------------------------------------------------------------- // Tests //-------------------------------------------------------------------------------------------------- #[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() { // Test basic ASCII 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()); // Test edge cases of valid range (space to tilde) assert!(MicroVmConfig::validate_command_line(" ").is_ok()); // space (0x20) assert!(MicroVmConfig::validate_command_line("~").is_ok()); // tilde (0x7E) // Test special characters within valid range 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() { // Test control characters assert!(MicroVmConfig::validate_command_line("\n").is_err()); // newline assert!(MicroVmConfig::validate_command_line("\t").is_err()); // tab assert!(MicroVmConfig::validate_command_line("\r").is_err()); // carriage return assert!(MicroVmConfig::validate_command_line("\x1B").is_err()); // escape // Test non-ASCII Unicode characters assert!(MicroVmConfig::validate_command_line("hello🌎").is_err()); // emoji assert!(MicroVmConfig::validate_command_line("über").is_err()); // umlaut assert!(MicroVmConfig::validate_command_line("café").is_err()); // accent assert!(MicroVmConfig::validate_command_line("你好").is_err()); // Chinese characters // Test strings mixing valid and invalid characters assert!(MicroVmConfig::validate_command_line("hello\nworld").is_err()); assert!(MicroVmConfig::validate_command_line("path/to/file\0").is_err()); // null byte assert!(MicroVmConfig::validate_command_line("hello\x7F").is_err()); // DEL character } #[test] fn test_validate_command_line_in_config() { let temp_dir = TempDir::new().unwrap(); // Test invalid executable path 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(_) )) )); // Test invalid argument 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<()> { // Test valid paths (no conflicts) let valid_paths = vec![ "/app".parse::<PathPair>()?, "/data".parse()?, "/var/log".parse()?, "/etc/config".parse()?, ]; assert!(MicroVmConfig::validate_guest_paths(&valid_paths).is_ok()); // Test conflicting paths (direct match) let conflicting_paths = vec![ "/app".parse()?, "/data".parse()?, "/app".parse()?, // Duplicate ]; assert!(MicroVmConfig::validate_guest_paths(&conflicting_paths).is_err()); // Test conflicting paths (subset) let subset_paths = vec![ "/app".parse()?, "/app/data".parse()?, // Subset of /app "/var/log".parse()?, ]; assert!(MicroVmConfig::validate_guest_paths(&subset_paths).is_err()); // Test conflicting paths (parent) let parent_paths = vec![ "/var/log".parse()?, "/var".parse()?, // Parent of /var/log "/etc".parse()?, ]; assert!(MicroVmConfig::validate_guest_paths(&parent_paths).is_err()); // Test paths needing normalization let unnormalized_paths = vec![ "/app/./data".parse()?, "/var/log".parse()?, "/etc//config".parse()?, ]; assert!(MicroVmConfig::validate_guest_paths(&unnormalized_paths).is_ok()); // Test paths with normalization conflicts let normalized_conflicts = vec![ "/app/./data".parse()?, "/app/data/".parse()?, // Same as first path after normalization "/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)?; // Test valid configuration 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()); // Test configuration with conflicting guest paths 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(()) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/microsandbox/microsandbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server