Skip to main content

mimobox_vm/
vm.rs

1use std::collections::HashMap;
2use std::env;
3use std::path::{Path, PathBuf};
4use std::sync::mpsc;
5use std::time::{Duration, Instant};
6
7use mimobox_core::{
8    DirEntry, FileStat, Sandbox, SandboxConfig, SandboxError, SandboxResult, SandboxSnapshot,
9};
10use tracing::debug;
11
12use crate::http_proxy::{HttpProxyError, HttpRequest, HttpResponse};
13use crate::snapshot::MicrovmSnapshot;
14#[cfg(all(target_os = "linux", feature = "kvm"))]
15use crate::snapshot::load_state_from_memory_file;
16
17#[cfg(all(target_os = "linux", feature = "kvm", not(feature = "zerocopy-fork")))]
18use crate::snapshot::{
19    FILE_SNAPSHOT_VERSION, SnapshotStateFile, create_snapshot_dir, memory_sha256_hex,
20};
21use crate::vm_assets::resolve_vm_assets_dir;
22
23#[cfg(all(target_os = "linux", feature = "kvm"))]
24use crate::kvm::{KvmBackend, restore_runtime_state};
25
26/// Configuration for a single microVM instance.
27///
28/// The configuration describes the guest CPU and memory shape plus the host-side
29/// assets required to boot the guest kernel and root filesystem. It is validated
30/// before any backend is created.
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct MicrovmConfig {
33    /// Number of virtual CPUs exposed to the guest.
34    pub vcpu_count: u8,
35    /// Guest memory size in mebibytes.
36    pub memory_mb: u32,
37    /// Optional CPU time quota in microseconds for backends that support CPU throttling.
38    #[serde(default)]
39    pub cpu_quota_us: Option<u64>,
40    /// Path to the guest kernel image on the host.
41    pub kernel_path: PathBuf,
42    /// Path to the gzip-compressed guest rootfs image on the host.
43    pub rootfs_path: PathBuf,
44}
45
46impl Default for MicrovmConfig {
47    fn default() -> Self {
48        let assets_dir = resolve_vm_assets_dir(
49            env::var_os("VM_ASSETS_DIR").map(PathBuf::from),
50            env::var_os("HOME").map(PathBuf::from),
51        )
52        .unwrap_or_else(|_| PathBuf::from("/var/lib/mimobox/vm"));
53
54        Self {
55            vcpu_count: 1,
56            memory_mb: 256,
57            cpu_quota_us: None,
58            kernel_path: assets_dir.join("vmlinux"),
59            rootfs_path: assets_dir.join("rootfs.cpio.gz"),
60        }
61    }
62}
63
64impl MicrovmConfig {
65    /// Returns the configured guest memory size in bytes.
66    ///
67    /// The conversion checks for arithmetic overflow and for platforms where the
68    /// requested memory size cannot fit into `usize`.
69    pub fn memory_bytes(&self) -> Result<usize, MicrovmError> {
70        let bytes = u64::from(self.memory_mb)
71            .checked_mul(1024 * 1024)
72            .ok_or_else(|| {
73                MicrovmError::InvalidConfig("memory_mb overflow when converting to bytes".into())
74            })?;
75        usize::try_from(bytes).map_err(|_| {
76            MicrovmError::InvalidConfig(
77                "required memory size exceeds platform address space".into(),
78            )
79        })
80    }
81
82    /// Validates the base microVM configuration before backend creation.
83    ///
84    /// Validation rejects zero vCPU count, memory below the minimum supported guest
85    /// size, empty asset paths, and missing kernel or rootfs files.
86    pub fn validate(&self) -> Result<(), MicrovmError> {
87        if self.vcpu_count == 0 {
88            return Err(MicrovmError::InvalidConfig(
89                "vcpu_count must not be 0".into(),
90            ));
91        }
92
93        if self.memory_mb < 64 {
94            return Err(MicrovmError::InvalidConfig(
95                "memory_mb must not be less than 64".into(),
96            ));
97        }
98
99        if self.kernel_path.as_os_str().is_empty() {
100            return Err(MicrovmError::InvalidConfig(
101                "kernel_path must not be empty".into(),
102            ));
103        }
104
105        if self.rootfs_path.as_os_str().is_empty() {
106            return Err(MicrovmError::InvalidConfig(
107                "rootfs_path must not be empty".into(),
108            ));
109        }
110
111        if !self.kernel_path.exists() {
112            return Err(MicrovmError::InvalidConfig(format!(
113                "kernel_path does not exist: {}",
114                self.kernel_path.display()
115            )));
116        }
117
118        if !self.rootfs_path.exists() {
119            return Err(MicrovmError::InvalidConfig(format!(
120                "rootfs_path does not exist: {}",
121                self.rootfs_path.display()
122            )));
123        }
124
125        Ok(())
126    }
127}
128
129/// Lifecycle state of a [`MicrovmSandbox`].
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum MicrovmState {
132    /// Instance has been created but is not yet executable.
133    Created,
134    /// Instance is ready to execute commands or file operations.
135    Ready,
136    /// Instance is currently executing a command or transferring data.
137    Running,
138    /// Instance has been destroyed and cannot be reused.
139    Destroyed,
140}
141
142/// Result of a guest command execution.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct GuestCommandResult {
145    /// Bytes written by the guest process to standard output.
146    pub stdout: Vec<u8>,
147    /// Bytes written by the guest process to standard error.
148    pub stderr: Vec<u8>,
149    /// Exit code reported by the guest process, or `None` when no normal exit code is available.
150    pub exit_code: Option<i32>,
151    /// Whether execution was terminated because the effective timeout expired.
152    pub timed_out: bool,
153}
154
155/// Per-command execution options passed to the guest command protocol.
156#[derive(Debug, Clone, Default, PartialEq, Eq)]
157pub struct GuestExecOptions {
158    /// Environment variables that apply only to this command.
159    pub env: HashMap<String, String>,
160    /// Timeout override that applies only to this command.
161    pub timeout: Option<Duration>,
162    /// Working directory override that applies only to this command.
163    pub cwd: Option<String>,
164}
165
166/// Event emitted by streaming guest command execution.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum StreamEvent {
169    /// A chunk of standard output data.
170    Stdout(Vec<u8>),
171    /// A chunk of standard error data.
172    Stderr(Vec<u8>),
173    /// Process exited with an exit code.
174    Exit(i32),
175    /// Execution was terminated by a timeout.
176    TimedOut,
177}
178
179/// Structured lifecycle error for microVM and pooled VM handles.
180///
181/// This error separates state-machine failures from backend failures so callers can
182/// distinguish invalid handle usage from guest or KVM execution problems.
183#[derive(Debug, Clone, thiserror::Error)]
184pub enum LifecycleError {
185    /// The current lifecycle state does not permit the requested operation.
186    #[error("{message}")]
187    InvalidState {
188        /// Lifecycle state required by the operation.
189        expected: String,
190        /// Description of the current lifecycle state.
191        current: String,
192        /// Stable user-facing error message.
193        message: String,
194    },
195    /// The sandbox has been destroyed and cannot be reused.
196    #[error("{0}")]
197    Destroyed(
198        /// Destroyed-state failure message.
199        String,
200    ),
201    /// The VM handle was released back to its pool and cannot be used again.
202    #[error("{0}")]
203    Released(
204        /// Released-handle failure message.
205        String,
206    ),
207    /// Snapshotting was requested while the VM was not ready.
208    #[error("snapshot only allowed in Ready state")]
209    NotReady,
210    /// Forking was requested while the VM was not ready.
211    #[error("fork only allowed in Ready state")]
212    NotReadyForFork,
213    /// The vsock command channel is required but not connected.
214    #[error("vsock command channel is not connected")]
215    VsockNotConnected,
216    /// The vsock command channel is not available for the requested operation.
217    #[error("vsock command channel unavailable")]
218    VsockUnavailable,
219    /// Any lifecycle error that does not fit a more specific category.
220    #[error("{0}")]
221    Other(
222        /// Additional lifecycle failure detail.
223        String,
224    ),
225}
226
227/// Guest-side file operation error categories.
228#[derive(Debug, Clone, thiserror::Error)]
229pub enum GuestFileErrorKind {
230    /// The guest path does not exist or cannot be resolved.
231    #[error("path not found")]
232    NotFound,
233    /// The guest reported a generic I/O failure.
234    #[error("I/O error")]
235    Io,
236    /// The guest denied the requested file operation.
237    #[error("permission denied")]
238    PermissionDenied,
239    /// The guest filesystem did not have enough free space.
240    #[error("out of space")]
241    OutOfSpace,
242    /// The guest returned an unknown file-operation status code.
243    #[error("unknown status code {0}")]
244    Unknown(
245        /// Raw status code returned by the guest.
246        u8,
247    ),
248}
249
250/// Top-level error type returned by the microVM crate.
251///
252/// Errors are intentionally grouped by configuration, lifecycle, backend, guest
253/// file protocol, HTTP proxy, snapshot, and host I/O boundaries.
254#[derive(Debug, thiserror::Error)]
255pub enum MicrovmError {
256    /// KVM microVMs are not supported on the current platform or build configuration.
257    #[error("KVM microVM backend not supported on current platform")]
258    UnsupportedPlatform,
259
260    /// microVM configuration is invalid.
261    #[error("invalid microVM config: {0}")]
262    InvalidConfig(
263        /// Configuration validation failure detail.
264        String,
265    ),
266
267    /// Current lifecycle state does not allow the requested operation.
268    #[error("microVM lifecycle error: {0}")]
269    Lifecycle(
270        /// Source lifecycle-state error.
271        LifecycleError,
272    ),
273
274    /// KVM or guest protocol error.
275    #[error("KVM backend error: {0}")]
276    Backend(
277        /// Backend failure detail.
278        String,
279    ),
280
281    /// Guest-side file operation error.
282    #[error("guest file error: {path}: {kind}")]
283    GuestFile {
284        /// Semantic category reported by the guest file protocol.
285        kind: GuestFileErrorKind,
286        /// Guest path associated with the failed operation.
287        path: String,
288    },
289
290    /// Controlled HTTP proxy error.
291    #[error(transparent)]
292    HttpProxy(
293        /// Source HTTP proxy error.
294        #[from]
295        HttpProxyError,
296    ),
297
298    /// Snapshot format is invalid or incompatible.
299    #[error("invalid snapshot format: {0}")]
300    SnapshotFormat(
301        /// Snapshot decoding or compatibility failure detail.
302        String,
303    ),
304
305    /// Standard library I/O error.
306    #[error("I/O error: {0}")]
307    Io(
308        /// Source host I/O error.
309        #[from]
310        std::io::Error,
311    ),
312}
313
314impl From<MicrovmError> for SandboxError {
315    fn from(value: MicrovmError) -> Self {
316        match value {
317            MicrovmError::UnsupportedPlatform => SandboxError::Unsupported,
318            MicrovmError::InvalidConfig(message)
319            | MicrovmError::Backend(message)
320            | MicrovmError::HttpProxy(crate::http_proxy::HttpProxyError::Internal(message))
321            | MicrovmError::SnapshotFormat(message) => SandboxError::ExecutionFailed(message),
322            MicrovmError::Lifecycle(error) => SandboxError::ExecutionFailed(error.to_string()),
323            error @ MicrovmError::GuestFile { .. } => {
324                SandboxError::ExecutionFailed(error.to_string())
325            }
326            MicrovmError::HttpProxy(error) => SandboxError::ExecutionFailed(error.to_string()),
327            MicrovmError::Io(error) => SandboxError::Io(error),
328        }
329    }
330}
331
332#[allow(dead_code)]
333enum BackendHandle {
334    #[cfg(all(target_os = "linux", feature = "kvm"))]
335    Kvm(Box<KvmBackend>),
336    Unsupported,
337}
338
339impl BackendHandle {
340    fn create(base_config: SandboxConfig, config: MicrovmConfig) -> Result<Self, MicrovmError> {
341        #[cfg(all(target_os = "linux", feature = "kvm"))]
342        {
343            return Ok(Self::Kvm(Box::new(KvmBackend::create_vm(
344                base_config,
345                config,
346            )?)));
347        }
348
349        #[allow(unreachable_code)]
350        {
351            let _ = base_config;
352            let _ = config;
353            Err(MicrovmError::UnsupportedPlatform)
354        }
355    }
356
357    fn create_for_restore(
358        base_config: SandboxConfig,
359        config: MicrovmConfig,
360    ) -> Result<Self, MicrovmError> {
361        #[cfg(all(target_os = "linux", feature = "kvm"))]
362        {
363            return Ok(Self::Kvm(Box::new(KvmBackend::create_vm_for_restore(
364                base_config,
365                config,
366            )?)));
367        }
368
369        #[allow(unreachable_code)]
370        {
371            let _ = base_config;
372            let _ = config;
373            Err(MicrovmError::UnsupportedPlatform)
374        }
375    }
376
377    #[allow(dead_code)]
378    fn run_command(&mut self, _cmd: &[String]) -> Result<GuestCommandResult, MicrovmError> {
379        self.run_command_with_options(_cmd, &GuestExecOptions::default())
380    }
381
382    fn run_command_with_options(
383        &mut self,
384        _cmd: &[String],
385        _options: &GuestExecOptions,
386    ) -> Result<GuestCommandResult, MicrovmError> {
387        match self {
388            #[cfg(all(target_os = "linux", feature = "kvm"))]
389            Self::Kvm(backend) => backend.run_command_with_options(_cmd, _options),
390            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
391        }
392    }
393
394    #[allow(dead_code)]
395    fn run_command_streaming(
396        &mut self,
397        _cmd: &[String],
398    ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
399        self.run_command_streaming_with_options(_cmd, &GuestExecOptions::default())
400    }
401
402    fn run_command_streaming_with_options(
403        &mut self,
404        _cmd: &[String],
405        _options: &GuestExecOptions,
406    ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
407        match self {
408            #[cfg(all(target_os = "linux", feature = "kvm"))]
409            Self::Kvm(backend) => backend.run_command_streaming_with_options(_cmd, _options),
410            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
411        }
412    }
413
414    fn read_file(&mut self, _path: &str) -> Result<Vec<u8>, MicrovmError> {
415        match self {
416            #[cfg(all(target_os = "linux", feature = "kvm"))]
417            Self::Kvm(backend) => backend.read_file(_path),
418            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
419        }
420    }
421
422    fn write_file(&mut self, _path: &str, _data: &[u8]) -> Result<(), MicrovmError> {
423        match self {
424            #[cfg(all(target_os = "linux", feature = "kvm"))]
425            Self::Kvm(backend) => backend.write_file(_path, _data),
426            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
427        }
428    }
429
430    fn ping(&mut self) -> Result<Duration, MicrovmError> {
431        match self {
432            #[cfg(all(target_os = "linux", feature = "kvm"))]
433            Self::Kvm(backend) => backend.ping(),
434            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
435        }
436    }
437
438    fn ping_with_timeout(&mut self, _timeout: Duration) -> Result<Duration, MicrovmError> {
439        match self {
440            #[cfg(all(target_os = "linux", feature = "kvm"))]
441            Self::Kvm(backend) => backend.ping_with_timeout(_timeout),
442            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
443        }
444    }
445
446    fn http_request(&mut self, _request: HttpRequest) -> Result<HttpResponse, MicrovmError> {
447        match self {
448            #[cfg(all(target_os = "linux", feature = "kvm"))]
449            Self::Kvm(backend) => backend.http_request(_request),
450            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
451        }
452    }
453
454    fn shutdown(&mut self) -> Result<(), MicrovmError> {
455        match self {
456            #[cfg(all(target_os = "linux", feature = "kvm"))]
457            Self::Kvm(backend) => backend.shutdown(),
458            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
459        }
460    }
461
462    fn is_destroyed(&self) -> bool {
463        match self {
464            #[cfg(all(target_os = "linux", feature = "kvm"))]
465            Self::Kvm(backend) => backend.lifecycle() == crate::kvm::KvmLifecycle::Destroyed,
466            Self::Unsupported => true,
467        }
468    }
469
470    fn is_ready(&self) -> bool {
471        match self {
472            #[cfg(all(target_os = "linux", feature = "kvm"))]
473            Self::Kvm(backend) => backend.is_guest_ready(),
474            Self::Unsupported => false,
475        }
476    }
477
478    fn snapshot_parts(&self) -> Result<(Vec<u8>, Vec<u8>), MicrovmError> {
479        match self {
480            #[cfg(all(target_os = "linux", feature = "kvm"))]
481            Self::Kvm(backend) => backend.snapshot_state(),
482            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
483        }
484    }
485
486    fn restore_parts(&mut self, _memory: &[u8], _vcpu_state: &[u8]) -> Result<(), MicrovmError> {
487        match self {
488            #[cfg(all(target_os = "linux", feature = "kvm"))]
489            Self::Kvm(backend) => backend.restore_state(_memory, _vcpu_state),
490            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
491        }
492    }
493
494    #[cfg(all(target_os = "linux", feature = "kvm"))]
495    fn restore_from_file_parts(
496        &mut self,
497        memory_path: &Path,
498        vcpu_state: &[u8],
499    ) -> Result<(), MicrovmError> {
500        match self {
501            #[cfg(all(target_os = "linux", feature = "kvm"))]
502            Self::Kvm(backend) => {
503                let mut restore_profile = backend.take_or_seed_restore_profile();
504
505                let restore_memory_started_at = Instant::now();
506                #[cfg(feature = "zerocopy-fork")]
507                backend.restore_from_file_zerocopy(memory_path)?;
508                #[cfg(not(feature = "zerocopy-fork"))]
509                backend.restore_from_file(memory_path)?;
510                restore_profile.memory_state_write = restore_memory_started_at.elapsed();
511
512                restore_profile.cpuid_config = backend.prepare_restored_vcpus()?;
513
514                let runtime_restore_profile = restore_runtime_state(backend, vcpu_state)?;
515                restore_profile.vcpu_state_restore = runtime_restore_profile.vcpu_state_restore;
516                restore_profile.device_state_restore = runtime_restore_profile.device_state_restore;
517
518                backend.set_lifecycle_ready();
519                backend.emit_restore_profile_without_resume(&restore_profile);
520                backend.set_pending_restore_profile(restore_profile);
521                Ok(())
522            }
523            Self::Unsupported => Err(MicrovmError::UnsupportedPlatform),
524        }
525    }
526}
527
528/// Public microVM sandbox implementation.
529///
530/// A `MicrovmSandbox` owns one backend instance and exposes guest command
531/// execution, file transfer, readiness probing, HTTP proxying, snapshotting, and
532/// fork/restore operations through a lifecycle-checked API.
533pub struct MicrovmSandbox {
534    base_config: SandboxConfig,
535    microvm_config: MicrovmConfig,
536    state: MicrovmState,
537    backend: BackendHandle,
538}
539
540impl std::fmt::Debug for MicrovmSandbox {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        f.debug_struct("MicrovmSandbox")
543            .field("microvm_config", &self.microvm_config)
544            .field("state", &self.state)
545            .finish_non_exhaustive()
546    }
547}
548
549impl MicrovmSandbox {
550    /// Creates a microVM with the default [`SandboxConfig`].
551    ///
552    /// This is a convenience wrapper around [`Self::new_with_base`].
553    pub fn new(config: MicrovmConfig) -> Result<Self, MicrovmError> {
554        Self::new_with_base(SandboxConfig::default(), config)
555    }
556
557    /// Creates a microVM with explicit sandbox and microVM configuration.
558    ///
559    /// The method validates the microVM asset paths and returns
560    /// [`MicrovmError::UnsupportedPlatform`] when the current build does not include
561    /// a supported KVM backend.
562    pub fn new_with_base(
563        base_config: SandboxConfig,
564        microvm_config: MicrovmConfig,
565    ) -> Result<Self, MicrovmError> {
566        if !cfg!(all(target_os = "linux", feature = "kvm")) {
567            return Err(MicrovmError::UnsupportedPlatform);
568        }
569
570        microvm_config.validate()?;
571        debug!(
572            vcpu_count = microvm_config.vcpu_count,
573            memory_mb = microvm_config.memory_mb,
574            "初始化 microVM 沙箱"
575        );
576        let backend = BackendHandle::create(base_config.clone(), microvm_config.clone())?;
577
578        Ok(Self {
579            base_config,
580            microvm_config,
581            state: MicrovmState::Ready,
582            backend,
583        })
584    }
585
586    /// Exports a file-backed snapshot of the current microVM.
587    ///
588    /// Snapshotting is only allowed while the sandbox is in [`MicrovmState::Ready`].
589    /// The returned [`SandboxSnapshot`] stores guest memory in a snapshot directory
590    /// and associated runtime state in sidecar metadata.
591    pub fn snapshot(&mut self) -> Result<SandboxSnapshot, MicrovmError> {
592        if self.state != MicrovmState::Ready {
593            return Err(MicrovmError::Lifecycle(LifecycleError::NotReady));
594        }
595
596        let (memory, vcpu_state) = self.backend.snapshot_parts()?;
597        MicrovmSnapshot::new(
598            self.base_config.clone(),
599            self.microvm_config.clone(),
600            memory,
601            vcpu_state,
602        )
603        .persist_to_files()
604    }
605
606    /// Restores a microVM from a [`SandboxSnapshot`].
607    ///
608    /// File-backed snapshots use the optimized file restore path when available.
609    /// Inline snapshots are decoded from their self-describing byte representation.
610    pub fn restore(snapshot: &SandboxSnapshot) -> Result<Self, MicrovmError> {
611        let _span = tracing::info_span!("vm_restore").entered();
612        if let Some(memory_path) = snapshot.memory_file_path() {
613            return Self::restore_from_file_snapshot(memory_path);
614        }
615
616        let data = snapshot.as_bytes().map_err(map_snapshot_access_error)?;
617        Self::restore_from_bytes(data)
618    }
619
620    /// Restores a microVM from self-describing snapshot bytes.
621    ///
622    /// The byte format is produced by [`MicrovmSnapshot::snapshot`].
623    pub fn restore_from_bytes(data: &[u8]) -> Result<Self, MicrovmError> {
624        let snapshot = MicrovmSnapshot::restore(data)?;
625        Self::from_snapshot(snapshot)
626    }
627
628    /// Restores from a file-backed snapshot, loading memory through mmap(MAP_PRIVATE).
629    #[cfg(all(target_os = "linux", feature = "kvm"))]
630    fn restore_from_file_snapshot(memory_path: &Path) -> Result<Self, MicrovmError> {
631        let (sandbox_config, microvm_config, vcpu_state) =
632            load_state_from_memory_file(memory_path)?;
633
634        let mut backend =
635            BackendHandle::create_for_restore(sandbox_config.clone(), microvm_config.clone())?;
636        backend.restore_from_file_parts(memory_path, &vcpu_state)?;
637
638        Ok(Self {
639            base_config: sandbox_config,
640            microvm_config,
641            state: MicrovmState::Ready,
642            backend,
643        })
644    }
645
646    /// File snapshot restore fallback for non-Linux platforms.
647    #[cfg(not(all(target_os = "linux", feature = "kvm")))]
648    fn restore_from_file_snapshot(_memory_path: &Path) -> Result<Self, MicrovmError> {
649        Err(MicrovmError::Backend(
650            "file snapshot restore only supported on Linux".into(),
651        ))
652    }
653
654    /// Creates an independent copy from the current microVM.
655    ///
656    /// When the zerocopy-fork feature is enabled, this directly shares guest memory
657    /// and relies on MAP_PRIVATE CoW. Otherwise it keeps the file snapshot restore
658    /// path as a fallback.
659    #[cfg(all(target_os = "linux", feature = "kvm"))]
660    #[cfg_attr(docsrs, doc(cfg(feature = "kvm")))]
661    pub fn fork(&mut self) -> Result<Self, MicrovmError> {
662        let _span = tracing::info_span!("vm_fork").entered();
663        if self.state != MicrovmState::Ready {
664            return Err(MicrovmError::Lifecycle(LifecycleError::NotReadyForFork));
665        }
666
667        #[cfg(feature = "zerocopy-fork")]
668        {
669            let (shared_memory, vcpu_state) = match &self.backend {
670                BackendHandle::Kvm(backend) => backend.snapshot_for_fork()?,
671                BackendHandle::Unsupported => return Err(MicrovmError::UnsupportedPlatform),
672            };
673
674            let mut backend_handle = BackendHandle::create_for_restore(
675                self.base_config.clone(),
676                self.microvm_config.clone(),
677            )?;
678
679            match &mut backend_handle {
680                BackendHandle::Kvm(backend) => {
681                    backend.restore_from_shared_memory(shared_memory, &vcpu_state)?;
682                }
683                BackendHandle::Unsupported => return Err(MicrovmError::UnsupportedPlatform),
684            }
685
686            return Ok(Self {
687                base_config: self.base_config.clone(),
688                microvm_config: self.microvm_config.clone(),
689                state: MicrovmState::Ready,
690                backend: backend_handle,
691            });
692        }
693
694        #[cfg(not(feature = "zerocopy-fork"))]
695        {
696            use base64::Engine as _;
697
698            let (memory, vcpu_state) = self.backend.snapshot_parts()?;
699            let snapshot_dir = create_snapshot_dir()?;
700            let memory_path = snapshot_dir.join("memory.bin");
701            let state_path = snapshot_dir.join("state.json");
702
703            let fork_result = (|| {
704                std::fs::write(&memory_path, &memory)?;
705
706                let state = SnapshotStateFile {
707                    version: FILE_SNAPSHOT_VERSION,
708                    sandbox_config: self.base_config.clone(),
709                    microvm_config: self.microvm_config.clone(),
710                    vcpu_state_base64: base64::engine::general_purpose::STANDARD
711                        .encode(&vcpu_state),
712                    memory_hash: Some(memory_sha256_hex(&memory)),
713                };
714                let state_bytes = serde_json::to_vec_pretty(&state).map_err(|error| {
715                    MicrovmError::SnapshotFormat(format!("failed to serialize state.json: {error}"))
716                })?;
717                std::fs::write(&state_path, state_bytes)?;
718
719                Self::restore_from_file_snapshot(&memory_path)
720            })();
721
722            let _ = std::fs::remove_dir_all(snapshot_dir);
723            fork_result
724        }
725    }
726
727    /// Attempts to fork the microVM on unsupported builds.
728    ///
729    /// This method preserves the public API on non-KVM builds and always returns an
730    /// unsupported-backend error.
731    #[cfg(not(all(target_os = "linux", feature = "kvm")))]
732    pub fn fork(&mut self) -> Result<Self, MicrovmError> {
733        let _span = tracing::info_span!("vm_fork").entered();
734        Err(MicrovmError::Backend(
735            "fork only supported on Linux + KVM".into(),
736        ))
737    }
738
739    /// Reconstructs a sandbox from a decoded [`MicrovmSnapshot`].
740    pub(crate) fn from_snapshot(snapshot: MicrovmSnapshot) -> Result<Self, MicrovmError> {
741        let (sandbox_config, microvm_config, memory, vcpu_state) = snapshot.into_parts();
742        let backend =
743            BackendHandle::create_for_restore(sandbox_config.clone(), microvm_config.clone())?;
744        let mut sandbox = Self {
745            base_config: sandbox_config,
746            microvm_config,
747            state: MicrovmState::Ready,
748            backend,
749        };
750        sandbox.backend.restore_parts(&memory, &vcpu_state)?;
751        sandbox.state = MicrovmState::Ready;
752        Ok(sandbox)
753    }
754
755    fn with_ready_state<F, T>(&mut self, op_name: &str, op: F) -> Result<T, MicrovmError>
756    where
757        F: FnOnce(&mut BackendHandle) -> Result<T, MicrovmError>,
758    {
759        if self.state != MicrovmState::Ready {
760            return Err(MicrovmError::Lifecycle(LifecycleError::InvalidState {
761                expected: "Ready".into(),
762                current: format!("not Ready for {op_name}"),
763                message: format!("microVM not ready for {op_name}"),
764            }));
765        }
766
767        self.state = MicrovmState::Running;
768        let result = op(&mut self.backend);
769        self.state = if self.backend.is_destroyed() {
770            MicrovmState::Destroyed
771        } else {
772            MicrovmState::Ready
773        };
774        result
775    }
776
777    /// Reads file contents from the guest filesystem.
778    ///
779    /// The path is interpreted by the guest agent. The call requires a ready guest
780    /// and returns [`MicrovmError::GuestFile`] for guest-side file failures.
781    pub fn read_file(&mut self, path: &str) -> Result<Vec<u8>, MicrovmError> {
782        self.with_ready_state("read_file", |backend| backend.read_file(path))
783    }
784
785    /// Writes file contents into the guest filesystem.
786    ///
787    /// Existing files are handled by the guest agent according to its filesystem
788    /// semantics. The call requires a ready guest.
789    pub fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), MicrovmError> {
790        self.with_ready_state("write_file", |backend| backend.write_file(path, data))
791    }
792
793    /// Lists directory entries from the guest filesystem.
794    pub fn list_dir(&mut self, path: &str) -> Result<Vec<DirEntry>, MicrovmError> {
795        self.with_ready_state("list_dir", |backend| {
796            crate::guest_file_ops::list_dir(path, |cmd| backend.run_command(cmd))
797        })
798    }
799
800    /// Returns whether a guest path exists.
801    pub fn file_exists(&mut self, path: &str) -> Result<bool, MicrovmError> {
802        self.with_ready_state("file_exists", |backend| {
803            crate::guest_file_ops::file_exists(path, |cmd| backend.run_command(cmd))
804        })
805    }
806
807    /// Removes a guest file.
808    pub fn remove_file(&mut self, path: &str) -> Result<(), MicrovmError> {
809        self.with_ready_state("remove_file", |backend| {
810            crate::guest_file_ops::remove_file(path, |cmd| backend.run_command(cmd))
811        })
812    }
813
814    /// Renames or moves a guest file.
815    pub fn rename(&mut self, from: &str, to: &str) -> Result<(), MicrovmError> {
816        self.with_ready_state("rename", |backend| {
817            crate::guest_file_ops::rename(from, to, |cmd| backend.run_command(cmd))
818        })
819    }
820
821    /// Returns guest file metadata.
822    pub fn stat(&mut self, path: &str) -> Result<FileStat, MicrovmError> {
823        self.with_ready_state("stat", |backend| {
824            crate::guest_file_ops::stat(path, |cmd| backend.run_command(cmd))
825        })
826    }
827
828    /// Waits until the microVM is responsive.
829    ///
830    /// Readiness is verified with the guest command loop `PING`/`PONG` protocol. A
831    /// zero timeout is rejected as invalid configuration.
832    pub fn wait_ready(&mut self, timeout: Duration) -> Result<(), MicrovmError> {
833        if timeout.is_zero() {
834            return Err(MicrovmError::InvalidConfig(
835                "wait_ready timeout must not be zero".into(),
836            ));
837        }
838        if self.state == MicrovmState::Destroyed {
839            return Err(MicrovmError::Lifecycle(LifecycleError::Destroyed(
840                "microVM destroyed, cannot wait for ready".into(),
841            )));
842        }
843
844        self.with_ready_state("wait_ready", |backend| {
845            backend.ping_with_timeout(timeout).map(|_| ())
846        })
847    }
848
849    /// Returns whether the microVM is in the ready state and the guest is responsive.
850    pub fn is_ready(&self) -> bool {
851        self.state == MicrovmState::Ready && self.backend.is_ready()
852    }
853
854    /// Runs one guest `PING`/`PONG` readiness probe.
855    ///
856    /// The returned duration measures the host-observed round trip.
857    pub fn ping(&mut self) -> Result<Duration, MicrovmError> {
858        self.with_ready_state("ping", BackendHandle::ping)
859    }
860
861    /// Executes a command and returns a receiver for streaming output events.
862    pub fn stream_execute(
863        &mut self,
864        cmd: &[String],
865    ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
866        self.stream_execute_with_options(cmd, GuestExecOptions::default())
867    }
868
869    /// Executes a command as a stream of output events with command-level options.
870    ///
871    /// An empty command vector is rejected before the guest protocol is invoked.
872    pub fn stream_execute_with_options(
873        &mut self,
874        cmd: &[String],
875        options: GuestExecOptions,
876    ) -> Result<mpsc::Receiver<StreamEvent>, MicrovmError> {
877        if cmd.is_empty() {
878            return Err(MicrovmError::InvalidConfig(
879                "command must not be empty".into(),
880            ));
881        }
882
883        self.with_ready_state("stream_execute", |backend| {
884            backend.run_command_streaming_with_options(cmd, &options)
885        })
886    }
887
888    /// Executes a command with command-level environment variables.
889    ///
890    /// The provided environment map is scoped to this single command.
891    pub fn execute_with_env(
892        &mut self,
893        cmd: &[String],
894        env: HashMap<String, String>,
895    ) -> Result<GuestCommandResult, MicrovmError> {
896        self.execute_with_options(
897            cmd,
898            GuestExecOptions {
899                env,
900                timeout: None,
901                cwd: None,
902            },
903        )
904    }
905
906    /// Executes a command with a command-level timeout override.
907    ///
908    /// The timeout overrides the base sandbox timeout for this command only.
909    pub fn execute_with_timeout(
910        &mut self,
911        cmd: &[String],
912        timeout: Duration,
913    ) -> Result<GuestCommandResult, MicrovmError> {
914        self.execute_with_options(
915            cmd,
916            GuestExecOptions {
917                env: HashMap::new(),
918                timeout: Some(timeout),
919                cwd: None,
920            },
921        )
922    }
923
924    /// Executes a command with the full set of command-level options.
925    ///
926    /// Command execution is serialized by the sandbox lifecycle state. The sandbox
927    /// returns to ready after the command unless the backend marks it destroyed.
928    pub fn execute_with_options(
929        &mut self,
930        cmd: &[String],
931        options: GuestExecOptions,
932    ) -> Result<GuestCommandResult, MicrovmError> {
933        if cmd.is_empty() {
934            return Err(MicrovmError::InvalidConfig(
935                "command must not be empty".into(),
936            ));
937        }
938
939        self.with_ready_state("execute", |backend| {
940            backend.run_command_with_options(cmd, &options)
941        })
942    }
943
944    /// Sends a request through the host-controlled HTTP proxy.
945    ///
946    /// The request is validated against the base sandbox network policy before the
947    /// host performs the outbound HTTPS request.
948    pub fn http_request(&mut self, request: HttpRequest) -> Result<HttpResponse, MicrovmError> {
949        self.with_ready_state("http_request", |backend| backend.http_request(request))
950    }
951}
952
953impl Sandbox for MicrovmSandbox {
954    fn new(config: SandboxConfig) -> Result<Self, SandboxError> {
955        Self::new_with_base(config, MicrovmConfig::default()).map_err(Into::into)
956    }
957
958    fn execute(&mut self, cmd: &[String]) -> Result<SandboxResult, SandboxError> {
959        self.execute_with_options_for_sandbox(cmd, GuestExecOptions::default())
960            .map_err(SandboxError::from)
961    }
962
963    fn create_pty(
964        &mut self,
965        _config: mimobox_core::PtyConfig,
966    ) -> Result<Box<dyn mimobox_core::PtySession>, SandboxError> {
967        Err(SandboxError::UnsupportedOperation(
968            "PTY sessions currently only support OS-level backend, microVM not supported"
969                .to_string(),
970        ))
971    }
972
973    fn read_file(&mut self, path: &str) -> Result<Vec<u8>, SandboxError> {
974        MicrovmSandbox::read_file(self, path).map_err(SandboxError::from)
975    }
976
977    fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), SandboxError> {
978        MicrovmSandbox::write_file(self, path, data).map_err(SandboxError::from)
979    }
980
981    fn list_dir(&mut self, path: &str) -> Result<Vec<DirEntry>, SandboxError> {
982        MicrovmSandbox::list_dir(self, path).map_err(SandboxError::from)
983    }
984
985    fn file_exists(&mut self, path: &str) -> Result<bool, SandboxError> {
986        MicrovmSandbox::file_exists(self, path).map_err(SandboxError::from)
987    }
988
989    fn remove_file(&mut self, path: &str) -> Result<(), SandboxError> {
990        MicrovmSandbox::remove_file(self, path).map_err(SandboxError::from)
991    }
992
993    fn rename(&mut self, from: &str, to: &str) -> Result<(), SandboxError> {
994        MicrovmSandbox::rename(self, from, to).map_err(SandboxError::from)
995    }
996
997    fn stat(&mut self, path: &str) -> Result<FileStat, SandboxError> {
998        MicrovmSandbox::stat(self, path).map_err(SandboxError::from)
999    }
1000
1001    fn snapshot(&mut self) -> Result<SandboxSnapshot, SandboxError> {
1002        MicrovmSandbox::snapshot(self).map_err(SandboxError::from)
1003    }
1004
1005    fn fork(&mut self) -> Result<Self, SandboxError> {
1006        MicrovmSandbox::fork(self).map_err(SandboxError::from)
1007    }
1008
1009    fn destroy(self) -> Result<(), SandboxError> {
1010        let mut this = self;
1011        this.backend.shutdown().map_err(SandboxError::from)?;
1012        this.state = MicrovmState::Destroyed;
1013        Ok(())
1014    }
1015}
1016
1017fn map_snapshot_access_error(error: SandboxError) -> MicrovmError {
1018    match error {
1019        SandboxError::Io(error) => MicrovmError::Io(error),
1020        SandboxError::InvalidSnapshot => {
1021            MicrovmError::SnapshotFormat("invalid sandbox snapshot".into())
1022        }
1023        other => MicrovmError::SnapshotFormat(other.to_string()),
1024    }
1025}
1026
1027impl MicrovmSandbox {
1028    fn execute_with_options_for_sandbox(
1029        &mut self,
1030        cmd: &[String],
1031        options: GuestExecOptions,
1032    ) -> Result<SandboxResult, MicrovmError> {
1033        if cmd.is_empty() {
1034            return Err(MicrovmError::InvalidConfig(
1035                "command must not be empty".into(),
1036            ));
1037        }
1038
1039        self.with_ready_state("execute", |backend| {
1040            let start = Instant::now();
1041            let guest = backend.run_command_with_options(cmd, &options)?;
1042            Ok(SandboxResult {
1043                stdout: guest.stdout,
1044                stderr: guest.stderr,
1045                exit_code: guest.exit_code,
1046                elapsed: start.elapsed(),
1047                timed_out: guest.timed_out,
1048            })
1049        })
1050    }
1051}