[RFC PATCH 24/55] WIP: KVM: x86: deliver events to suspended higher planes

From: David Woodhouse

Date: Sat Oct 17 2026 - 08:44:49 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

A suspended plane's events must preempt the lower plane running on the
same vCPU, but nothing told the running twin:

- An interrupt, local-APIC timer or Hyper-V stimer landing on a
stopped twin only kicked the twin itself — a no-op while it is
parked. Kick the lower twins too, with KVM_REQ_UNBLOCK so a sibling
already past its event checks aborts the in-flight vmentry.
- A parked plane's lapic timer rode the VMX preemption timer, which
only counts in guest mode, so it never fired while the plane was
suspended. Hand it to the hrtimer when a non-zero plane's KVM_RUN
ends.
- KVM_REQ_HV_STIMER was a shared request, so the running plane
consumed the twin's request without processing the twin's stimers.
Make it per-plane.
- kvm_higher_plane_with_event() now also reports a
pending-but-not-injected apic timer.
- Suppress KVM_EXIT_HYPERV_SYNIC for non-zero planes: userspace's
SynIC device models plane 0 only, and treated a twin's SIMP move as
a plane-0 overlay relocation, carrying stale overlay contents to the
new GPA.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
Assisted-by: LLM
---
arch/x86/include/asm/kvm_host.h | 2 +-
arch/x86/kvm/hyperv.c | 27 +++++++++++++++++++
arch/x86/kvm/lapic.c | 46 ++++++++++++++++++++++++++++++++-
arch/x86/kvm/x86.c | 19 ++++++++++++--
4 files changed, 90 insertions(+), 4 deletions(-)

diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index 74810e0e9ed7..c54f47c52a59 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -112,7 +112,7 @@
#define KVM_REQ_IOAPIC_EOI_EXIT KVM_ARCH_PLANE_REQ(19)
#define KVM_REQ_HV_RESET KVM_ARCH_REQ(20)
#define KVM_REQ_HV_EXIT KVM_ARCH_REQ(21)
-#define KVM_REQ_HV_STIMER KVM_ARCH_REQ(22)
+#define KVM_REQ_HV_STIMER KVM_ARCH_PLANE_REQ(22)
#define KVM_REQ_LOAD_EOI_EXITMAP KVM_ARCH_REQ(23)
#define KVM_REQ_GET_NESTED_STATE_PAGES KVM_ARCH_REQ(24)
#define KVM_REQ_APICV_UPDATE \
diff --git a/arch/x86/kvm/hyperv.c b/arch/x86/kvm/hyperv.c
index ca31054ed065..4551e011bf56 100644
--- a/arch/x86/kvm/hyperv.c
+++ b/arch/x86/kvm/hyperv.c
@@ -247,6 +247,15 @@ static void synic_exit(struct kvm_vcpu_hv_synic *synic, u32 msr)
struct kvm_vcpu *vcpu = hv_synic_to_vcpu(synic);
struct kvm_vcpu_hv *hv_vcpu = to_hv_vcpu(vcpu);

+ /*
+ * Userspace's SynIC device models plane 0 only; a higher plane's
+ * exit would be mistaken for a plane-0 page move and relocate the
+ * overlay the VMM keeps for SIMP/SIEFP. Higher-plane SynICs are
+ * handled fully in-kernel.
+ */
+ if (vcpu->plane_level)
+ return;
+
hv_vcpu->exit.type = KVM_EXIT_HYPERV_SYNIC;
hv_vcpu->exit.u.synic.msr = msr;
hv_vcpu->exit.u.synic.control = synic->control;
@@ -598,6 +607,24 @@ static void stimer_mark_pending(struct kvm_vcpu_hv_stimer *stimer,
kvm_make_request(KVM_REQ_HV_STIMER, vcpu);
if (vcpu_kick)
kvm_vcpu_kick(vcpu);
+
+ /*
+ * A pending stimer on a suspended higher plane is only processed
+ * once that plane runs; kick the lower twins so the running one
+ * re-evaluates pending higher-plane events and forces entry.
+ */
+ if (vcpu_kick && vcpu->plane_level) {
+ struct kvm_vcpu *twin;
+ int p;
+
+ for (p = 0; p < vcpu->plane_level; p++) {
+ twin = vcpu->common->vcpus[p];
+ if (twin) {
+ kvm_make_request(KVM_REQ_UNBLOCK, twin);
+ kvm_vcpu_kick(twin);
+ }
+ }
+ }
}

static void stimer_cleanup(struct kvm_vcpu_hv_stimer *stimer)
diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
index dc7a08831a54..f89156c26972 100644
--- a/arch/x86/kvm/lapic.c
+++ b/arch/x86/kvm/lapic.c
@@ -1436,6 +1436,26 @@ static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode,

