Skip to main content

hyperlight_host/hypervisor/virtual_machine/
mod.rs

1/*
2Copyright 2025 The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17use std::fmt::Debug;
18use std::sync::OnceLock;
19
20use tracing::{Span, instrument};
21
22#[cfg(gdb)]
23use crate::hypervisor::gdb::DebugError;
24use crate::hypervisor::regs::{
25    CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters,
26};
27use crate::mem::memory_region::MemoryRegion;
28#[cfg(feature = "trace_guest")]
29use crate::sandbox::trace::TraceContext as SandboxTraceContext;
30
31/// KVM (Kernel-based Virtual Machine) functionality (linux)
32#[cfg(kvm)]
33pub(crate) mod kvm;
34/// MSHV (Microsoft Hypervisor) functionality (linux)
35#[cfg(mshv3)]
36pub(crate) mod mshv;
37/// WHP (Windows Hypervisor Platform) functionality (windows)
38#[cfg(target_os = "windows")]
39pub(crate) mod whp;
40
41/// Shared x86-64 helpers for hardware interrupt support (MSHV and WHP)
42#[cfg(feature = "hw-interrupts")]
43pub(crate) mod x86_64;
44
45static AVAILABLE_HYPERVISOR: OnceLock<Option<HypervisorType>> = OnceLock::new();
46
47/// Returns which type of hypervisor is available, if any
48pub fn get_available_hypervisor() -> &'static Option<HypervisorType> {
49    AVAILABLE_HYPERVISOR.get_or_init(|| {
50        cfg_if::cfg_if! {
51            if #[cfg(all(kvm, mshv3))] {
52                // If both features are enabled, we need to determine hypervisor at runtime.
53                // Currently /dev/kvm and /dev/mshv cannot exist on the same machine, so the first one
54                // that works is guaranteed to be correct.
55                if mshv::is_hypervisor_present() {
56                    Some(HypervisorType::Mshv)
57                } else if kvm::is_hypervisor_present() {
58                    Some(HypervisorType::Kvm)
59                } else {
60                    None
61                }
62            } else if #[cfg(kvm)] {
63                if kvm::is_hypervisor_present() {
64                    Some(HypervisorType::Kvm)
65                } else {
66                    None
67                }
68            } else if #[cfg(mshv3)] {
69                if mshv::is_hypervisor_present() {
70                    Some(HypervisorType::Mshv)
71                } else {
72                    None
73                }
74            } else if #[cfg(target_os = "windows")] {
75                if whp::is_hypervisor_present() {
76                    Some(HypervisorType::Whp)
77                } else {
78                    None
79                }
80            } else {
81                None
82            }
83        }
84    })
85}
86
87/// Returns `true` if a suitable hypervisor is available.
88/// If this returns `false`, no hypervisor-backed sandboxes can be created.
89#[instrument(skip_all, parent = Span::current())]
90pub fn is_hypervisor_present() -> bool {
91    get_available_hypervisor().is_some()
92}
93
94/// The hypervisor types available for the current platform
95#[derive(PartialEq, Eq, Debug, Copy, Clone)]
96pub(crate) enum HypervisorType {
97    #[cfg(kvm)]
98    Kvm,
99
100    #[cfg(mshv3)]
101    Mshv,
102
103    #[cfg(target_os = "windows")]
104    Whp,
105}
106
107/// Minimum XSAVE buffer size: 512 bytes legacy region + 64 bytes header.
108/// Only used by MSHV and WHP which use compacted XSAVE format and need to
109/// validate buffer size before accessing XCOMP_BV.
110#[cfg(all(target_arch = "x86_64", any(mshv3, target_os = "windows")))]
111pub(crate) const XSAVE_MIN_SIZE: usize = 576;
112
113/// Standard XSAVE buffer size (4KB) used by KVM and MSHV.
114/// WHP queries the required size dynamically.
115#[cfg(all(any(kvm, mshv3), test, not(target_arch = "aarch64")))]
116pub(crate) const XSAVE_BUFFER_SIZE: usize = 4096;
117
118// Compiler error if no hypervisor type is available (not applicable on aarch64 yet)
119#[cfg(not(any(kvm, mshv3, target_os = "windows", target_arch = "aarch64")))]
120compile_error!(
121    "No hypervisor type is available for the current platform. Please enable either the `kvm` or `mshv3` cargo feature."
122);
123
124/// The various reasons a VM's vCPU can exit
125pub(crate) enum VmExit {
126    /// The vCPU has exited due to a debug event (usually breakpoint)
127    #[cfg(gdb)]
128    Debug {
129        #[cfg(target_arch = "x86_64")]
130        dr6: u64,
131        #[cfg(target_arch = "x86_64")]
132        exception: u32,
133    },
134    /// The vCPU has halted
135    Halt(),
136    /// The vCPU has issued a write to the given port with the given value
137    IoOut(u16, Vec<u8>),
138    /// The vCPU tried to read from the given (unmapped) addr
139    MmioRead(u64),
140    /// The vCPU tried to write to the given (unmapped) addr
141    MmioWrite(u64),
142    /// The vCPU execution has been cancelled
143    Cancelled(),
144    /// The vCPU has exited for a reason that is not handled by Hyperlight
145    Unknown(String),
146    /// The operation should be retried, for example this can happen on Linux where a call to run the CPU can return EAGAIN
147    #[cfg_attr(
148        any(target_os = "windows", feature = "hw-interrupts"),
149        expect(
150            dead_code,
151            reason = "Retry() is never constructed on Windows or with hw-interrupts (EAGAIN causes continue instead)"
152        )
153    )]
154    Retry(),
155}
156
157/// VM error
158#[derive(Debug, Clone, thiserror::Error)]
159pub enum VmError {
160    #[error("Failed to create vm: {0}")]
161    CreateVm(#[from] CreateVmError),
162    #[cfg(gdb)]
163    #[error("Debug operation failed: {0}")]
164    Debug(#[from] DebugError),
165    #[error("Map memory operation failed: {0}")]
166    MapMemory(#[from] MapMemoryError),
167    #[error("Register operation failed: {0}")]
168    Register(#[from] RegisterError),
169    #[error("Failed to run vcpu: {0}")]
170    RunVcpu(#[from] RunVcpuError),
171    #[error("Unmap memory operation failed: {0}")]
172    UnmapMemory(#[from] UnmapMemoryError),
173}
174
175/// Create VM error
176#[derive(Debug, Clone, thiserror::Error)]
177pub enum CreateVmError {
178    #[error("VCPU creation failed: {0}")]
179    CreateVcpuFd(HypervisorError),
180    #[error("VM creation failed: {0}")]
181    CreateVmFd(HypervisorError),
182    #[error("Hypervisor is not available: {0}")]
183    HypervisorNotAvailable(HypervisorError),
184    #[error("Initialize VM failed: {0}")]
185    InitializeVm(HypervisorError),
186    #[error("Set Partition Property failed: {0}")]
187    SetPartitionProperty(HypervisorError),
188    #[cfg(target_os = "windows")]
189    #[error("Surrogate process creation failed: {0}")]
190    SurrogateProcess(String),
191}
192
193/// RunVCPU error
194#[derive(Debug, Clone, thiserror::Error)]
195pub enum RunVcpuError {
196    #[error("Failed to decode message type: {0}")]
197    DecodeIOMessage(u32),
198    #[cfg(gdb)]
199    #[error("Failed to get DR6 debug register: {0}")]
200    GetDr6(HypervisorError),
201    #[error("Increment RIP failed: {0}")]
202    IncrementRip(HypervisorError),
203    #[error("Parse GPA access info failed")]
204    ParseGpaAccessInfo,
205    #[cfg(target_arch = "aarch64")]
206    #[error("Flush MMIO pending state failed: {0}")]
207    FlushMmioPending(String),
208    #[error("Unknown error: {0}")]
209    Unknown(HypervisorError),
210}
211
212/// Register error
213#[derive(Debug, Clone, thiserror::Error)]
214pub enum RegisterError {
215    #[error("Failed to get registers: {0}")]
216    GetRegs(HypervisorError),
217    #[error("Failed to set registers: {0}")]
218    SetRegs(HypervisorError),
219    #[error("Failed to get FPU registers: {0}")]
220    GetFpu(HypervisorError),
221    #[error("Failed to set FPU registers: {0}")]
222    SetFpu(HypervisorError),
223    #[error("Failed to get special registers: {0}")]
224    GetSregs(HypervisorError),
225    #[error("Failed to set special registers: {0}")]
226    SetSregs(HypervisorError),
227    #[error("Failed to get debug registers: {0}")]
228    GetDebugRegs(HypervisorError),
229    #[error("Failed to set debug registers: {0}")]
230    SetDebugRegs(HypervisorError),
231    #[error("Failed to get xsave: {0}")]
232    GetXsave(HypervisorError),
233    #[error("Failed to set xsave: {0}")]
234    SetXsave(HypervisorError),
235    #[error("Xsave size mismatch: expected {expected} bytes, got {actual}")]
236    XsaveSizeMismatch {
237        /// Expected size in bytes
238        expected: u32,
239        /// Actual size in bytes
240        actual: u32,
241    },
242    #[error("Invalid xsave alignment")]
243    InvalidXsaveAlignment,
244    #[cfg(target_os = "windows")]
245    #[error("Failed to get xsave size: {0}")]
246    GetXsaveSize(#[from] HypervisorError),
247    #[cfg(target_os = "windows")]
248    #[error("Failed to convert WHP registers: {0}")]
249    ConversionFailed(String),
250}
251
252#[derive(Debug, Clone, thiserror::Error)]
253pub enum ResetVcpuError {
254    #[error("Single-operation vcpu reset not supported on this hypervisor")]
255    NotSupported,
256    #[error("Hypervisor operation failed: {0}")]
257    Hypervisor(HypervisorError),
258    #[error("Register operation failed: {0}")]
259    Register(#[from] RegisterError),
260    #[error("Operation failed: {0}")]
261    Unknown(String),
262}
263
264/// Map memory error
265#[derive(Debug, Clone, thiserror::Error)]
266pub enum MapMemoryError {
267    #[cfg(target_os = "windows")]
268    #[error("Address conversion failed: {0}")]
269    AddressConversion(std::num::TryFromIntError),
270    #[error("Hypervisor error: {0}")]
271    Hypervisor(HypervisorError),
272    #[cfg(target_os = "windows")]
273    #[error("Invalid memory region flags: {0}")]
274    InvalidFlags(String),
275    #[cfg(target_os = "windows")]
276    #[error("Failed to load API '{api_name}': {source}")]
277    LoadApi {
278        api_name: &'static str,
279        source: windows_result::Error,
280    },
281    #[cfg(target_os = "windows")]
282    #[error("Operation not supported: {0}")]
283    NotSupported(String),
284    #[cfg(target_os = "windows")]
285    #[error("Surrogate process creation failed: {0}")]
286    SurrogateProcess(String),
287}
288
289/// Unmap memory error
290#[derive(Debug, Clone, thiserror::Error)]
291pub enum UnmapMemoryError {
292    #[error("Hypervisor error: {0}")]
293    Hypervisor(HypervisorError),
294}
295
296/// Implementation-specific Hypervisor error
297#[derive(Debug, Clone, thiserror::Error)]
298pub enum HypervisorError {
299    #[cfg(kvm)]
300    #[error("KVM error: {0}")]
301    KvmError(#[from] kvm_ioctls::Error),
302    #[cfg(mshv3)]
303    #[error("MSHV error: {0}")]
304    MshvError(#[from] mshv_ioctls::MshvError),
305    #[cfg(target_os = "windows")]
306    #[error("Windows error: {0}")]
307    WindowsError(#[from] windows_result::Error),
308}
309
310/// Trait for single-vCPU VMs. Provides a common interface for basic VM operations.
311/// Abstracts over differences between KVM, MSHV and WHP implementations.
312pub(crate) trait VirtualMachine: Debug + Send {
313    /// Map memory region into this VM
314    ///
315    /// # Safety
316    /// The caller must ensure that the memory region is valid and points to valid memory,
317    /// and lives long enough for the VM to use it.
318    /// The caller must ensure that the given u32 is not already mapped, otherwise previously mapped
319    /// memory regions may be overwritten.
320    /// The memory region must not overlap with an existing region, and depending on platform, must be aligned to page boundaries.
321    unsafe fn map_memory(
322        &mut self,
323        region: (u32, &MemoryRegion),
324    ) -> std::result::Result<(), MapMemoryError>;
325
326    /// Unmap memory region from this VM that has previously been mapped using `map_memory`.
327    fn unmap_memory(
328        &mut self,
329        region: (u32, &MemoryRegion),
330    ) -> std::result::Result<(), UnmapMemoryError>;
331
332    /// Runs the vCPU until it exits.
333    /// Note: this function emits traces spans for guests
334    /// and the span setup is called right before the run virtual processor call of each hypervisor
335    fn run_vcpu(
336        &mut self,
337        #[cfg(feature = "trace_guest")] tc: &mut SandboxTraceContext,
338    ) -> std::result::Result<VmExit, RunVcpuError>;
339
340    /// Get regs
341    #[allow(dead_code)]
342    fn regs(&self) -> std::result::Result<CommonRegisters, RegisterError>;
343    /// Set regs
344    fn set_regs(&self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError>;
345    /// Get fpu regs
346    #[allow(dead_code)]
347    fn fpu(&self) -> std::result::Result<CommonFpu, RegisterError>;
348    /// Set fpu regs
349    fn set_fpu(&self, fpu: &CommonFpu) -> std::result::Result<(), RegisterError>;
350    /// Get special regs
351    #[allow(dead_code)]
352    fn sregs(&self) -> std::result::Result<CommonSpecialRegisters, RegisterError>;
353    /// Set special regs
354    fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> std::result::Result<(), RegisterError>;
355    /// Get the debug registers of the vCPU
356    #[allow(dead_code)]
357    fn debug_regs(&self) -> std::result::Result<CommonDebugRegs, RegisterError>;
358    /// Set the debug registers of the vCPU
359    #[allow(dead_code)]
360    fn set_debug_regs(&self, drs: &CommonDebugRegs) -> std::result::Result<(), RegisterError>;
361
362    /// Get xsave
363    #[allow(dead_code)]
364    #[cfg(not(target_arch = "aarch64"))]
365    fn xsave(&self) -> std::result::Result<Vec<u8>, RegisterError>;
366    /// Reset xsave to default state
367    #[cfg(not(target_arch = "aarch64"))]
368    fn reset_xsave(&self) -> std::result::Result<(), RegisterError>;
369    /// Set xsave - only used for tests
370    #[cfg(test)]
371    #[cfg(not(target_arch = "aarch64"))]
372    fn set_xsave(&self, xsave: &[u32]) -> std::result::Result<(), RegisterError>;
373
374    /// Single-operation vCPU reset
375    #[cfg(target_arch = "aarch64")]
376    fn can_reset_vcpu(&self) -> bool {
377        false
378    }
379    #[cfg(target_arch = "aarch64")]
380    fn reset_vcpu(&mut self) -> std::result::Result<(), ResetVcpuError> {
381        Err(ResetVcpuError::NotSupported)
382    }
383    /// Get partition handle
384    #[cfg(target_os = "windows")]
385    fn partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE;
386}
387
388#[cfg(test)]
389mod tests {
390
391    #[test]
392    // TODO: add support for testing on WHP
393    #[cfg(target_os = "linux")]
394    fn is_hypervisor_present() {
395        use std::path::Path;
396
397        cfg_if::cfg_if! {
398            if #[cfg(all(kvm, mshv3))] {
399                assert_eq!(Path::new("/dev/kvm").exists() || Path::new("/dev/mshv").exists(), super::is_hypervisor_present());
400            } else if #[cfg(kvm)] {
401                assert_eq!(Path::new("/dev/kvm").exists(), super::is_hypervisor_present());
402            } else if #[cfg(mshv3)] {
403                assert_eq!(Path::new("/dev/mshv").exists(), super::is_hypervisor_present());
404            } else {
405                assert!(!super::is_hypervisor_present());
406            }
407        }
408    }
409}