From 09b12b35b99241f0c853c41b76e3948b26f042e2 Mon Sep 17 00:00:00 2001 From: lstocchi Date: Tue, 26 May 2026 00:12:46 +0200 Subject: [PATCH] whp: add Hyper-V enlightenment support and invariant TSC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure the WHP partition to expose Hyper-V enlightenments to the guest, enabling it to use synthetic MSRs for timekeeping and hypercalls instead of expensive VM exits on native hardware interfaces. - Advertise Hyper-V CPUID leaves 0x40000000–0x40000006 (signature, interface ID, version, feature identification, recommendations, implementation limits, and hardware features). - Enable synthetic processor features: HypervisorPresent, Hv1, reference TSC, reference counter, frequency registers, hypercalls, and VP index/runtime access. - Set the MSR exit bitmap so unhandled MSR accesses trigger a VM exit rather than injecting #GP into the guest. - Retrieve and re-apply processor features banks with the invariant TSC bit set, and expose it via CPUID 0x80000007. - Register CPUID leaf 1 in the exit list so the hypervisor-present bit (ECX.31) can be injected, which is required for the guest to query the 0x40000000+ range. Assisted-by: Cursor:claude-opus-4.6 Signed-off-by: lstocchi --- src/whp/src/lib.rs | 220 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 198 insertions(+), 22 deletions(-) diff --git a/src/whp/src/lib.rs b/src/whp/src/lib.rs index 268e5519e..e2aab1963 100644 --- a/src/whp/src/lib.rs +++ b/src/whp/src/lib.rs @@ -11,14 +11,17 @@ use std::sync::Arc; use log::{debug, error}; use windows_sys::Win32::Foundation::S_OK; use windows_sys::Win32::System::Hypervisor::{ - WHvCancelRunVirtualProcessor, WHvCapabilityCodeHypervisorPresent, WHvCreatePartition, - WHvCreateVirtualProcessor, WHvDeletePartition, WHvDeleteVirtualProcessor, - WHvEmulatorCreateEmulator, WHvEmulatorDestroyEmulator, WHvEmulatorTryIoEmulation, - WHvEmulatorTryMmioEmulation, WHvGetCapability, WHvGetVirtualProcessorRegisters, WHvMapGpaRange, - WHvMapGpaRangeFlagExecute, WHvMapGpaRangeFlagRead, WHvMapGpaRangeFlagWrite, + WHvCancelRunVirtualProcessor, WHvCapabilityCodeHypervisorPresent, + WHvCapabilityCodeProcessorFeaturesBanks, WHvCreatePartition, WHvCreateVirtualProcessor, + WHvDeletePartition, WHvDeleteVirtualProcessor, WHvEmulatorCreateEmulator, + WHvEmulatorDestroyEmulator, WHvEmulatorTryIoEmulation, WHvEmulatorTryMmioEmulation, + WHvGetCapability, WHvGetVirtualProcessorRegisters, WHvMapGpaRange, WHvMapGpaRangeFlagExecute, + WHvMapGpaRangeFlagRead, WHvMapGpaRangeFlagWrite, WHvPartitionPropertyCodeCpuidExitList, WHvPartitionPropertyCodeCpuidResultList, WHvPartitionPropertyCodeExtendedVmExits, WHvPartitionPropertyCodeLocalApicEmulationMode, WHvPartitionPropertyCodeProcessorCount, - WHvRegisterInternalActivityState, WHvRequestInterrupt, WHvRunVirtualProcessor, + WHvPartitionPropertyCodeProcessorFeaturesBanks, + WHvPartitionPropertyCodeSyntheticProcessorFeaturesBanks, + WHvPartitionPropertyCodeX64MsrExitBitmap, WHvRequestInterrupt, WHvRunVirtualProcessor, WHvRunVpExitReasonCanceled, WHvRunVpExitReasonInvalidVpRegisterValue, WHvRunVpExitReasonMemoryAccess, WHvRunVpExitReasonUnrecoverableException, WHvRunVpExitReasonUnsupportedFeature, WHvRunVpExitReasonX64Cpuid, WHvRunVpExitReasonX64Halt, @@ -28,9 +31,9 @@ use windows_sys::Win32::System::Hypervisor::{ WHvX64RegisterDeliverabilityNotifications, WHvX64RegisterRax, WHvX64RegisterRbx, WHvX64RegisterRcx, WHvX64RegisterRdx, WHvX64RegisterRip, WHV_CAPABILITY, WHV_EMULATOR_CALLBACKS, WHV_EMULATOR_STATUS, WHV_MEMORY_ACCESS_CONTEXT, WHV_PARTITION_HANDLE, - WHV_PARTITION_PROPERTY, WHV_PARTITION_PROPERTY_CODE, WHV_REGISTER_NAME, WHV_REGISTER_VALUE, - WHV_RUN_VP_EXIT_CONTEXT, WHV_VP_EXIT_CONTEXT, WHV_X64_CPUID_RESULT, - WHV_X64_IO_PORT_ACCESS_CONTEXT, + WHV_PARTITION_PROPERTY, WHV_PARTITION_PROPERTY_CODE, WHV_PROCESSOR_FEATURES_BANKS, + WHV_REGISTER_NAME, WHV_REGISTER_VALUE, WHV_RUN_VP_EXIT_CONTEXT, WHV_VP_EXIT_CONTEXT, + WHV_X64_CPUID_RESULT, WHV_X64_IO_PORT_ACCESS_CONTEXT, }; use windows_sys::Win32::System::Performance::{QueryPerformanceCounter, QueryPerformanceFrequency}; @@ -153,6 +156,26 @@ pub fn check_hypervisor() -> Result<(), Error> { Ok(()) } +fn get_processor_features_banks() -> Result { + let mut capability = MaybeUninit::::uninit(); + let mut written_size: u32 = 0; + + unsafe { + let hr = WHvGetCapability( + WHvCapabilityCodeProcessorFeaturesBanks, + capability.as_mut_ptr().cast(), + mem::size_of::() as u32, + &mut written_size, + ); + + if hr != S_OK { + return Err(Error::CheckCapability(hr)); + } + + Ok(capability.assume_init().ProcessorFeaturesBanks) + } +} + /// Parsed CPUID exit context returned by [`WhpVcpu::cpuid_exit_info`]. #[derive(Debug, Clone)] pub struct CpuidExitInfo { @@ -266,8 +289,148 @@ impl WhpVm { p.ExtendedVmExits.AsUINT64 = 0b11; // bit 0 = X64CpuidExit, bit 1 = X64MsrExit })?; + // Configure how MSRs are handled + // We just set the bit 0 (UnhandledMsrs) so that any MSR read/write does not automatically fail + // but it triggers an exit that we can handle + Self::set_property(handle, WHvPartitionPropertyCodeX64MsrExitBitmap, |p| { + p.X64MsrExitBitmap.AsUINT64 = 0b01; // bit 0 = UnhandledMsrs + })?; + + // Set invariant TSC support + // First we need to retrieve the processor features banks and re-set them with the invariant TSC support + // otherwise they get lost + let processor_features_banks = get_processor_features_banks()?; + Self::set_property( + handle, + WHvPartitionPropertyCodeProcessorFeaturesBanks, + |p| { + p.ProcessorFeaturesBanks = processor_features_banks; + + unsafe { + if p.ProcessorFeaturesBanks.BanksCount < 2 { + p.ProcessorFeaturesBanks.BanksCount = 2; + } + + p.ProcessorFeaturesBanks.Anonymous.AsUINT64[1] |= 0x2; // TscInvariantSupport + } + }, + )?; + + // This unlocks the MSRs you are advertising in CPUID. + Self::set_property( + handle, + WHvPartitionPropertyCodeSyntheticProcessorFeaturesBanks, + |p| { + p.SyntheticProcessorFeaturesBanks.BanksCount = 1; + // We use bitwise OR on the AsUINT64 array of the union for safety/clarity. + // Bit 0: HypervisorPresent + // Bit 1: Hv1 (Report support for Hv1: CPUID leaves 0x40000000 - 0x40000006) + // Bit 2: AccessVpRunTimeReg + // Bit 3: AccessPartitionReferenceCounter + // Bit 7: Hypercalls + // Bit 8: AccessVpIndex + // Bit 9: AccessPartitionReferenceTsc + // Bit 11: AccessFrequencyRegs + unsafe { + p.SyntheticProcessorFeaturesBanks.Anonymous.AsUINT64[0] = 0xB8F; + } + }, + )?; + let mut cpuid_results: Vec = Vec::new(); + // WHP does NOT expose Hyper-V CPUID to the guest automatically; + // we must provide 0x40000000+ via CpuidResultList. + // More info on Hypervisor Top Level Functional Specification + // https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/tlfs + + // 0x40000000 — Hypervisor signature: "Microsoft Hv" + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x40000000, + Reserved: [0; 3], + Eax: 0x40000006, + Ebx: 0x7263694D, // "Micr" + Ecx: 0x666F736F, // "osof" + Edx: 0x76482074, // "t Hv" + }); + // 0x40000001 — Interface identification: "Hv#1" + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x40000001, + Reserved: [0; 3], + Eax: 0x31237648, // "Hv#1" + Ebx: 0, + Ecx: 0, + Edx: 0, + }); + // 0x40000002 — Version (minimal) + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x40000002, + Reserved: [0; 3], + Eax: 0x3839, // build number + Ebx: 0xa0000, // version + Ecx: 0, + Edx: 0, + }); + // 0x40000003 — Feature identification (Hyper-V TLFS §2.4) + const ACCESS_VP_RUNTIME: u32 = 1 << 0; + const ACCESS_REF_COUNTER: u32 = 1 << 1; + const ACCESS_HYPERCALLS: u32 = 1 << 5; + const ACCESS_VP_INDEX: u32 = 1 << 6; + const ACCESS_REF_TSC: u32 = 1 << 9; + const ACCESS_FREQ_REGS: u32 = 1 << 11; + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x40000003, + Reserved: [0; 3], + Eax: ACCESS_VP_RUNTIME + | ACCESS_REF_COUNTER + | ACCESS_HYPERCALLS + | ACCESS_VP_INDEX + | ACCESS_REF_TSC + | ACCESS_FREQ_REGS, + Ebx: 0, + Ecx: 0, + Edx: 0, + }); + // 0x40000004 — Recommendations + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x40000004, + Reserved: [0; 3], + Eax: 1 << 5, // RelaxedTiming + Ebx: 0, + Ecx: 0, + Edx: 0, + }); + // 0x40000005 — Implementation limits + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x40000005, + Reserved: [0; 3], + Eax: 64, // max virtual processors + Ebx: 0, + Ecx: 0, + Edx: 0, + }); + // 0x40000006: Hardware Features + const HV_MSR_BITMAPS: u32 = 1 << 1; + const HV_SLAT: u32 = 1 << 3; + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x40000006, + Reserved: [0; 3], + Eax: HV_MSR_BITMAPS | HV_SLAT, + Ebx: 0, + Ecx: 0, + Edx: 0, + }); + + // invariant tsc + cpuid_results.push(WHV_X64_CPUID_RESULT { + Function: 0x80000007, + Reserved: [0; 3], + Eax: 0, + Ebx: 0, + Ecx: 0, + Edx: 0x100, // bit 8 (Invariant TSC / nonstop_tsc) + }); + // Standard Intel CPUID leaves (Intel's SDM Vol. 2A) if tsc_freq_hz > 0 { debug!("Providing TSC frequency to guest: {} Hz", tsc_freq_hz); @@ -292,19 +455,32 @@ impl WhpVm { }); } - if !cpuid_results.is_empty() { - let hr = unsafe { - WHvSetPartitionProperty( - handle, - WHvPartitionPropertyCodeCpuidResultList, - cpuid_results.as_ptr() as *const _, - (cpuid_results.len() * mem::size_of::()) as u32, - ) - }; - if hr != S_OK { - return Err(Error::SetPartitionProperty(hr)); - } - debug!("CpuidResultList set ({} entries)", cpuid_results.len()); + // Force CPUID leaf 1 to exit so we can set the hypervisor-present + // bit (ECX.31). Without it the guest ignores 0x40000000+ leaves. + let cpuid_exit_list: [u32; 1] = [1]; + let hr = unsafe { + WHvSetPartitionProperty( + handle, + WHvPartitionPropertyCodeCpuidExitList, + cpuid_exit_list.as_ptr() as *const _, + (cpuid_exit_list.len() * mem::size_of::()) as u32, + ) + }; + + if hr != S_OK { + return Err(Error::SetPartitionProperty(hr)); + } + + let hr = unsafe { + WHvSetPartitionProperty( + handle, + WHvPartitionPropertyCodeCpuidResultList, + cpuid_results.as_ptr() as *const _, + (cpuid_results.len() * mem::size_of::()) as u32, + ) + }; + if hr != S_OK { + return Err(Error::SetPartitionProperty(hr)); } let hr = unsafe { WHvSetupPartition(handle) };