Skip to content

Commit fd36a31

Browse files
committed
hypervisor: kvm: enable Enlightened VMCS for nested Hyper-V guests
A Windows guest that itself runs Hyper-V (nested Hyper-V, or WSL2 which is built on it) acts as an L1 hypervisor on top of KVM. On Intel, Hyper-V does not drive KVM through the architectural VMCS — it uses Microsoft's Enlightened VMCS (eVMCS) layout, a paravirtualized L0(KVM)<->L1(Hyper-V) protocol. Cloud Hypervisor never negotiated eVMCS: it enabled only KVM_CAP_HYPERV_SYNIC and left the Hyper-V nested-features CPUID leaf (0x4000000A) zeroed. The guest's hvix64 therefore reaches VMXON with no eVMCS contract in place and the nested launch path stalls. Wire up the missing piece: - Add Vcpu::enable_hyperv_enlightened_vmcs(), a no-op by default (so MSHV, which handles nesting natively, and non-x86 backends are unaffected) and implemented on the KVM backend as KVM_ENABLE_CAP(KVM_CAP_HYPERV_ENLIGHTENED_VMCS). - In configure_vcpu(), when Hyper-V enlightenments + nested are both on and the vCPU is Intel, enable the eVMCS capability before set_cpuid2 (KVM only accepts leaf 0x4000000A after the cap is enabled) and then advertise: * 0x40000004.EAX bit 14 HV_ENLIGHTENED_VMCS_RECOMMENDED * 0x4000000A.EAX low 16 supported eVMCS version range (v1: 0x0101) bit 17 HV_NESTED_DIRECT_FLUSH bit 19 HV_NESTED_MSR_BITMAP Gated Intel-only because eVMCS is a VMX construct; AMD/SVM is untouched. Gated on `nested` so non-nested Windows guests are unchanged. Scope note: this is necessary but, in a deeply-nested environment (e.g. a cloud instance that is itself an L1 guest, making the Windows guest L2 and its WSL2/Hyper-V workload L3), may not be sufficient on its own — the host's own nesting depth can still be the limiting factor. On a bare-metal Intel KVM host this provides the eVMCS contract Hyper-V requires. Sources: Linux Documentation/virt/kvm/api.rst (KVM_CAP_HYPERV_ENLIGHTENED_VMCS; HYPERV_CPUID_NESTED_FEATURES exposed only after the cap is enabled) Microsoft Hypervisor Top-Level Functional Specification (eVMCS, HV_CPUID_NESTED_FEATURES) qemu/qemu docs/system/i386/hyperv.rst (hv-evmcs: Intel-only, nested-only)
1 parent a769777 commit fd36a31

3 files changed

Lines changed: 77 additions & 3 deletions

File tree

arch/src/x86_64/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ const TSC_DEADLINE_TIMER_ECX_BIT: u8 = 24; // tsc deadline timer ecx bit.
5151
const HYPERVISOR_ECX_BIT: u8 = 31; // Hypervisor ecx bit.
5252
const VMX_ECX_BIT: u8 = 5; // VMX for Intel
5353
const SVM_ECX_BIT: u8 = 2; // SVM for AMD
54+
55+
// Hyper-V Enlightened VMCS, for nested Hyper-V / WSL2 on Intel.
56+
// Recommended bit in HV_CPUID_ENLIGHTMENT_INFO (0x40000004) EAX.
57+
const HV_ENLIGHTENED_VMCS_RECOMMENDED_BIT: u32 = 14;
58+
// HV_CPUID_NESTED_FEATURES (0x4000000A) EAX: low 16 bits encode the supported
59+
// Enlightened VMCS version range; we report v1 only (low=high=1).
60+
const HV_ENLIGHTENED_VMCS_VERSION: u32 = 1;
61+
// HV_CPUID_NESTED_FEATURES (0x4000000A) EAX feature bits.
62+
const HV_NESTED_DIRECT_FLUSH_BIT: u32 = 17;
63+
const HV_NESTED_MSR_BITMAP_BIT: u32 = 19;
5464
const MTRR_EDX_BIT: u8 = 12; // Hypervisor ecx bit.
5565
const INVARIANT_TSC_EDX_BIT: u8 = 8; // Invariant TSC bit on 0x8000_0007 EDX
5666
const AMX_BF16: u8 = 22; // AMX tile computation on bfloat16 numbers
@@ -1000,6 +1010,33 @@ pub fn configure_vcpu(
10001010
}
10011011
}
10021012

