microsandbox_core/vm/vm.rs
1use std::{ffi::CString, net::Ipv4Addr, path::PathBuf, ptr};
2
3use getset::Getters;
4use ipnetwork::Ipv4Network;
5use microsandbox_utils::SupportedPathType;
6use typed_path::Utf8UnixPathBuf;
7
8use crate::{
9 config::{EnvPair, NetworkScope, PathPair, PortPair},
10 utils, InvalidMicroVMConfigError, MicrosandboxError, MicrosandboxResult,
11};
12
13use super::{ffi, LinuxRlimit, MicroVmBuilder, MicroVmConfigBuilder};
14
15//--------------------------------------------------------------------------------------------------
16// Constants
17//--------------------------------------------------------------------------------------------------
18
19/// The prefix used for virtio-fs tags when mounting shared directories
20pub const VIRTIOFS_TAG_PREFIX: &str = "virtiofs";
21
22//--------------------------------------------------------------------------------------------------
23// Types
24//--------------------------------------------------------------------------------------------------
25
26/// A lightweight Linux virtual machine.
27///
28/// MicroVm provides a secure, isolated environment for running applications with their own
29/// filesystem, network, and resource constraints.
30///
31/// ## Examples
32///
33/// ```no_run
34/// use microsandbox_core::vm::{MicroVm, Rootfs};
35/// use tempfile::TempDir;
36///
37/// # fn main() -> anyhow::Result<()> {
38/// let temp_dir = TempDir::new()?;
39/// let vm = MicroVm::builder()
40/// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
41/// .ram_mib(1024)
42/// .exec_path("/bin/echo")
43/// .args(["Hello, World!"])
44/// .build()?;
45///
46/// // Start the MicroVm
47/// vm.start()?; // This would actually run the VM
48/// # Ok(())
49/// # }
50/// ```
51#[derive(Debug, Getters)]
52pub struct MicroVm {
53 /// The context ID for the MicroVm configuration.
54 ctx_id: u32,
55
56 /// The configuration for the MicroVm.
57 #[get = "pub with_prefix"]
58 config: MicroVmConfig,
59}
60
61/// The type of rootfs to use for the MicroVm.
62///
63/// This enum represents the different types of rootfss that can be used for the MicroVm.
64///
65/// ## Variants
66///
67/// * `Native(PathBuf)` - A native rootfs using a single path.
68/// * `Overlayfs(Vec<PathBuf>)` - An overlayfs rootfs using a list of paths.
69///
70/// ## Examples
71///
72/// ```rust
73/// use microsandbox_core::vm::Rootfs;
74/// use std::path::PathBuf;
75///
76/// let native_root = Rootfs::Native(PathBuf::from("/path/to/root"));
77/// let overlayfs_root = Rootfs::Overlayfs(vec![PathBuf::from("/path/to/root1"), PathBuf::from("/path/to/root2")]);
78/// ```
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum Rootfs {
81 /// A rootfs using underlying native filesystem.
82 Native(PathBuf),
83
84 /// An overlayfs rootfs using a list of paths.
85 Overlayfs(Vec<PathBuf>),
86}
87
88/// Configuration for a MicroVm instance.
89///
90/// This struct holds all the settings needed to create and run a MicroVm,
91/// including system resources, filesystem configuration, networking, and
92/// process execution details.
93///
94/// Rather than creating this directly, use `MicroVmConfigBuilder` or
95/// `MicroVmBuilder` for a more ergonomic interface.
96///
97/// ## Examples
98///
99/// ```rust
100/// use microsandbox_core::vm::{MicroVm, MicroVmConfig, Rootfs};
101/// use tempfile::TempDir;
102///
103/// # fn main() -> anyhow::Result<()> {
104/// let temp_dir = TempDir::new()?;
105/// let config = MicroVmConfig::builder()
106/// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
107/// .memory_mib(1024)
108/// .exec_path("/bin/echo")
109/// .build();
110///
111/// let vm = MicroVm::from_config(config)?;
112/// # Ok(())
113/// # }
114/// ```
115#[derive(Debug)]
116pub struct MicroVmConfig {
117 /// The log level to use for the MicroVm.
118 pub log_level: LogLevel,
119
120 /// The rootfs for the MicroVm.
121 pub rootfs: Rootfs,
122
123 /// The number of vCPUs to use for the MicroVm.
124 pub num_vcpus: u8,
125
126 /// The amount of memory in MiB to use for the MicroVm.
127 pub memory_mib: u32,
128
129 /// The directories to mount in the MicroVm using virtio-fs.
130 /// Each PathPair represents a host:guest path mapping.
131 pub mapped_dirs: Vec<PathPair>,
132
133 /// The port map to use for the MicroVm.
134 pub port_map: Vec<PortPair>,
135
136 /// The network scope to use for the MicroVm.
137 pub scope: NetworkScope,
138
139 /// The IP address to use for the MicroVm.
140 pub ip: Option<Ipv4Addr>,
141
142 /// The subnet to use for the MicroVm.
143 pub subnet: Option<Ipv4Network>,
144
145 /// The resource limits to use for the MicroVm.
146 pub rlimits: Vec<LinuxRlimit>,
147
148 /// The working directory path to use for the MicroVm.
149 pub workdir_path: Option<Utf8UnixPathBuf>,
150
151 /// The executable path to use for the MicroVm.
152 pub exec_path: Utf8UnixPathBuf,
153
154 /// The arguments to pass to the executable.
155 pub args: Vec<String>,
156
157 /// The environment variables to set for the executable.
158 pub env: Vec<EnvPair>,
159
160 /// The console output path to use for the MicroVm.
161 pub console_output: Option<Utf8UnixPathBuf>,
162}
163
164/// The log level to use for the MicroVm.
165#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
166#[repr(u8)]
167pub enum LogLevel {
168 /// No logging.
169 #[default]
170 Off = 0,
171
172 /// Error messages.
173 Error = 1,
174
175 /// Warning messages.
176 Warn = 2,
177
178 /// Informational messages.
179 Info = 3,
180
181 /// Debug messages.
182 Debug = 4,
183
184 /// Trace messages.
185 Trace = 5,
186}
187
188//--------------------------------------------------------------------------------------------------
189// Methods
190//--------------------------------------------------------------------------------------------------
191
192impl MicroVm {
193 /// Creates a new MicroVm from the given configuration.
194 ///
195 /// This is a low-level constructor - prefer using `MicroVm::builder()`
196 /// for a more ergonomic interface.
197 ///
198 /// ## Errors
199 /// Returns an error if:
200 /// - The configuration is invalid
201 /// - Required resources cannot be allocated
202 /// - The system lacks required capabilities
203 pub fn from_config(config: MicroVmConfig) -> MicrosandboxResult<Self> {
204 let ctx_id = Self::create_ctx();
205
206 config.validate()?;
207
208 Self::apply_config(ctx_id, &config);
209
210 Ok(Self { ctx_id, config })
211 }
212
213 /// Creates a builder for configuring a new MicroVm instance.
214 ///
215 /// This is the recommended way to create a new MicroVm.
216 ///
217 /// ## Examples
218 ///
219 /// ```rust
220 /// use microsandbox_core::vm::{MicroVm, Rootfs};
221 /// use tempfile::TempDir;
222 ///
223 /// # fn main() -> anyhow::Result<()> {
224 /// let temp_dir = TempDir::new()?;
225 /// let vm = MicroVm::builder()
226 /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
227 /// .ram_mib(1024)
228 /// .exec_path("/bin/echo")
229 /// .build()?;
230 /// # Ok(())
231 /// # }
232 /// ```
233 pub fn builder() -> MicroVmBuilder<(), ()> {
234 MicroVmBuilder::default()
235 }
236
237 /// Starts the MicroVm and waits for it to complete.
238 ///
239 /// This function will block until the MicroVm exits. The exit status
240 /// of the guest process is returned.
241 ///
242 /// ## Examples
243 ///
244 /// ```rust,no_run
245 /// use microsandbox_core::vm::{MicroVm, Rootfs};
246 /// use tempfile::TempDir;
247 ///
248 /// # fn main() -> anyhow::Result<()> {
249 /// let temp_dir = TempDir::new()?;
250 /// let vm = MicroVm::builder()
251 /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
252 /// .ram_mib(1024)
253 /// .exec_path("/usr/bin/python3")
254 /// .args(["-c", "print('Hello from MicroVm!')"])
255 /// .build()?;
256 ///
257 /// // let status = vm.start()?;
258 /// // assert_eq!(status, 0); // Process exited successfully
259 /// # Ok(())
260 /// # }
261 /// ```
262 ///
263 /// ## Notes
264 /// - This function takes control of stdin/stdout
265 /// - The MicroVm is automatically cleaned up when this returns
266 /// - A non-zero status indicates the guest process failed
267 pub fn start(&self) -> MicrosandboxResult<i32> {
268 let ctx_id = self.ctx_id;
269 let status = unsafe { ffi::krun_start_enter(ctx_id) };
270 if status < 0 {
271 tracing::error!("failed to start microvm: {}", status);
272 return Err(MicrosandboxError::StartVmFailed(status));
273 }
274 tracing::info!("microvm exited with status: {}", status);
275 Ok(status)
276 }
277
278 /// Creates a new MicroVm context.
279 fn create_ctx() -> u32 {
280 let ctx_id = unsafe { ffi::krun_create_ctx() };
281 assert!(ctx_id >= 0, "failed to create microvm context: {}", ctx_id);
282 ctx_id as u32
283 }
284
285 /// Applies the configuration to the MicroVm context.
286 ///
287 /// This method configures all aspects of the MicroVm including:
288 /// - Basic VM settings (vCPUs, RAM)
289 /// - Root filesystem
290 /// - Directory mappings via virtio-fs
291 /// - Port mappings
292 /// - Resource limits
293 /// - Working directory
294 /// - Executable and arguments
295 /// - Environment variables
296 /// - Console output
297 /// - Network settings
298 ///
299 /// ## Arguments
300 /// * `ctx_id` - The MicroVm context ID to configure
301 /// * `config` - The configuration to apply
302 ///
303 /// ## Panics
304 /// Panics if:
305 /// - Any libkrun API call fails
306 /// - Cannot update the rootfs fstab file
307 fn apply_config(ctx_id: u32, config: &MicroVmConfig) {
308 // Set log level
309 unsafe {
310 let status = ffi::krun_set_log_level(config.log_level as u32);
311 assert!(status >= 0, "failed to set log level: {}", status);
312 }
313
314 // Set basic VM configuration
315 unsafe {
316 let status = ffi::krun_set_vm_config(ctx_id, config.num_vcpus, config.memory_mib);
317 assert!(status >= 0, "failed to set VM config: {}", status);
318 }
319
320 // Set rootfs.
321 match &config.rootfs {
322 Rootfs::Native(path) => {
323 let c_path = CString::new(path.to_str().unwrap().as_bytes()).unwrap();
324 unsafe {
325 let status = ffi::krun_set_root(ctx_id, c_path.as_ptr());
326 assert!(status >= 0, "failed to set rootfs: {}", status);
327 }
328 }
329 Rootfs::Overlayfs(paths) => {
330 tracing::debug!("setting overlayfs rootfs: {:?}", paths);
331 let c_paths: Vec<_> = paths
332 .iter()
333 .map(|p| CString::new(p.to_str().unwrap().as_bytes()).unwrap())
334 .collect();
335 let c_paths_ptrs = utils::to_null_terminated_c_array(&c_paths);
336 unsafe {
337 let status = ffi::krun_set_overlayfs_root(ctx_id, c_paths_ptrs.as_ptr());
338 assert!(status >= 0, "failed to set rootfs: {}", status);
339 }
340 }
341 }
342
343 tracing::debug!("applying config: {:#?}", config);
344
345 // Add mapped directories using virtio-fs
346 let mapped_dirs = &config.mapped_dirs;
347 for (idx, dir) in mapped_dirs.iter().enumerate() {
348 let tag = CString::new(format!("{}_{}", VIRTIOFS_TAG_PREFIX, idx)).unwrap();
349 tracing::debug!("adding virtiofs mount for {}", tag.to_string_lossy());
350
351 // Canonicalize the host path
352 let host_path_buf = PathBuf::from(dir.get_host().as_str());
353 let canonical_host_path = match host_path_buf.canonicalize() {
354 Ok(path) => path,
355 Err(e) => {
356 tracing::error!("failed to canonicalize host path: {}", e);
357 panic!("failed to canonicalize host path: {}", e);
358 }
359 };
360
361 let host_path = CString::new(canonical_host_path.to_string_lossy().as_bytes()).unwrap();
362 tracing::debug!("canonical host path: {}", host_path.to_string_lossy());
363
364 unsafe {
365 let status = ffi::krun_add_virtiofs(ctx_id, tag.as_ptr(), host_path.as_ptr());
366 assert!(status >= 0, "failed to add mapped directory: {}", status);
367 }
368 }
369
370 // Set port map
371 let c_port_map: Vec<_> = config
372 .port_map
373 .iter()
374 .map(|p| CString::new(p.to_string()).unwrap())
375 .collect();
376 let c_port_map_ptrs = utils::to_null_terminated_c_array(&c_port_map);
377
378 unsafe {
379 let status = ffi::krun_set_port_map(ctx_id, c_port_map_ptrs.as_ptr());
380 assert!(status >= 0, "failed to set port map: {}", status);
381 }
382
383 // Set network scope
384 unsafe {
385 let status =
386 ffi::krun_set_tsi_scope(ctx_id, ptr::null(), ptr::null(), config.scope as u8);
387 assert!(status >= 0, "failed to set network scope: {}", status);
388 }
389
390 // Set resource limits
391 if !config.rlimits.is_empty() {
392 let c_rlimits: Vec<_> = config
393 .rlimits
394 .iter()
395 .map(|s| CString::new(s.to_string()).unwrap())
396 .collect();
397 let c_rlimits_ptrs = utils::to_null_terminated_c_array(&c_rlimits);
398 unsafe {
399 let status = ffi::krun_set_rlimits(ctx_id, c_rlimits_ptrs.as_ptr());
400 assert!(status >= 0, "failed to set resource limits: {}", status);
401 }
402 }
403
404 // Set working directory
405 if let Some(workdir) = &config.workdir_path {
406 let c_workdir = CString::new(workdir.to_string().as_bytes()).unwrap();
407 unsafe {
408 let status = ffi::krun_set_workdir(ctx_id, c_workdir.as_ptr());
409 assert!(status >= 0, "Failed to set working directory: {}", status);
410 }
411 }
412
413 // Set executable path, arguments, and environment variables
414 let c_exec_path = CString::new(config.exec_path.to_string().as_bytes()).unwrap();
415
416 let c_argv: Vec<_> = config
417 .args
418 .iter()
419 .map(|s| CString::new(s.as_str()).unwrap())
420 .collect();
421 let c_argv_ptrs = utils::to_null_terminated_c_array(&c_argv);
422
423 let c_env: Vec<_> = config
424 .env
425 .iter()
426 .map(|s| CString::new(s.to_string()).unwrap())
427 .collect();
428 let c_env_ptrs = utils::to_null_terminated_c_array(&c_env);
429
430 unsafe {
431 let status = ffi::krun_set_exec(
432 ctx_id,
433 c_exec_path.as_ptr(),
434 c_argv_ptrs.as_ptr(),
435 c_env_ptrs.as_ptr(),
436 );
437 assert!(
438 status >= 0,
439 "Failed to set executable configuration: {}",
440 status
441 );
442 }
443
444 // Set console output
445 if let Some(console_output) = &config.console_output {
446 let c_console_output = CString::new(console_output.to_string().as_bytes()).unwrap();
447 unsafe {
448 let status = ffi::krun_set_console_output(ctx_id, c_console_output.as_ptr());
449 assert!(status >= 0, "Failed to set console output: {}", status);
450 }
451 }
452 }
453}
454
455impl MicroVmConfig {
456 /// Creates a builder for configuring a new MicroVm configuration.
457 ///
458 /// This is the recommended way to create a new MicroVmConfig instance. The builder pattern
459 /// provides a more ergonomic interface and ensures all required fields are set.
460 ///
461 /// ## Examples
462 ///
463 /// ```rust
464 /// use microsandbox_core::vm::{MicroVmConfig, Rootfs};
465 /// use tempfile::TempDir;
466 ///
467 /// # fn main() -> anyhow::Result<()> {
468 /// let temp_dir = TempDir::new()?;
469 /// let config = MicroVmConfig::builder()
470 /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
471 /// .memory_mib(1024)
472 /// .exec_path("/bin/echo")
473 /// .build();
474 /// # Ok(())
475 /// # }
476 /// ```
477 pub fn builder() -> MicroVmConfigBuilder<(), ()> {
478 MicroVmConfigBuilder::default()
479 }
480
481 /// Validates that guest paths are not subsets of each other.
482 ///
483 /// For example, these paths would conflict:
484 /// - /app and /app/data
485 /// - /var/log and /var
486 /// - /data and /data
487 ///
488 /// ## Arguments
489 /// * `mapped_dirs` - The mapped directories to validate
490 ///
491 /// ## Returns
492 /// - Ok(()) if no paths are subsets of each other
493 /// - Err with details about conflicting paths
494 fn validate_guest_paths(mapped_dirs: &[PathPair]) -> MicrosandboxResult<()> {
495 // Early return if we have 0 or 1 paths - no conflicts possible
496 if mapped_dirs.len() <= 1 {
497 return Ok(());
498 }
499
500 // Pre-normalize all paths once to avoid repeated normalization
501 let normalized_paths: Vec<_> = mapped_dirs
502 .iter()
503 .map(|dir| {
504 microsandbox_utils::normalize_path(
505 dir.get_guest().as_str(),
506 SupportedPathType::Absolute,
507 )
508 .map_err(Into::into)
509 })
510 .collect::<MicrosandboxResult<Vec<_>>>()?;
511
512 // Compare each path with every other path only once
513 // Using windows of size 2 would miss some comparisons since we need to check both directions
514 for i in 0..normalized_paths.len() {
515 let path1 = &normalized_paths[i];
516
517 // Only need to check paths after this one since previous comparisons were already done
518 for path2 in &normalized_paths[i + 1..] {
519 // Check both directions for prefix relationship
520 if utils::paths_overlap(path1, path2) {
521 return Err(MicrosandboxError::InvalidMicroVMConfig(
522 InvalidMicroVMConfigError::ConflictingGuestPaths(
523 path1.to_string(),
524 path2.to_string(),
525 ),
526 ));
527 }
528 }
529 }
530
531 Ok(())
532 }
533
534 /// Validates the MicroVm configuration.
535 ///
536 /// Performs a series of checks to ensure the configuration is valid:
537 /// - Verifies the root path exists and is accessible
538 /// - Verifies all host paths in mapped_dirs exist and are accessible
539 /// - Ensures number of vCPUs is non-zero
540 /// - Ensures memory allocation is non-zero
541 /// - Validates executable path and arguments contain only printable ASCII characters
542 /// - Validates guest paths don't overlap or conflict with each other
543 ///
544 /// ## Returns
545 /// - `Ok(())` if the configuration is valid
546 /// - `Err(MicrosandboxError::InvalidMicroVMConfig)` with details about what failed
547 ///
548 /// ## Examples
549 /// ```rust
550 /// use microsandbox_core::vm::{MicroVmConfig, Rootfs};
551 /// use tempfile::TempDir;
552 ///
553 /// # fn main() -> anyhow::Result<()> {
554 /// let temp_dir = TempDir::new()?;
555 /// let config = MicroVmConfig::builder()
556 /// .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
557 /// .memory_mib(1024)
558 /// .exec_path("/bin/echo")
559 /// .build();
560 ///
561 /// assert!(config.validate().is_ok());
562 /// # Ok(())
563 /// # }
564 /// ```
565 pub fn validate(&self) -> MicrosandboxResult<()> {
566 // Check that paths specified in rootfs exist
567 match &self.rootfs {
568 Rootfs::Native(path) => {
569 if !path.exists() {
570 return Err(MicrosandboxError::InvalidMicroVMConfig(
571 InvalidMicroVMConfigError::RootPathDoesNotExist(
572 path.to_str().unwrap().into(),
573 ),
574 ));
575 }
576 }
577 Rootfs::Overlayfs(paths) => {
578 for path in paths {
579 if !path.exists() {
580 return Err(MicrosandboxError::InvalidMicroVMConfig(
581 InvalidMicroVMConfigError::RootPathDoesNotExist(
582 path.to_str().unwrap().into(),
583 ),
584 ));
585 }
586 }
587 }
588 }
589
590 // Check all host paths in mapped_dirs exist
591 for dir in &self.mapped_dirs {
592 let host_path = PathBuf::from(dir.get_host().as_str());
593 if !host_path.exists() {
594 return Err(MicrosandboxError::InvalidMicroVMConfig(
595 InvalidMicroVMConfigError::HostPathDoesNotExist(
596 host_path.to_str().unwrap().into(),
597 ),
598 ));
599 }
600 }
601
602 if self.num_vcpus == 0 {
603 return Err(MicrosandboxError::InvalidMicroVMConfig(
604 InvalidMicroVMConfigError::NumVCPUsIsZero,
605 ));
606 }
607
608 // Validate memory_mib is not zero
609 if self.memory_mib == 0 {
610 return Err(MicrosandboxError::InvalidMicroVMConfig(
611 InvalidMicroVMConfigError::MemoryIsZero,
612 ));
613 }
614
615 Self::validate_command_line(self.exec_path.as_ref())?;
616
617 for arg in &self.args {
618 Self::validate_command_line(arg)?;
619 }
620
621 // Validate guest paths are not subsets of each other
622 Self::validate_guest_paths(&self.mapped_dirs)?;
623
624 Ok(())
625 }
626
627 /// Validates that a command line string contains only allowed characters.
628 ///
629 /// Command line strings (executable paths and arguments) must contain only printable ASCII
630 /// characters in the range from space (0x20) to tilde (0x7E). This excludes:
631 /// - Control characters (newlines, tabs, etc.)
632 /// - Non-ASCII Unicode characters
633 /// - Null bytes
634 ///
635 /// ## Arguments
636 /// * `s` - The string to validate
637 ///
638 /// ## Returns
639 /// - `Ok(())` if the string contains only valid characters
640 /// - `Err(MicrosandboxError::InvalidMicroVMConfig)` if invalid characters are found
641 ///
642 /// ## Examples
643 /// ```rust
644 /// use microsandbox_core::vm::MicroVmConfig;
645 ///
646 /// // Valid strings
647 /// assert!(MicroVmConfig::validate_command_line("/bin/echo").is_ok());
648 /// assert!(MicroVmConfig::validate_command_line("Hello, World!").is_ok());
649 ///
650 /// // Invalid strings
651 /// assert!(MicroVmConfig::validate_command_line("/bin/echo\n").is_err()); // newline
652 /// assert!(MicroVmConfig::validate_command_line("hello🌎").is_err()); // emoji
653 /// ```
654 pub fn validate_command_line(s: &str) -> MicrosandboxResult<()> {
655 fn valid_char(c: char) -> bool {
656 matches!(c, ' '..='~')
657 }
658
659 if s.chars().all(valid_char) {
660 Ok(())
661 } else {
662 Err(MicrosandboxError::InvalidMicroVMConfig(
663 InvalidMicroVMConfigError::InvalidCommandLineString(s.to_string()),
664 ))
665 }
666 }
667}
668
669//--------------------------------------------------------------------------------------------------
670// Trait Implementations
671//--------------------------------------------------------------------------------------------------
672
673impl Drop for MicroVm {
674 fn drop(&mut self) {
675 unsafe { ffi::krun_free_ctx(self.ctx_id) };
676 }
677}
678
679impl TryFrom<u8> for LogLevel {
680 type Error = MicrosandboxError;
681
682 fn try_from(value: u8) -> Result<Self, MicrosandboxError> {
683 match value {
684 0 => Ok(LogLevel::Off),
685 1 => Ok(LogLevel::Error),
686 2 => Ok(LogLevel::Warn),
687 3 => Ok(LogLevel::Info),
688 4 => Ok(LogLevel::Debug),
689 5 => Ok(LogLevel::Trace),
690 _ => Err(MicrosandboxError::InvalidLogLevel(value)),
691 }
692 }
693}
694
695//--------------------------------------------------------------------------------------------------
696// Tests
697//--------------------------------------------------------------------------------------------------
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702 use microsandbox_utils::DEFAULT_NUM_VCPUS;
703 use std::path::PathBuf;
704 use tempfile::TempDir;
705
706 #[test]
707 fn test_microvm_config_builder() {
708 let config = MicroVmConfig::builder()
709 .log_level(LogLevel::Info)
710 .rootfs(Rootfs::Native(PathBuf::from("/tmp")))
711 .memory_mib(512)
712 .exec_path("/bin/echo")
713 .build();
714
715 assert!(config.log_level == LogLevel::Info);
716 assert_eq!(config.rootfs, Rootfs::Native(PathBuf::from("/tmp")));
717 assert_eq!(config.memory_mib, 512);
718 assert_eq!(config.num_vcpus, DEFAULT_NUM_VCPUS);
719 }
720
721 #[test]
722 fn test_microvm_config_validation_success() {
723 let temp_dir = TempDir::new().unwrap();
724 let config = MicroVmConfig::builder()
725 .log_level(LogLevel::Info)
726 .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
727 .exec_path("/bin/echo")
728 .build();
729
730 assert!(config.validate().is_ok());
731 }
732
733 #[test]
734 fn test_microvm_config_validation_failure_root_path() {
735 let config = MicroVmConfig::builder()
736 .log_level(LogLevel::Info)
737 .rootfs(Rootfs::Native(PathBuf::from("/non/existent/path")))
738 .memory_mib(512)
739 .exec_path("/bin/echo")
740 .build();
741
742 assert!(matches!(
743 config.validate(),
744 Err(MicrosandboxError::InvalidMicroVMConfig(
745 InvalidMicroVMConfigError::RootPathDoesNotExist(_)
746 ))
747 ));
748 }
749
750 #[test]
751 fn test_microvm_config_validation_failure_zero_ram() {
752 let temp_dir = TempDir::new().unwrap();
753 let config = MicroVmConfig::builder()
754 .log_level(LogLevel::Info)
755 .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
756 .memory_mib(0)
757 .exec_path("/bin/echo")
758 .build();
759
760 assert!(matches!(
761 config.validate(),
762 Err(MicrosandboxError::InvalidMicroVMConfig(
763 InvalidMicroVMConfigError::MemoryIsZero
764 ))
765 ));
766 }
767
768 #[test]
769 fn test_validate_command_line_valid_strings() {
770 // Test basic ASCII strings
771 assert!(MicroVmConfig::validate_command_line("hello").is_ok());
772 assert!(MicroVmConfig::validate_command_line("hello world").is_ok());
773 assert!(MicroVmConfig::validate_command_line("Hello, World!").is_ok());
774
775 // Test edge cases of valid range (space to tilde)
776 assert!(MicroVmConfig::validate_command_line(" ").is_ok()); // space (0x20)
777 assert!(MicroVmConfig::validate_command_line("~").is_ok()); // tilde (0x7E)
778
779 // Test special characters within valid range
780 assert!(MicroVmConfig::validate_command_line("!@#$%^&*()").is_ok());
781 assert!(MicroVmConfig::validate_command_line("path/to/file").is_ok());
782 assert!(MicroVmConfig::validate_command_line("user-name_123").is_ok());
783 }
784
785 #[test]
786 fn test_validate_command_line_invalid_strings() {
787 // Test control characters
788 assert!(MicroVmConfig::validate_command_line("\n").is_err()); // newline
789 assert!(MicroVmConfig::validate_command_line("\t").is_err()); // tab
790 assert!(MicroVmConfig::validate_command_line("\r").is_err()); // carriage return
791 assert!(MicroVmConfig::validate_command_line("\x1B").is_err()); // escape
792
793 // Test non-ASCII Unicode characters
794 assert!(MicroVmConfig::validate_command_line("hello🌎").is_err()); // emoji
795 assert!(MicroVmConfig::validate_command_line("über").is_err()); // umlaut
796 assert!(MicroVmConfig::validate_command_line("café").is_err()); // accent
797 assert!(MicroVmConfig::validate_command_line("ä½ å¥½").is_err()); // Chinese characters
798
799 // Test strings mixing valid and invalid characters
800 assert!(MicroVmConfig::validate_command_line("hello\nworld").is_err());
801 assert!(MicroVmConfig::validate_command_line("path/to/file\0").is_err()); // null byte
802 assert!(MicroVmConfig::validate_command_line("hello\x7F").is_err()); // DEL character
803 }
804
805 #[test]
806 fn test_validate_command_line_in_config() {
807 let temp_dir = TempDir::new().unwrap();
808
809 // Test invalid executable path
810 let config = MicroVmConfig::builder()
811 .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
812 .memory_mib(512)
813 .exec_path("/bin/hello\nworld")
814 .build();
815 assert!(matches!(
816 config.validate(),
817 Err(MicrosandboxError::InvalidMicroVMConfig(
818 InvalidMicroVMConfigError::InvalidCommandLineString(_)
819 ))
820 ));
821
822 // Test invalid argument
823 let config = MicroVmConfig::builder()
824 .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
825 .memory_mib(512)
826 .exec_path("/bin/echo")
827 .args(["hello\tworld"])
828 .build();
829 assert!(matches!(
830 config.validate(),
831 Err(MicrosandboxError::InvalidMicroVMConfig(
832 InvalidMicroVMConfigError::InvalidCommandLineString(_)
833 ))
834 ));
835 }
836
837 #[test]
838 fn test_validate_guest_paths() -> anyhow::Result<()> {
839 // Test valid paths (no conflicts)
840 let valid_paths = vec![
841 "/app".parse::<PathPair>()?,
842 "/data".parse()?,
843 "/var/log".parse()?,
844 "/etc/config".parse()?,
845 ];
846 assert!(MicroVmConfig::validate_guest_paths(&valid_paths).is_ok());
847
848 // Test conflicting paths (direct match)
849 let conflicting_paths = vec![
850 "/app".parse()?,
851 "/data".parse()?,
852 "/app".parse()?, // Duplicate
853 ];
854 assert!(MicroVmConfig::validate_guest_paths(&conflicting_paths).is_err());
855
856 // Test conflicting paths (subset)
857 let subset_paths = vec![
858 "/app".parse()?,
859 "/app/data".parse()?, // Subset of /app
860 "/var/log".parse()?,
861 ];
862 assert!(MicroVmConfig::validate_guest_paths(&subset_paths).is_err());
863
864 // Test conflicting paths (parent)
865 let parent_paths = vec![
866 "/var/log".parse()?,
867 "/var".parse()?, // Parent of /var/log
868 "/etc".parse()?,
869 ];
870 assert!(MicroVmConfig::validate_guest_paths(&parent_paths).is_err());
871
872 // Test paths needing normalization
873 let unnormalized_paths = vec![
874 "/app/./data".parse()?,
875 "/var/log".parse()?,
876 "/etc//config".parse()?,
877 ];
878 assert!(MicroVmConfig::validate_guest_paths(&unnormalized_paths).is_ok());
879
880 // Test paths with normalization conflicts
881 let normalized_conflicts = vec![
882 "/app/./data".parse()?,
883 "/app/data/".parse()?, // Same as first path after normalization
884 "/var/log".parse()?,
885 ];
886 assert!(MicroVmConfig::validate_guest_paths(&normalized_conflicts).is_err());
887
888 Ok(())
889 }
890
891 #[test]
892 fn test_microvm_config_validation_with_guest_paths() -> anyhow::Result<()> {
893 use tempfile::TempDir;
894
895 let temp_dir = TempDir::new()?;
896 let host_dir1 = temp_dir.path().join("dir1");
897 let host_dir2 = temp_dir.path().join("dir2");
898 std::fs::create_dir_all(&host_dir1)?;
899 std::fs::create_dir_all(&host_dir2)?;
900
901 // Test valid configuration
902 let valid_config = MicroVmConfig::builder()
903 .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
904 .memory_mib(1024)
905 .exec_path("/bin/echo")
906 .mapped_dirs([
907 format!("{}:/app", host_dir1.display()).parse()?,
908 format!("{}:/data", host_dir2.display()).parse()?,
909 ])
910 .build();
911
912 assert!(valid_config.validate().is_ok());
913
914 // Test configuration with conflicting guest paths
915 let invalid_config = MicroVmConfig::builder()
916 .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
917 .memory_mib(1024)
918 .exec_path("/bin/echo")
919 .mapped_dirs([
920 format!("{}:/app/data", host_dir1.display()).parse()?,
921 format!("{}:/app", host_dir2.display()).parse()?,
922 ])
923 .build();
924
925 assert!(matches!(
926 invalid_config.validate(),
927 Err(MicrosandboxError::InvalidMicroVMConfig(
928 InvalidMicroVMConfigError::ConflictingGuestPaths(_, _)
929 ))
930 ));
931
932 Ok(())
933 }
934}