use std::os::raw::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use hyperlight_common::outb::VmAction;
#[cfg(feature = "trace_guest")]
use tracing::Span;
#[cfg(feature = "trace_guest")]
use tracing_opentelemetry::OpenTelemetrySpanExt;
use windows::Win32::Foundation::{CloseHandle, FreeLibrary, HANDLE};
use windows::Win32::System::Hypervisor::*;
use windows::Win32::System::LibraryLoader::*;
use windows::Win32::System::Memory::{MEMORY_MAPPED_VIEW_ADDRESS, UnmapViewOfFile};
use windows::core::s;
use windows_result::HRESULT;
#[cfg(gdb)]
use crate::hypervisor::gdb::{DebugError, DebuggableVm};
use crate::hypervisor::regs::{
Align16, CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters,
FP_CONTROL_WORD_DEFAULT, MSR_APERF, MSR_BNDCFGS, MSR_CSTAR, MSR_IA32_SSP,
MSR_INTERRUPT_SSP_TABLE_ADDR, MSR_KERNEL_GS_BASE, MSR_LSTAR, MSR_MPERF, MSR_MTRR_CAP, MSR_PAT,
MSR_PL0_SSP, MSR_PL1_SSP, MSR_PL2_SSP, MSR_PL3_SSP, MSR_S_CET, MSR_SFMASK, MSR_SPEC_CTRL,
MSR_STAR, MSR_SYSENTER_CS, MSR_SYSENTER_EIP, MSR_SYSENTER_ESP, MSR_TSC_DEADLINE, MSR_TSX_CTRL,
MSR_U_CET, MSR_UMWAIT_CONTROL, MSR_XFD, MSR_XFD_ERR, MXCSR_DEFAULT, MsrEntry,
WHP_DEBUG_REGS_NAMES, WHP_DEBUG_REGS_NAMES_LEN, WHP_FPU_NAMES, WHP_FPU_NAMES_LEN,
WHP_REGS_NAMES, WHP_REGS_NAMES_LEN, WHP_SREGS_NAMES, WHP_SREGS_NAMES_LEN,
};
use crate::hypervisor::surrogate_process::SurrogateProcess;
use crate::hypervisor::surrogate_process_manager::{
get_surrogate_process_manager, surrogates_disabled,
};
#[cfg(feature = "hw-interrupts")]
use crate::hypervisor::virtual_machine::x86_64::hw_interrupts::TimerThread;
use crate::hypervisor::virtual_machine::{
CreateVmError, HypervisorError, MapMemoryError, RegisterError, RunVcpuError, UnmapMemoryError,
VirtualMachine, VmExit, XSAVE_MIN_SIZE,
};
use crate::hypervisor::wrappers::HandleWrapper;
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
#[cfg(feature = "trace_guest")]
use crate::sandbox::trace::TraceContext as SandboxTraceContext;
#[allow(dead_code)] pub(crate) fn is_hypervisor_present() -> bool {
let mut capability: WHV_CAPABILITY = Default::default();
let written_size: Option<*mut u32> = None;
match unsafe {
WHvGetCapability(
WHvCapabilityCodeHypervisorPresent,
&mut capability as *mut _ as *mut c_void,
std::mem::size_of::<WHV_CAPABILITY>() as u32,
written_size,
)
} {
Ok(_) => unsafe { capability.HypervisorPresent.as_bool() },
Err(_) => {
tracing::info!("Windows Hypervisor Platform is not available on this system");
false
}
}
}
fn msr_to_whv_register_name(index: u32) -> Option<WHV_REGISTER_NAME> {
Some(match index {
MSR_MTRR_CAP => WHvX64RegisterMsrMtrrCap,
MSR_SYSENTER_CS => WHvX64RegisterSysenterCs,
MSR_SYSENTER_ESP => WHvX64RegisterSysenterEsp,
MSR_SYSENTER_EIP => WHvX64RegisterSysenterEip,
MSR_PAT => WHvX64RegisterPat,
MSR_STAR => WHvX64RegisterStar,
MSR_LSTAR => WHvX64RegisterLstar,
MSR_CSTAR => WHvX64RegisterCstar,
MSR_SFMASK => WHvX64RegisterSfmask,
MSR_KERNEL_GS_BASE => WHvX64RegisterKernelGsBase,
MSR_SPEC_CTRL => WHvX64RegisterSpecCtrl,
MSR_U_CET => WHvX64RegisterUCet,
MSR_S_CET => WHvX64RegisterSCet,
MSR_PL0_SSP => WHvX64RegisterPl0Ssp,
MSR_PL1_SSP => WHvX64RegisterPl1Ssp,
MSR_PL2_SSP => WHvX64RegisterPl2Ssp,
MSR_PL3_SSP => WHvX64RegisterPl3Ssp,
MSR_INTERRUPT_SSP_TABLE_ADDR => WHvX64RegisterInterruptSspTableAddr,
MSR_IA32_SSP => WHvX64RegisterSsp,
0x10 => WHvX64RegisterTsc,
0x3B => WHvX64RegisterTscAdjust,
0xC000_0103 => WHvX64RegisterTscAux,
MSR_MPERF => WHvX64RegisterMCount,
MSR_APERF => WHvX64RegisterACount,
MSR_TSX_CTRL => WHvX64RegisterTsxCtrl,
MSR_XFD => WHvX64RegisterXfd,
MSR_XFD_ERR => WHvX64RegisterXfdErr,
MSR_UMWAIT_CONTROL => WHvX64RegisterUmwaitControl,
MSR_TSC_DEADLINE => WHvX64RegisterTscDeadline,
MSR_BNDCFGS => WHvX64RegisterBndcfgs,
0xDA0 => WHvX64RegisterXss,
0x2FF => WHvX64RegisterMsrMtrrDefType,
0x200 => WHvX64RegisterMsrMtrrPhysBase0,
0x201 => WHvX64RegisterMsrMtrrPhysMask0,
0x202 => WHvX64RegisterMsrMtrrPhysBase1,
0x203 => WHvX64RegisterMsrMtrrPhysMask1,
0x204 => WHvX64RegisterMsrMtrrPhysBase2,
0x205 => WHvX64RegisterMsrMtrrPhysMask2,
0x206 => WHvX64RegisterMsrMtrrPhysBase3,
0x207 => WHvX64RegisterMsrMtrrPhysMask3,
0x208 => WHvX64RegisterMsrMtrrPhysBase4,
0x209 => WHvX64RegisterMsrMtrrPhysMask4,
0x20A => WHvX64RegisterMsrMtrrPhysBase5,
0x20B => WHvX64RegisterMsrMtrrPhysMask5,
0x20C => WHvX64RegisterMsrMtrrPhysBase6,
0x20D => WHvX64RegisterMsrMtrrPhysMask6,
0x20E => WHvX64RegisterMsrMtrrPhysBase7,
0x20F => WHvX64RegisterMsrMtrrPhysMask7,
0x210 => WHvX64RegisterMsrMtrrPhysBase8,
0x211 => WHvX64RegisterMsrMtrrPhysMask8,
0x212 => WHvX64RegisterMsrMtrrPhysBase9,
0x213 => WHvX64RegisterMsrMtrrPhysMask9,
0x214 => WHvX64RegisterMsrMtrrPhysBaseA,
0x215 => WHvX64RegisterMsrMtrrPhysMaskA,
0x216 => WHvX64RegisterMsrMtrrPhysBaseB,
0x217 => WHvX64RegisterMsrMtrrPhysMaskB,
0x218 => WHvX64RegisterMsrMtrrPhysBaseC,
0x219 => WHvX64RegisterMsrMtrrPhysMaskC,
0x21A => WHvX64RegisterMsrMtrrPhysBaseD,
0x21B => WHvX64RegisterMsrMtrrPhysMaskD,
0x21C => WHvX64RegisterMsrMtrrPhysBaseE,
0x21D => WHvX64RegisterMsrMtrrPhysMaskE,
0x21E => WHvX64RegisterMsrMtrrPhysBaseF,
0x21F => WHvX64RegisterMsrMtrrPhysMaskF,
0x250 => WHvX64RegisterMsrMtrrFix64k00000,
0x258 => WHvX64RegisterMsrMtrrFix16k80000,
0x259 => WHvX64RegisterMsrMtrrFix16kA0000,
0x268 => WHvX64RegisterMsrMtrrFix4kC0000,
0x269 => WHvX64RegisterMsrMtrrFix4kC8000,
0x26A => WHvX64RegisterMsrMtrrFix4kD0000,
0x26B => WHvX64RegisterMsrMtrrFix4kD8000,
0x26C => WHvX64RegisterMsrMtrrFix4kE0000,
0x26D => WHvX64RegisterMsrMtrrFix4kE8000,
0x26E => WHvX64RegisterMsrMtrrFix4kF0000,
0x26F => WHvX64RegisterMsrMtrrFix4kF8000,
_ => return None,
})
}
#[cfg(test)]
mod msr_mapping_tests {
use super::*;
use crate::hypervisor::regs::{MSR_DEBUGCTL, MSR_VIRT_SPEC_CTRL, resettable_msr_indices};
#[test]
fn maps_all_stateful_msrs_except_debugctl_and_virt_spec_ctrl() {
for index in resettable_msr_indices() {
if index != MSR_DEBUGCTL && index != MSR_VIRT_SPEC_CTRL {
assert!(
msr_to_whv_register_name(index).is_some(),
"missing MSR mapping for {index:#x}"
);
}
}
assert!(msr_to_whv_register_name(MSR_DEBUGCTL).is_none());
assert!(msr_to_whv_register_name(MSR_VIRT_SPEC_CTRL).is_none());
assert_eq!(
msr_to_whv_register_name(MSR_MPERF),
Some(WHvX64RegisterMCount)
);
assert_eq!(
msr_to_whv_register_name(MSR_APERF),
Some(WHvX64RegisterACount)
);
assert_eq!(
msr_to_whv_register_name(MSR_TSX_CTRL),
Some(WHvX64RegisterTsxCtrl)
);
assert_eq!(msr_to_whv_register_name(MSR_XFD), Some(WHvX64RegisterXfd));
assert_eq!(
msr_to_whv_register_name(MSR_XFD_ERR),
Some(WHvX64RegisterXfdErr)
);
assert_eq!(
msr_to_whv_register_name(MSR_UMWAIT_CONTROL),
Some(WHvX64RegisterUmwaitControl)
);
assert_eq!(
msr_to_whv_register_name(MSR_TSC_DEADLINE),
Some(WHvX64RegisterTscDeadline)
);
assert_eq!(
msr_to_whv_register_name(MSR_BNDCFGS),
Some(WHvX64RegisterBndcfgs)
);
}
}
fn release_file_mapping(view_base: *mut c_void, mapping_handle: HandleWrapper) {
unsafe {
if let Err(e) = UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { Value: view_base }) {
tracing::error!("Failed to unmap file view at {:?}: {:?}", view_base, e);
}
if let Err(e) = CloseHandle(mapping_handle.into()) {
tracing::error!(
"Failed to close file mapping handle {:?}: {:?}",
mapping_handle,
e
);
}
}
}
static NO_SURROGATE_VM_ACTIVE: AtomicBool = AtomicBool::new(false);
#[derive(Debug)]
struct NoSurrogateGuard;
impl NoSurrogateGuard {
fn acquire() -> Result<Self, CreateVmError> {
if NO_SURROGATE_VM_ACTIVE.swap(true, Ordering::SeqCst) {
return Err(CreateVmError::SurrogateProcess(
"HYPERLIGHT_MAX_SURROGATES=0 limits the process to a single VM; \
a VM is already active"
.into(),
));
}
Ok(Self)
}
}
impl Drop for NoSurrogateGuard {
fn drop(&mut self) {
NO_SURROGATE_VM_ACTIVE.store(false, Ordering::SeqCst);
}
}
#[derive(Debug)]
pub(crate) struct WhpVm {
partition: WHV_PARTITION_HANDLE,
surrogate_process: Option<SurrogateProcess>,
file_mappings: Vec<(HandleWrapper, *mut c_void)>,
_no_surrogate_guard: Option<NoSurrogateGuard>,
#[cfg(feature = "hw-interrupts")]
timer: Option<TimerThread>,
}
unsafe impl Send for WhpVm {}
impl WhpVm {
pub(crate) fn new() -> Result<Self, CreateVmError> {
const NUM_CPU: u32 = 1;
let no_surrogate = surrogates_disabled();
let no_surrogate_guard = if no_surrogate {
Some(NoSurrogateGuard::acquire()?)
} else {
None
};
let partition = unsafe {
#[cfg(feature = "hw-interrupts")]
Self::check_lapic_emulation_support()?;
let p = WHvCreatePartition().map_err(|e| CreateVmError::CreateVmFd(e.into()))?;
WHvSetPartitionProperty(
p,
WHvPartitionPropertyCodeProcessorCount,
&NUM_CPU as *const _ as *const _,
std::mem::size_of_val(&NUM_CPU) as _,
)
.map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?;
#[cfg(feature = "hw-interrupts")]
Self::enable_lapic_emulation(p)?;
WHvSetupPartition(p).map_err(|e| CreateVmError::InitializeVm(e.into()))?;
WHvCreateVirtualProcessor(p, 0, 0)
.map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?;
#[cfg(feature = "hw-interrupts")]
Self::init_lapic_bulk(p).map_err(|e| CreateVmError::InitializeVm(e.into()))?;
p
};
let surrogate_process = if no_surrogate {
None
} else {
let mgr = get_surrogate_process_manager()
.map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?;
Some(
mgr.get_surrogate_process()
.map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?,
)
};
Ok(WhpVm {
partition,
surrogate_process,
file_mappings: Vec::new(),
_no_surrogate_guard: no_surrogate_guard,
#[cfg(feature = "hw-interrupts")]
timer: None,
})
}
fn get_registers(
&self,
names: &[WHV_REGISTER_NAME],
values: &mut [Align16<WHV_REGISTER_VALUE>],
) -> windows_result::Result<()> {
assert_eq!(names.len(), values.len());
unsafe {
WHvGetVirtualProcessorRegisters(
self.partition,
0,
names.as_ptr(),
names.len() as u32,
values.as_mut_ptr() as *mut WHV_REGISTER_VALUE,
)
}
}
fn set_registers(
&self,
registers: &[(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>)],
) -> windows_result::Result<()> {
let (names, values): (Vec<_>, Vec<_>) = registers.iter().copied().unzip();
unsafe {
WHvSetVirtualProcessorRegisters(
self.partition,
0,
names.as_ptr(),
names.len() as u32,
values.as_ptr() as *const WHV_REGISTER_VALUE, )
}
}
}
impl VirtualMachine for WhpVm {
unsafe fn map_memory(
&mut self,
(_slot, region): (u32, &MemoryRegion),
) -> Result<(), MapMemoryError> {
let flags = region
.flags
.iter()
.map(|flag| match flag {
MemoryRegionFlags::NONE => Ok(WHvMapGpaRangeFlagNone),
MemoryRegionFlags::READ => Ok(WHvMapGpaRangeFlagRead),
MemoryRegionFlags::WRITE => Ok(WHvMapGpaRangeFlagWrite),
MemoryRegionFlags::EXECUTE => Ok(WHvMapGpaRangeFlagExecute),
_ => Err(MapMemoryError::InvalidFlags(format!(
"Invalid memory region flag: {:?}",
flag
))),
})
.collect::<std::result::Result<Vec<WHV_MAP_GPA_RANGE_FLAGS>, MapMemoryError>>()?
.iter()
.fold(WHvMapGpaRangeFlagNone, |acc, flag| acc | *flag);
match &mut self.surrogate_process {
None => {
let host_addr = (region.host_region.start.handle_base
+ region.host_region.start.offset)
as *const c_void;
let res = unsafe {
WHvMapGpaRange(
self.partition,
host_addr,
region.guest_region.start as u64,
region.guest_region.len() as u64,
flags,
)
};
if let Err(e) = res {
return Err(MapMemoryError::Hypervisor(HypervisorError::WindowsError(e)));
}
}
Some(surrogate) => {
let surrogate_base = surrogate
.map(
region.host_region.start.from_handle,
region.host_region.start.handle_base,
region.host_region.start.handle_size,
®ion.region_type.surrogate_mapping(),
)
.map_err(|e| MapMemoryError::SurrogateProcess(e.to_string()))?;
let surrogate_addr = surrogate_base.wrapping_add(region.host_region.start.offset);
let whvmapgparange2_func = unsafe {
match try_load_whv_map_gpa_range2() {
Ok(func) => func,
Err(e) => {
return Err(MapMemoryError::LoadApi {
api_name: "WHvMapGpaRange2",
source: e,
});
}
}
};
let res = unsafe {
whvmapgparange2_func(
self.partition,
surrogate.process_handle.into(),
surrogate_addr,
region.guest_region.start as u64,
region.guest_region.len() as u64,
flags,
)
};
if res.is_err() {
return Err(MapMemoryError::Hypervisor(HypervisorError::WindowsError(
windows_result::Error::from_hresult(res),
)));
}
}
}
if region.region_type == MemoryRegionType::MappedFile {
self.file_mappings.push((
region.host_region.start.from_handle,
region.host_region.start.handle_base as *mut c_void,
));
}
Ok(())
}
fn unmap_memory(
&mut self,
(_slot, region): (u32, &MemoryRegion),
) -> Result<(), UnmapMemoryError> {
unsafe {
WHvUnmapGpaRange(
self.partition,
region.guest_region.start as u64,
region.guest_region.len() as u64,
)
.map_err(|e| UnmapMemoryError::Hypervisor(HypervisorError::WindowsError(e)))?;
}
if let Some(surrogate) = &mut self.surrogate_process {
surrogate.unmap(region.host_region.start.handle_base);
}
if region.region_type == MemoryRegionType::MappedFile {
let handle_base = region.host_region.start.handle_base as *mut c_void;
if let Some(pos) = self
.file_mappings
.iter()
.position(|(_, vb)| *vb == handle_base)
{
let (handle, view) = self.file_mappings.swap_remove(pos);
release_file_mapping(view, handle);
}
}
Ok(())
}
#[expect(non_upper_case_globals, reason = "Windows API constant are lower case")]
fn run_vcpu(
&mut self,
#[cfg(feature = "trace_guest")] tc: &mut SandboxTraceContext,
) -> std::result::Result<VmExit, RunVcpuError> {
let mut exit_context: WHV_RUN_VP_EXIT_CONTEXT = Default::default();
#[cfg(feature = "trace_guest")]
tc.setup_guest_trace(Span::current().context());
loop {
unsafe {
WHvRunVirtualProcessor(
self.partition,
0,
&mut exit_context as *mut _ as *mut c_void,
std::mem::size_of::<WHV_RUN_VP_EXIT_CONTEXT>() as u32,
)
.map_err(|e| RunVcpuError::Unknown(e.into()))?;
}
match exit_context.ExitReason {
WHvRunVpExitReasonX64IoPortAccess => unsafe {
let instruction_length = exit_context.VpContext._bitfield & 0xF;
let rip = exit_context.VpContext.Rip + instruction_length as u64;
let port = exit_context.Anonymous.IoPortAccess.PortNumber;
let rax = exit_context.Anonymous.IoPortAccess.Rax;
let access_info_bits = exit_context
.Anonymous
.IoPortAccess
.AccessInfo
.Anonymous
._bitfield;
let is_write = access_info_bits & 1 != 0;
let access_size = (((access_info_bits >> 1) & 0x7) as usize).clamp(1, 8);
self.set_registers(&[(
WHvX64RegisterRip,
Align16(WHV_REGISTER_VALUE { Reg64: rip }),
)])
.map_err(|e| RunVcpuError::IncrementRip(e.into()))?;
if is_write && port == VmAction::Halt as u16 {
#[cfg(feature = "hw-interrupts")]
if let Some(mut t) = self.timer.take() {
t.stop();
}
return Ok(VmExit::Halt());
}
#[cfg(feature = "hw-interrupts")]
{
if is_write {
let data = rax.to_le_bytes();
if self.handle_hw_io_out(port, &data) {
continue;
}
} else if let Some(val) = super::x86_64::hw_interrupts::handle_io_in(port) {
self.set_registers(&[(
WHvX64RegisterRax,
Align16(WHV_REGISTER_VALUE { Reg64: val }),
)])
.map_err(|e| RunVcpuError::Unknown(e.into()))?;
continue;
}
}
let _ = is_write;
let data = rax.to_le_bytes();
return Ok(VmExit::IoOut(port, data[..access_size].to_vec()));
},
WHvRunVpExitReasonX64Halt => {
#[cfg(feature = "hw-interrupts")]
if self.timer.as_ref().is_some_and(|t| t.is_active()) {
continue;
}
return Ok(VmExit::Halt());
}
WHvRunVpExitReasonMemoryAccess => {
let gpa = unsafe { exit_context.Anonymous.MemoryAccess.Gpa };
let access_info = unsafe {
WHV_MEMORY_ACCESS_TYPE(
(exit_context.Anonymous.MemoryAccess.AccessInfo.AsUINT32 & 0b11) as i32,
)
};
let access_info = MemoryRegionFlags::try_from(access_info)
.map_err(|_| RunVcpuError::ParseGpaAccessInfo)?;
return match access_info {
MemoryRegionFlags::READ => Ok(VmExit::MmioRead(gpa)),
MemoryRegionFlags::WRITE => Ok(VmExit::MmioWrite(gpa)),
_ => Ok(VmExit::Unknown("Unknown memory access type".to_string())),
};
}
WHvRunVpExitReasonCanceled => {
return Ok(VmExit::Cancelled());
}
#[cfg(gdb)]
WHvRunVpExitReasonException => {
let exception = unsafe { exit_context.Anonymous.VpException };
let dr6 = {
let names = [WHvX64RegisterDr6];
let mut out: [Align16<WHV_REGISTER_VALUE>; 1] =
unsafe { std::mem::zeroed() };
self.get_registers(&names, &mut out)
.map_err(|e| RunVcpuError::GetDr6(e.into()))?;
unsafe { out[0].0.Reg64 }
};
return Ok(VmExit::Debug {
dr6,
exception: exception.ExceptionType as u32,
});
}
WHV_RUN_VP_EXIT_REASON(_) => {
let rip = exit_context.VpContext.Rip;
tracing::error!(
"WHP unknown exit reason {}: RIP={:#x}",
exit_context.ExitReason.0,
rip,
);
if let Ok(regs) = self.regs() {
tracing::error!(
" RAX={:#x} RCX={:#x} RDX={:#x}",
regs.rax,
regs.rcx,
regs.rdx
);
}
if let Ok(sregs) = self.sregs() {
tracing::error!(
" CR0={:#x} CR4={:#x} EFER={:#x} APIC_BASE={:#x}",
sregs.cr0,
sregs.cr4,
sregs.efer,
sregs.apic_base
);
}
return Ok(VmExit::Unknown(format!(
"Unknown exit reason '{}' at RIP={:#x}",
exit_context.ExitReason.0, rip
)));
}
}
}
}
fn regs(&self) -> std::result::Result<CommonRegisters, RegisterError> {
let mut whv_regs_values: [Align16<WHV_REGISTER_VALUE>; WHP_REGS_NAMES_LEN] =
unsafe { std::mem::zeroed() };
self.get_registers(&WHP_REGS_NAMES, &mut whv_regs_values)
.map_err(|e| RegisterError::GetRegs(e.into()))?;
WHP_REGS_NAMES
.into_iter()
.zip(whv_regs_values)
.collect::<Vec<(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>)>>()
.as_slice()
.try_into()
.map_err(|e| {
RegisterError::ConversionFailed(format!(
"Failed to convert WHP registers to CommonRegisters: {:?}",
e
))
})
}
fn set_regs(&mut self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError> {
let whp_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_REGS_NAMES_LEN] =
regs.into();
self.set_registers(&whp_regs)
.map_err(|e| RegisterError::SetRegs(e.into()))?;
Ok(())
}
fn fpu(&self) -> std::result::Result<CommonFpu, RegisterError> {
let mut whp_fpu_values: [Align16<WHV_REGISTER_VALUE>; WHP_FPU_NAMES_LEN] =
unsafe { std::mem::zeroed() };
self.get_registers(&WHP_FPU_NAMES, &mut whp_fpu_values)
.map_err(|e| RegisterError::GetFpu(e.into()))?;
WHP_FPU_NAMES
.into_iter()
.zip(whp_fpu_values)
.collect::<Vec<(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>)>>()
.as_slice()
.try_into()
.map_err(|e| {
RegisterError::ConversionFailed(format!(
"Failed to convert WHP registers to CommonFpu: {:?}",
e
))
})
}
fn set_fpu(&mut self, fpu: &CommonFpu) -> std::result::Result<(), RegisterError> {
let whp_fpu: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_FPU_NAMES_LEN] =
fpu.into();
self.set_registers(&whp_fpu)
.map_err(|e| RegisterError::SetFpu(e.into()))?;
Ok(())
}
fn sregs(&self) -> std::result::Result<CommonSpecialRegisters, RegisterError> {
let mut whp_sregs_values: [Align16<WHV_REGISTER_VALUE>; WHP_SREGS_NAMES_LEN] =
unsafe { std::mem::zeroed() };
self.get_registers(&WHP_SREGS_NAMES, &mut whp_sregs_values)
.map_err(|e| RegisterError::GetSregs(e.into()))?;
WHP_SREGS_NAMES
.into_iter()
.zip(whp_sregs_values)
.collect::<Vec<(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>)>>()
.as_slice()
.try_into()
.map_err(|e| {
RegisterError::ConversionFailed(format!(
"Failed to convert WHP registers to CommonSpecialRegisters: {:?}",
e
))
})
}
fn set_sregs(
&mut self,
sregs: &CommonSpecialRegisters,
) -> std::result::Result<(), RegisterError> {
let whp_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_SREGS_NAMES_LEN] =
sregs.into();
#[cfg(feature = "hw-interrupts")]
{
let filtered: Vec<_> = whp_regs
.iter()
.copied()
.filter(|(name, _)| *name != WHvX64RegisterApicBase)
.collect();
self.set_registers(&filtered)
.map_err(|e| RegisterError::SetSregs(e.into()))?;
Ok(())
}
#[cfg(not(feature = "hw-interrupts"))]
{
self.set_registers(&whp_regs)
.map_err(|e| RegisterError::SetSregs(e.into()))?;
Ok(())
}
}
fn msrs(&self, indices: &[u32]) -> std::result::Result<Vec<MsrEntry>, RegisterError> {
if indices.is_empty() {
return Ok(Vec::new());
}
let names: Vec<WHV_REGISTER_NAME> = indices
.iter()
.map(|&i| msr_to_whv_register_name(i).ok_or(RegisterError::MsrsUnsupported))
.collect::<std::result::Result<_, _>>()?;
let mut values: Vec<Align16<WHV_REGISTER_VALUE>> =
vec![unsafe { std::mem::zeroed() }; names.len()];
self.get_registers(&names, &mut values)
.map_err(|e| RegisterError::GetMsrs(e.into()))?;
Ok(indices
.iter()
.zip(values)
.map(|(&index, v)| MsrEntry {
index,
value: unsafe { v.0.Reg64 },
})
.collect())
}
fn set_msrs(&self, msrs: &[MsrEntry]) -> std::result::Result<(), RegisterError> {
let regs: Vec<(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>)> = msrs
.iter()
.map(|e| {
msr_to_whv_register_name(e.index)
.map(|name| (name, Align16(WHV_REGISTER_VALUE { Reg64: e.value })))
.ok_or(RegisterError::MsrsUnsupported)
})
.collect::<std::result::Result<_, _>>()?;
if regs.is_empty() {
return Ok(());
}
self.set_registers(®s)
.map_err(|e| RegisterError::SetMsrs(e.into()))?;
Ok(())
}
fn msr_reset_indices(
&self,
guest_msrs: &[u32],
) -> std::result::Result<Vec<u32>, CreateVmError> {
crate::hypervisor::virtual_machine::hyperv_msr_reset_indices(self, guest_msrs)
}
fn debug_regs(&self) -> std::result::Result<CommonDebugRegs, RegisterError> {
let mut whp_debug_regs_values: [Align16<WHV_REGISTER_VALUE>; WHP_DEBUG_REGS_NAMES_LEN] =
Default::default();
self.get_registers(&WHP_DEBUG_REGS_NAMES, &mut whp_debug_regs_values)
.map_err(|e| RegisterError::GetDebugRegs(e.into()))?;
let whp_debug_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>);
WHP_DEBUG_REGS_NAMES_LEN] =
std::array::from_fn(|i| (WHP_DEBUG_REGS_NAMES[i], whp_debug_regs_values[i]));
whp_debug_regs.as_slice().try_into().map_err(|e| {
RegisterError::ConversionFailed(format!(
"Failed to convert WHP registers to CommonDebugRegs: {:?}",
e
))
})
}
fn set_debug_regs(&self, drs: &CommonDebugRegs) -> std::result::Result<(), RegisterError> {
let whp_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_DEBUG_REGS_NAMES_LEN] =
drs.into();
self.set_registers(&whp_regs)
.map_err(|e| RegisterError::SetDebugRegs(e.into()))?;
Ok(())
}
#[allow(dead_code)]
fn xsave(&self) -> std::result::Result<Vec<u8>, RegisterError> {
let mut buffer_size_needed: u32 = 0;
let result = unsafe {
WHvGetVirtualProcessorXsaveState(
self.partition,
0,
std::ptr::null_mut(),
0,
&mut buffer_size_needed,
)
};
if let Err(e) = result
&& e.code() != windows::Win32::Foundation::WHV_E_INSUFFICIENT_BUFFER
{
return Err(RegisterError::GetXsave(e.into()));
}
let mut xsave_buffer = vec![0u8; buffer_size_needed as usize];
let mut written_bytes = 0;
unsafe {
WHvGetVirtualProcessorXsaveState(
self.partition,
0,
xsave_buffer.as_mut_ptr() as *mut std::ffi::c_void,
buffer_size_needed,
&mut written_bytes,
)
}
.map_err(|e| RegisterError::GetXsave(e.into()))?;
if written_bytes != buffer_size_needed {
return Err(RegisterError::XsaveSizeMismatch {
expected: buffer_size_needed,
actual: written_bytes,
});
}
Ok(xsave_buffer)
}
fn reset_xsave(&self) -> std::result::Result<(), RegisterError> {
let mut buffer_size_needed: u32 = 0;
let result = unsafe {
WHvGetVirtualProcessorXsaveState(
self.partition,
0,
std::ptr::null_mut(),
0,
&mut buffer_size_needed,
)
};
if let Err(e) = result
&& e.code() != windows::Win32::Foundation::WHV_E_INSUFFICIENT_BUFFER
{
return Err(RegisterError::GetXsaveSize(e.into()));
}
if buffer_size_needed < XSAVE_MIN_SIZE as u32 {
return Err(RegisterError::XsaveSizeMismatch {
expected: XSAVE_MIN_SIZE as u32,
actual: buffer_size_needed,
});
}
let mut current_state = vec![0u8; buffer_size_needed as usize];
let mut written_bytes = 0;
unsafe {
WHvGetVirtualProcessorXsaveState(
self.partition,
0,
current_state.as_mut_ptr() as *mut std::ffi::c_void,
buffer_size_needed,
&mut written_bytes,
)
.map_err(|e| RegisterError::GetXsave(e.into()))?;
};
current_state[0..520].fill(0);
current_state[528..].fill(0);
current_state[0..2].copy_from_slice(&FP_CONTROL_WORD_DEFAULT.to_le_bytes());
current_state[24..28].copy_from_slice(&MXCSR_DEFAULT.to_le_bytes());
current_state[512..520].copy_from_slice(&0x3u64.to_le_bytes());
unsafe {
WHvSetVirtualProcessorXsaveState(
self.partition,
0,
current_state.as_ptr() as *const std::ffi::c_void,
buffer_size_needed,
)
.map_err(|e| RegisterError::SetXsave(e.into()))?;
}
Ok(())
}
#[cfg(test)]
fn xcr0(&self) -> std::result::Result<u64, RegisterError> {
let mut values = [Align16(WHV_REGISTER_VALUE::default())];
self.get_registers(&[WHvX64RegisterXCr0], &mut values)
.map_err(|e| RegisterError::GetXcrs(e.into()))?;
Ok(unsafe { values[0].0.Reg64 })
}
fn set_xcr0(&self, value: u64) -> std::result::Result<(), RegisterError> {
self.set_registers(&[(
WHvX64RegisterXCr0,
Align16(WHV_REGISTER_VALUE { Reg64: value }),
)])
.map_err(|e| RegisterError::SetXcrs(e.into()))
}
fn can_batch_registers(&self) -> bool {
true
}
fn set_batched_registers(
&mut self,
regs: &CommonRegisters,
debug_regs: &CommonDebugRegs,
sregs: &CommonSpecialRegisters,
xcr0: u64,
msrs: &[MsrEntry],
) -> std::result::Result<(), RegisterError> {
let regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_REGS_NAMES_LEN] =
regs.into();
let debug_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>);
WHP_DEBUG_REGS_NAMES_LEN] = debug_regs.into();
let sregs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_SREGS_NAMES_LEN] =
sregs.into();
let msrs: Vec<_> = msrs
.iter()
.map(|entry| {
msr_to_whv_register_name(entry.index)
.map(|name| (name, Align16(WHV_REGISTER_VALUE { Reg64: entry.value })))
.ok_or(RegisterError::MsrsUnsupported)
})
.collect::<std::result::Result<_, _>>()?;
let mut registers =
Vec::with_capacity(regs.len() + debug_regs.len() + sregs.len() + 1 + msrs.len());
registers.extend(regs);
registers.extend(debug_regs);
#[cfg(feature = "hw-interrupts")]
registers.extend(
sregs
.into_iter()
.filter(|(name, _)| *name != WHvX64RegisterApicBase),
);
#[cfg(not(feature = "hw-interrupts"))]
registers.extend(sregs);
registers.push((
WHvX64RegisterXCr0,
Align16(WHV_REGISTER_VALUE { Reg64: xcr0 }),
));
registers.extend(msrs);
self.set_registers(®isters)
.map_err(|error| RegisterError::SetBatchedRegisters(error.into()))
}
#[cfg(test)]
fn set_xsave(&self, xsave: &[u32]) -> std::result::Result<(), RegisterError> {
let mut buffer_size_needed: u32 = 0;
let result = unsafe {
WHvGetVirtualProcessorXsaveState(
self.partition,
0,
std::ptr::null_mut(),
0,
&mut buffer_size_needed,
)
};
if let Err(e) = result
&& e.code() != windows::Win32::Foundation::WHV_E_INSUFFICIENT_BUFFER
{
return Err(RegisterError::GetXsaveSize(e.into()));
}
let provided_size = std::mem::size_of_val(xsave) as u32;
if provided_size != buffer_size_needed {
return Err(RegisterError::XsaveSizeMismatch {
expected: buffer_size_needed,
actual: provided_size,
});
}
unsafe {
WHvSetVirtualProcessorXsaveState(
self.partition,
0,
xsave.as_ptr() as *const std::ffi::c_void,
buffer_size_needed,
)
.map_err(|e| RegisterError::SetXsave(e.into()))?;
}
Ok(())
}
fn partition_handle(&self) -> WHV_PARTITION_HANDLE {
self.partition
}
}
#[cfg(gdb)]
impl DebuggableVm for WhpVm {
fn translate_gva(&self, gva: u64) -> std::result::Result<u64, DebugError> {
let mut gpa = 0;
let mut result = WHV_TRANSLATE_GVA_RESULT::default();
let translateflags = WHvTranslateGvaFlagValidateRead;
unsafe {
WHvTranslateGva(
self.partition,
0,
gva,
translateflags,
&mut result,
&mut gpa,
)
.map_err(|_| DebugError::TranslateGva(gva))?;
}
Ok(gpa)
}
fn set_debug(&mut self, enable: bool) -> std::result::Result<(), DebugError> {
let extended_vm_exits = if enable { 1 << 2 } else { 0 };
let exception_exit_bitmap = if enable {
(1 << WHvX64ExceptionTypeDebugTrapOrFault.0)
| (1 << WHvX64ExceptionTypeBreakpointTrap.0)
} else {
0
};
let properties = [
(
WHvPartitionPropertyCodeExtendedVmExits,
WHV_PARTITION_PROPERTY {
ExtendedVmExits: WHV_EXTENDED_VM_EXITS {
AsUINT64: extended_vm_exits,
},
},
),
(
WHvPartitionPropertyCodeExceptionExitBitmap,
WHV_PARTITION_PROPERTY {
ExceptionExitBitmap: exception_exit_bitmap,
},
),
];
for (code, property) in properties {
unsafe {
WHvSetPartitionProperty(
self.partition,
code,
&property as *const _ as *const c_void,
std::mem::size_of::<WHV_PARTITION_PROPERTY>() as u32,
)
.map_err(|e| DebugError::Intercept {
enable,
inner: e.into(),
})?;
}
}
Ok(())
}
fn set_single_step(&mut self, enable: bool) -> std::result::Result<(), DebugError> {
let mut regs = self.regs()?;
if enable {
regs.rflags |= 1 << 8;
} else {
regs.rflags &= !(1 << 8);
}
self.set_regs(®s)?;
Ok(())
}
fn add_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> {
use crate::hypervisor::gdb::arch::MAX_NO_OF_HW_BP;
let mut regs = self.debug_regs()?;
if [regs.dr0, regs.dr1, regs.dr2, regs.dr3].contains(&addr) {
return Ok(());
}
let i = (0..MAX_NO_OF_HW_BP)
.position(|i| regs.dr7 & (1 << (i * 2)) == 0)
.ok_or(DebugError::TooManyHwBreakpoints(MAX_NO_OF_HW_BP))?;
*[&mut regs.dr0, &mut regs.dr1, &mut regs.dr2, &mut regs.dr3][i] = addr;
regs.dr7 |= 1 << (i * 2);
self.set_debug_regs(®s)?;
Ok(())
}
fn remove_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> {
let mut debug_regs = self.debug_regs()?;
let regs = [
&mut debug_regs.dr0,
&mut debug_regs.dr1,
&mut debug_regs.dr2,
&mut debug_regs.dr3,
];
if let Some(i) = regs.iter().position(|&&mut reg| reg == addr) {
*regs[i] = 0;
debug_regs.dr7 &= !(1 << (i * 2));
self.set_debug_regs(&debug_regs)?;
Ok(())
} else {
Err(DebugError::HwBreakpointNotFound(addr))
}
}
}
#[cfg(feature = "hw-interrupts")]
impl WhpVm {
const LAPIC_STATE_MAX_SIZE: u32 = 4096;
fn check_lapic_emulation_support() -> Result<(), CreateVmError> {
const LAPIC_EMULATION_BIT: u64 = 1 << 1;
let mut capability: WHV_CAPABILITY = Default::default();
let has_lapic = unsafe {
WHvGetCapability(
WHvCapabilityCodeFeatures,
&mut capability as *mut _ as *mut c_void,
std::mem::size_of::<WHV_CAPABILITY>() as u32,
None,
)
.is_ok()
&& (capability.Features.AsUINT64 & LAPIC_EMULATION_BIT != 0)
};
if !has_lapic {
return Err(CreateVmError::InitializeVm(
windows_result::Error::new(
HRESULT::from_win32(0x32), "WHP LocalApicEmulation capability is required for hw-interrupts",
)
.into(),
));
}
Ok(())
}
fn enable_lapic_emulation(partition: WHV_PARTITION_HANDLE) -> Result<(), CreateVmError> {
let apic_mode = WHvX64LocalApicEmulationModeXApic;
unsafe {
WHvSetPartitionProperty(
partition,
WHvPartitionPropertyCodeLocalApicEmulationMode,
&apic_mode as *const _ as *const _,
std::mem::size_of_val(&apic_mode) as _,
)
.map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?;
}
Ok(())
}
unsafe fn init_lapic_bulk(partition: WHV_PARTITION_HANDLE) -> windows_result::Result<()> {
let mut state = vec![0u8; Self::LAPIC_STATE_MAX_SIZE as usize];
let mut written: u32 = 0;
unsafe {
WHvGetVirtualProcessorInterruptControllerState2(
partition,
0,
state.as_mut_ptr() as *mut c_void,
Self::LAPIC_STATE_MAX_SIZE,
Some(&mut written),
)?;
}
state.truncate(written as usize);
const MIN_LAPIC_STATE_SIZE: usize = 0x374;
if state.len() < MIN_LAPIC_STATE_SIZE {
return Err(windows_result::Error::new(
HRESULT::from_win32(0x32), "WHP LAPIC state buffer is too small for init_lapic_registers",
));
}
super::x86_64::hw_interrupts::init_lapic_registers(&mut state);
unsafe {
WHvSetVirtualProcessorInterruptControllerState2(
partition,
0,
state.as_ptr() as *const c_void,
state.len() as u32,
)?;
}
Ok(())
}
fn get_lapic_state(&self) -> windows_result::Result<Vec<u8>> {
let mut state = vec![0u8; Self::LAPIC_STATE_MAX_SIZE as usize];
let mut written: u32 = 0;
unsafe {
WHvGetVirtualProcessorInterruptControllerState2(
self.partition,
0,
state.as_mut_ptr() as *mut c_void,
Self::LAPIC_STATE_MAX_SIZE,
Some(&mut written),
)?;
}
state.truncate(written as usize);
Ok(state)
}
fn set_lapic_state(&self, state: &[u8]) -> windows_result::Result<()> {
unsafe {
WHvSetVirtualProcessorInterruptControllerState2(
self.partition,
0,
state.as_ptr() as *const c_void,
state.len() as u32,
)
}
}
fn do_lapic_eoi(&self) {
if let Ok(mut state) = self.get_lapic_state() {
super::x86_64::hw_interrupts::lapic_eoi(&mut state);
if let Err(e) = self.set_lapic_state(&state) {
tracing::warn!("WHP set_lapic_state (EOI) failed: {e}");
}
}
}
fn handle_hw_io_out(&mut self, port: u16, data: &[u8]) -> bool {
if port == VmAction::PvTimerConfig as u16 {
let partition_raw = self.partition.0;
let vector = super::x86_64::hw_interrupts::TIMER_VECTOR;
super::x86_64::hw_interrupts::handle_pv_timer_config(
&mut self.timer,
data,
move || {
let partition = WHV_PARTITION_HANDLE(partition_raw);
let interrupt = WHV_INTERRUPT_CONTROL {
_bitfield: 0, Destination: 0,
Vector: vector,
};
let _ = unsafe {
WHvRequestInterrupt(
partition,
&interrupt,
std::mem::size_of::<WHV_INTERRUPT_CONTROL>() as u32,
)
};
},
);
return true;
}
let timer_active = self.timer.as_ref().is_some_and(|t| t.is_active());
super::x86_64::hw_interrupts::handle_common_io_out(port, data, timer_active, || {
self.do_lapic_eoi()
})
}
}
impl Drop for WhpVm {
fn drop(&mut self) {
for (handle, view) in self.file_mappings.drain(..) {
release_file_mapping(view, handle);
}
#[cfg(feature = "hw-interrupts")]
if let Some(mut t) = self.timer.take() {
t.stop();
}
if let Err(e) = unsafe { WHvDeletePartition(self.partition) } {
tracing::error!("Failed to delete partition: {}", e);
}
}
}
type WHvMapGpaRange2Func = unsafe extern "C" fn(
WHV_PARTITION_HANDLE,
HANDLE,
*const c_void,
u64,
u64,
WHV_MAP_GPA_RANGE_FLAGS,
) -> HRESULT;
unsafe fn try_load_whv_map_gpa_range2() -> windows_result::Result<WHvMapGpaRange2Func> {
let library = unsafe {
LoadLibraryExA(
s!("winhvplatform.dll"),
None,
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
)
}?;
let address = unsafe { GetProcAddress(library, s!("WHvMapGpaRange2")) };
if address.is_none() {
unsafe { FreeLibrary(library)? };
return Err(windows_result::Error::new(
HRESULT::from_win32(127), "Failed to find WHvMapGpaRange2 in winhvplatform.dll",
));
}
unsafe { Ok(std::mem::transmute_copy(&address)) }
}
#[cfg(test)]
mod no_surrogate_tests {
use super::*;
#[test]
fn single_vm_lifecycle() {
if !surrogates_disabled() {
eprintln!("SKIP: HYPERLIGHT_MAX_SURROGATES != 0");
return;
}
NO_SURROGATE_VM_ACTIVE.store(false, Ordering::SeqCst);
let vm1 = WhpVm::new();
assert!(vm1.is_ok(), "first VM should succeed: {:?}", vm1.err());
let vm2 = WhpVm::new();
assert!(vm2.is_err(), "second concurrent VM should be rejected");
let err_msg = format!("{:?}", vm2.unwrap_err());
assert!(
err_msg.contains("single VM"),
"error should mention single-VM constraint, got: {err_msg}"
);
drop(vm1);
let vm3 = WhpVm::new();
assert!(
vm3.is_ok(),
"sequential VM after drop should succeed: {:?}",
vm3.err()
);
}
}
#[cfg(test)]
#[cfg(feature = "hw-interrupts")]
mod hw_interrupt_tests {
use super::*;
#[test]
fn lapic_register_helpers_delegate() {
use crate::hypervisor::virtual_machine::x86_64::hw_interrupts;
let mut state = vec![0u8; 1024];
hw_interrupts::write_lapic_u32(&mut state, 0xF0, 0x1FF);
assert_eq!(hw_interrupts::read_lapic_u32(&state, 0xF0), 0x1FF);
}
#[test]
fn check_lapic_emulation_capability() {
let mut capability: WHV_CAPABILITY = Default::default();
let result = unsafe {
WHvGetCapability(
WHvCapabilityCodeFeatures,
&mut capability as *mut _ as *mut std::os::raw::c_void,
std::mem::size_of::<WHV_CAPABILITY>() as u32,
None,
)
};
assert!(
result.is_ok(),
"WHvGetCapability(Features) failed: {result:?}"
);
let raw = unsafe { capability.Features.AsUINT64 };
let has_lapic = raw & (1 << 1) != 0; assert!(
has_lapic,
"This host does not support WHP LocalApicEmulation. \
hw-interrupts requires Windows 11 22H2+ or a recent Windows Server build."
);
}
}