1013+
// Enlightened VMCS lets a Hyper-V guest run as an L1 hypervisor (nested
1014+
// Hyper-V / WSL2) over KVM. It is Intel-only and only meaningful when both
1015+
// Hyper-V enlightenments and nested virtualization are enabled. KVM accepts
1016+
// the Hyper-V nested-features CPUID leaf (0x4000000A) only after the eVMCS
1017+
// capability has been enabled on the vCPU, so enable it before set_cpuid2.
1018+
let evmcs = kvm_hyperv && nested && matches!(cpu_vendor, CpuVendor::Intel);
1019+
if evmcs {
1020+
vcpu.enable_hyperv_enlightened_vmcs()
1021+
.map_err(|e| Error::SetSupportedCpusFailed(e.into()))?;
1022+
1023+
for entry in &mut cpuid {
1024+
match entry.function {
1025+
// HV_CPUID_ENLIGHTMENT_INFO: tell the guest eVMCS is recommended.
1026+
0x4000_0004 => entry.eax |= 1 << HV_ENLIGHTENED_VMCS_RECOMMENDED_BIT,
1027+
// HV_CPUID_NESTED_FEATURES: advertise the supported eVMCS version
1028+
// (low 16 bits) plus the nested enlightenments KVM implements.
1029+
0x4000_000a => {
1030+
entry.eax = HV_ENLIGHTENED_VMCS_VERSION
1031+
| (HV_ENLIGHTENED_VMCS_VERSION << 8)
1032+
| (1 << HV_NESTED_DIRECT_FLUSH_BIT)
1033+
| (1 << HV_NESTED_MSR_BITMAP_BIT);
1034+
}
1035+
_ => {}
1036+
}
1037+
}
1038+
}
1039+
10031040
for c in &cpuid {
10041041
debug!("{c}");
10051042
}

hypervisor/src/cpu.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ pub enum HypervisorCpuError {
212212
///
213213
#[error("Failed to enable HyperV SynIC")]
214214
EnableHyperVSyncIc(#[source] anyhow::Error),
215+
/// Enabling HyperV Enlightened VMCS error
216+
///
217+
#[error("Failed to enable HyperV Enlightened VMCS")]
218+
EnableHyperVEnlightenedVmcs(#[source] anyhow::Error),
215219
///
216220
/// Getting AArch64 core register error
217221
///
@@ -417,6 +421,19 @@ pub trait Vcpu: Send + Sync {
417421
fn enable_hyperv_synic(&self) -> Result<()>;
418422
#[cfg(target_arch = "x86_64")]
419423
///
424+
/// X86 specific call to enable HyperV Enlightened VMCS for nested guests.
425+
///
426+
/// Enlightened VMCS is the paravirtualized L0<->L1 protocol that a Hyper-V
427+
/// guest (acting as an L1 hypervisor, e.g. for WSL2 or nested Hyper-V) uses
428+
/// instead of the architectural VMCS layout. It is Intel-only. The default
429+
/// implementation is a no-op so backends that do not need it (or handle
430+
/// nesting natively, like MSHV) are unaffected.
431+
///
432+
fn enable_hyperv_enlightened_vmcs(&self) -> Result<()> {
433+
Ok(())
434+
}
435+
#[cfg(target_arch = "x86_64")]
436+
///
420437
/// X86 specific call to retrieve the CPUID registers.
421438
///
422439
fn get_cpuid2(&self, num_entries: usize) -> Result<Vec<CpuIdEntry>>;

hypervisor/src/kvm/mod.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ use crate::riscv64_reg_id;
6868
pub mod x86_64;
6969
#[cfg(target_arch = "x86_64")]
7070
use kvm_bindings::{
71-
KVM_CAP_HYPERV_SYNIC, KVM_CAP_HYPERV_SYNIC2, KVM_CAP_SPLIT_IRQCHIP, KVM_CAP_X2APIC_API,
72-
KVM_GUESTDBG_USE_HW_BP, KVM_X2APIC_API_DISABLE_BROADCAST_QUIRK, KVM_X2APIC_API_USE_32BIT_IDS,
73-
MsrList, kvm_enable_cap, kvm_msr_entry,
71+
KVM_CAP_HYPERV_ENLIGHTENED_VMCS, KVM_CAP_HYPERV_SYNIC, KVM_CAP_HYPERV_SYNIC2,
72+
KVM_CAP_SPLIT_IRQCHIP, KVM_CAP_X2APIC_API, KVM_GUESTDBG_USE_HW_BP,
73+
KVM_X2APIC_API_DISABLE_BROADCAST_QUIRK, KVM_X2APIC_API_USE_32BIT_IDS, MsrList, kvm_enable_cap,
74+
kvm_msr_entry,
7475
};
7576
#[cfg(target_arch = "x86_64")]
7677
use x86_64::check_required_kvm_extensions;
@@ -2247,6 +2248,25 @@ impl cpu::Vcpu for KvmVcpu {
22472248
.map_err(|e| cpu::HypervisorCpuError::EnableHyperVSyncIc(e.into()))
22482249
}
22492250

2251+
#[cfg(target_arch = "x86_64")]
2252+
///
2253+
/// X86 specific call to enable HyperV Enlightened VMCS
2254+
///
2255+
fn enable_hyperv_enlightened_vmcs(&self) -> cpu::Result<()> {
2256+
// A Hyper-V guest acting as an L1 hypervisor (nested Hyper-V / WSL2)
2257+
// drives KVM through the Enlightened VMCS layout rather than the
2258+
// architectural VMCS. args[0] receives the eVMCS version supported by
2259+
// KVM; we don't pin a version here (KVM guarantees at least v1, which
2260+
// is what the guest negotiates via CPUID 0x4000000A).
2261+
let cap = kvm_enable_cap {
2262+
cap: KVM_CAP_HYPERV_ENLIGHTENED_VMCS,
2263+
..Default::default()
2264+
};
2265+
self.fd
2266+
.enable_cap(&cap)
2267+
.map_err(|e| cpu::HypervisorCpuError::EnableHyperVEnlightenedVmcs(e.into()))
2268+
}
2269+
22502270
///
22512271
/// X86 specific call to retrieve the CPUID registers.
22522272
///

0 commit comments

Comments
 (0)