[RFC PATCH 12/55] KVM: x86: initialise Hyper-V state on every plane vCPU

From: David Woodhouse

Date: Sat Oct 17 2026 - 08:45:37 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

When userspace sets a Hyper-V CPUID, kvm_set_cpuid() walks the vCPU's
planes to allocate the per-plane kvm_vcpu_hv, but passes the plane-0
vCPU to kvm_hv_vcpu_init() on every iteration. Non-zero plane vCPUs
are left without hv_vcpu, which trips the
WARN_ON_ONCE(vcpu->arch.hyperv_enabled) in kvm_hv_set_cpuid() and
leaves their vp_index and SynIC state uninitialised until a lazy init
path happens to run. Pass the plane vCPU being iterated.

A twin created after KVM_SET_CPUID never passes through
kvm_set_cpuid() at all: it shares the common CPUID, and
kvm_arch_vcpu_create() syncs it through kvm_vcpu_after_set_cpuid(),
so that walk cannot cover it and the twin hits the same WARN with an
empty cpuid_cache. Allocate the context in kvm_vcpu_after_set_cpuid()
when the CPUID has the Hyper-V signature and the vCPU doesn't have
one yet. The allocation is idempotent, so the kvm_set_cpuid() path is
unaffected; if it fails, treat Hyper-V as disabled for the vCPU
exactly as a failing kvm_set_cpuid() would have.

Also admit KVM_ENABLE_CAP on plane vCPU fds, so userspace can enable
per-vCPU capabilities such as KVM_CAP_HYPERV_SYNIC2 on a plane vCPU
before it first runs.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
Assisted-by: LLM
---
arch/x86/kvm/cpuid.c | 18 +++++++++++++++---
arch/x86/kvm/cpuid.h | 1 +
arch/x86/kvm/x86.c | 20 +++++++++++++++++++-
3 files changed, 35 insertions(+), 4 deletions(-)

diff --git a/arch/x86/kvm/cpuid.c b/arch/x86/kvm/cpuid.c
index fab075bb6fdc..fe8a793b1643 100644
--- a/arch/x86/kvm/cpuid.c
+++ b/arch/x86/kvm/cpuid.c
@@ -324,7 +324,7 @@ static void kvm_update_cpuid_runtime(struct kvm_vcpu *vcpu)
vcpu->arch.ia32_xss, true);
}

-static bool kvm_cpuid_has_hyperv(struct kvm_vcpu *vcpu)
+bool kvm_cpuid_has_hyperv(struct kvm_vcpu *vcpu)
{
#ifdef CONFIG_KVM_HYPERV
struct kvm_cpuid_entry2 *entry;
@@ -380,6 +380,7 @@ void kvm_vcpu_after_set_cpuid(struct kvm_vcpu *vcpu)
struct kvm_cpuid_entry2 *best;
struct kvm_cpuid_entry2 *entry;
bool allow_gbpages;
+ bool hyperv_enabled;
int i;

memset(common->arch.cpu_caps, 0, sizeof(common->arch.cpu_caps));
@@ -456,7 +457,18 @@ void kvm_vcpu_after_set_cpuid(struct kvm_vcpu *vcpu)
__cr4_reserved_bits(guest_cpu_cap_has, vcpu);
#undef __kvm_cpu_cap_has

- kvm_hv_set_cpuid(vcpu, kvm_cpuid_has_hyperv(vcpu));
+ hyperv_enabled = kvm_cpuid_has_hyperv(vcpu);
+
+ /*
+ * A plane twin created after KVM_SET_CPUID shares the common CPUID
+ * without passing through kvm_set_cpuid(), which is what allocates
+ * the Hyper-V context for the planes that exist at set time;
+ * allocate it here for a late-created twin.
+ */
+ if (hyperv_enabled && kvm_hv_vcpu_init(vcpu))
+ hyperv_enabled = false;
+
+ kvm_hv_set_cpuid(vcpu, hyperv_enabled);

/* Invoke the vendor callback only after the above state is updated. */
kvm_x86_call(vcpu_after_set_cpuid)(vcpu);
@@ -565,7 +577,7 @@ static int kvm_set_cpuid(struct kvm_vcpu *vcpu, struct kvm_cpuid_entry2 *e2,
#ifdef CONFIG_KVM_HYPERV
if (kvm_cpuid_has_hyperv(vcpu)) {
vcpu_for_each_plane(common, i, v) {
- r = kvm_hv_vcpu_init(vcpu);
+ r = kvm_hv_vcpu_init(v);
if (r)
goto err;
}
diff --git a/arch/x86/kvm/cpuid.h b/arch/x86/kvm/cpuid.h
index 75abf447eabf..873377404899 100644
--- a/arch/x86/kvm/cpuid.h
+++ b/arch/x86/kvm/cpuid.h
@@ -19,6 +19,7 @@ static inline void kvm_finalize_cpu_caps(void)
}

void kvm_vcpu_after_set_cpuid(struct kvm_vcpu *vcpu);
+bool kvm_cpuid_has_hyperv(struct kvm_vcpu *vcpu);
struct kvm_cpuid_entry2 *kvm_find_cpuid_entry2(struct kvm_cpuid_entry2 *entries,
int nent, u32 function, u64 index);
/*
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 2c41c53e4d4f..0828889db149 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -6286,6 +6286,7 @@ static int kvm_get_reg_list(struct kvm_vcpu *vcpu,
bool kvm_arch_is_vcpu_plane_ioctl(unsigned ioctl)
{
switch (ioctl) {
+ case KVM_ENABLE_CAP:
case KVM_GET_DEBUGREGS:
case KVM_SET_DEBUGREGS:
case KVM_GET_LAPIC:
@@ -13135,9 +13136,24 @@ int kvm_arch_vcpu_create(struct kvm_vcpu *vcpu)
vcpu->arch.hv_root_tdp = INVALID_PAGE;
#endif

+ /*
+ * A plane twin created after KVM_SET_CPUID shares CPUID that may
+ * already advertise Hyper-V. Allocate the Hyper-V context now so
+ * a failure fails the creation; left to kvm_vcpu_after_set_cpuid()
+ * below, an -ENOMEM silently downgrades just this twin to no
+ * Hyper-V (its MSRs fail) while the shared CPUID still advertises
+ * the Hyper-V signature, and nothing short of another full
+ * KVM_SET_CPUID repairs it.
+ */
+ if (kvm_cpuid_has_hyperv(vcpu)) {
+ r = kvm_hv_vcpu_init(vcpu);
+ if (r)
+ goto free_guest_fpu;
+ }
+
r = kvm_x86_call(vcpu_create)(vcpu);
if (r)
- goto free_guest_fpu;
+ goto free_hv;

kvm_xen_init_vcpu(vcpu);
vcpu_load(vcpu);
@@ -13148,6 +13164,8 @@ int kvm_arch_vcpu_create(struct kvm_vcpu *vcpu)
vcpu_put(vcpu);
return 0;

+free_hv:
+ kvm_hv_vcpu_uninit(vcpu);
free_guest_fpu:
fpu_free_guest_fpstate(&vcpu->arch.guest_fpu);
free_emulate_ctxt:
--
2.50.1