kvm_x86_call(deliver_interrupt)(apic, delivery_mode,
trig_mode, vector);
+
+ /*
+ * An interrupt for a higher plane must preempt a lower
+ * plane running on the same vCPU: kicking only the (stopped)
+ * target twin wakes a blocked sibling via the shared wait,
+ * but a sibling executing in guest mode needs an explicit
+ * kick to re-evaluate pending higher-plane events.
+ */
+ if (vcpu->plane_level) {
+ struct kvm_vcpu *twin;
+ int p;
+
+ for (p = 0; p < vcpu->plane_level; p++) {
+ twin = vcpu->common->vcpus[p];
+ if (twin) {
+ kvm_make_request(KVM_REQ_UNBLOCK, twin);
+ kvm_vcpu_kick(twin);
+ }
+ }
+ }
break;

case APIC_DM_REMRD:
@@ -2060,6 +2080,24 @@ static void apic_timer_expired(struct kvm_lapic *apic, bool from_timer_fn)
kvm_make_request(KVM_REQ_UNBLOCK, vcpu);
if (from_timer_fn)
kvm_vcpu_kick(vcpu);
+
+ /*
+ * A pending timer on a suspended higher plane only gets injected
+ * once that plane runs again; kick the lower twins so the running
+ * one re-evaluates pending higher-plane events and forces entry.
+ */
+ if (vcpu->plane_level) {
+ struct kvm_vcpu *twin;
+ int p;
+
+ for (p = 0; p < vcpu->plane_level; p++) {
+ twin = vcpu->common->vcpus[p];
+ if (twin) {
+ kvm_make_request(KVM_REQ_UNBLOCK, twin);
+ kvm_vcpu_kick(twin);
+ }
+ }
+ }
}

static void start_sw_tscdeadline(struct kvm_lapic *apic)
@@ -3107,7 +3145,13 @@ int kvm_create_lapic(struct kvm_vcpu *vcpu)
* that vCPU creation is guaranteed to run with a deterministic value,
* the request will ensure the vCPU gets the correct state before VM-Entry.
*/
- if (enable_apicv) {
+ /*
+ * Higher-plane twins are software-switched and mostly suspended;
+ * hardware interrupt virtualization on them only leaves pending
+ * vectors in the PIR where a suspended plane's state cannot be
+ * reliably observed. Keep them on the legacy delivery path.
+ */
+ if (enable_apicv && !vcpu->plane_level) {
apic->apicv_active = true;
kvm_make_request(KVM_REQ_APICV_UPDATE, vcpu);
}
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 374330f61eb2..4b12ec7ee100 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -11226,7 +11226,9 @@ static struct kvm_vcpu *kvm_higher_plane_with_event(struct kvm_vcpu *vcpu)
*/
if (kvm_cpu_has_extint(higher) ||
kvm_apic_has_interrupt(higher) != -1 ||
- kvm_test_request(KVM_REQ_HV_STIMER, higher))
+ kvm_test_request(KVM_REQ_HV_STIMER, higher) ||
+ (higher->arch.apic &&
+ atomic_read(&higher->arch.apic->lapic_timer.pending)))
return higher;
}
return NULL;
@@ -11336,8 +11338,12 @@ void __kvm_vcpu_update_apicv(struct kvm_vcpu *vcpu)
down_read(&vcpu->kvm->arch.apicv_update_lock);
preempt_disable();

- /* Do not activate APICV when APIC is disabled */
+ /*
+ * Do not activate APICV when APIC is disabled, nor on higher-plane
+ * twins (see kvm_create_lapic).
+ */
activate = kvm_vcpu_apicv_activated(vcpu) &&
+ !vcpu->plane_level &&
(kvm_get_apic_mode(vcpu) != LAPIC_MODE_DISABLED);

if (apic->apicv_active == activate)
@@ -12566,6 +12572,15 @@ static int __kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
post_kvm_run_save(vcpu);
kvm_vcpu_srcu_read_unlock(vcpu);

+ /*
+ * A non-zero plane may stay suspended indefinitely after this run
+ * ends, and its hv timer only counts in VMX non-root mode. Hand
+ * the lapic timer to the hrtimer so it keeps firing while the
+ * plane is parked; firing is what forces re-entry into the plane.
+ */
+ if (vcpu->plane_level && kvm_lapic_hv_timer_in_use(vcpu))
+ kvm_lapic_switch_to_sw_timer(vcpu);
+
kvm_sigset_deactivate(vcpu);
vcpu_put(vcpu);
return r;
--
2.50.1