[RFC PATCH v2 12/31] KVM: x86: Kick lower twins when an event lands on a suspended plane
From: David Woodhouse
Date: Fri Nov 13 2026 - 16:31:13 EST
From: David Woodhouse <dwmw@xxxxxxxxxxxx>
An interrupt or synthetic timer expiry posted to a suspended higher
plane raises a per-plane request on that plane's vCPU and kicks it. The
kick reaches the lower twin running on the same vCPU thread, since the
twins share their mode and wait queue, but the per-plane request does
not: a twin that is already past its event checks enters the guest
anyway, and the event does not preempt it until something else makes it
exit.
Add kvm_kick_lower_twins(), which raises KVM_REQ_UNBLOCK, a request in
the word the twins share, and kicks, so the running twin aborts a
vmentry in flight and re-evaluates pending higher-plane events. Call it
after delivering a fixed interrupt to a non-zero plane and whenever a
synthetic timer is marked pending, including the retry paths that call
stimer_mark_pending() with vcpu_kick false: a timer that expires again
on a parked plane through a retry must preempt the lower plane too. A
LAPIC timer expiry already raises KVM_REQ_UNBLOCK and kicks in
apic_timer_expired(), so that path needs nothing more.
The kick is skipped when the event cannot be consumed: without
KVM_HYPERV_HCALL_VTL_CALLS, or, with in-kernel switching, on a plane the
forced entry would refuse. kvm_higher_plane_event_consumable() holds
that rule, and kvm_higher_plane_with_event() now uses it too.
Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
---
arch/x86/kvm/hyperv.c | 7 +++++
arch/x86/kvm/lapic.c | 9 ++++---
arch/x86/kvm/x86.c | 63 +++++++++++++++++++++++++++++--------------
arch/x86/kvm/x86.h | 2 ++
4 files changed, 58 insertions(+), 23 deletions(-)
diff --git a/arch/x86/kvm/hyperv.c b/arch/x86/kvm/hyperv.c
index 6b76e8ca845a..d2ad255ae212 100644
--- a/arch/x86/kvm/hyperv.c
+++ b/arch/x86/kvm/hyperv.c
@@ -611,6 +611,13 @@ 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, so it must preempt the lower plane, on
+ * the retry paths (vcpu_kick == false) too.
+ */
+ kvm_kick_lower_twins(vcpu);
}
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 7842d0f53e9f..79e4998da644 100644
--- a/arch/x86/kvm/lapic.c
+++ b/arch/x86/kvm/lapic.c
@@ -1450,11 +1450,14 @@ static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode,
trig_mode, vector);
/*
- * Mark the posting of an interrupt to a higher plane for
- * the forced entry (see kvm_higher_plane_with_event()).
+ * An interrupt for a higher plane preempts a lower one: mark
+ * the posting for the forced entry, and make the running
+ * lower twin look.
*/
- if (vcpu->plane_level)
+ if (vcpu->plane_level) {
WRITE_ONCE(vcpu->arch.hv_apic_event_pending, true);
+ kvm_kick_lower_twins(vcpu);
+ }
break;
case APIC_DM_REMRD:
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 8cd3415d23f6..99a18f9da769 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -8249,6 +8249,47 @@ static int kvm_check_and_inject_events(struct kvm_vcpu *vcpu,
return r;
}
+/*
+ * Whether an event pending on the suspended plane @higher can be consumed.
+ * A higher plane preempting a lower one is VTL semantics: without
+ * KVM_HYPERV_HCALL_VTL_CALLS nothing consumes the event (no forced entry,
+ * no VTL interrupt exit). With in-kernel switching, an event on a plane
+ * the forced entry would refuse (never run, user-held, parked on a
+ * foreign completion) has no consumer until userspace starts the plane;
+ * reporting it would wake a halted lower VTL into an endless hlt/wake
+ * loop. With userspace switching the event is consumable via the
+ * interrupt exit, so it does count.
+ */
+bool kvm_higher_plane_event_consumable(struct kvm_vcpu *higher)
+{
+ if (!kvm_hv_hcall_feature(higher->kvm, KVM_HYPERV_HCALL_VTL_CALLS))
+ return false;
+#ifdef CONFIG_KVM_HYPERV
+ if (kvm_hv_hcall_feature(higher->kvm, KVM_HYPERV_HCALL_VTL_SWITCH) &&
+ kvm_hv_vtl_entry_blocked(higher))
+ return false;
+#endif
+ return true;
+}
+
+/*
+ * An event posted to a suspended higher plane must make the lower twin
+ * running on the same vCPU thread re-evaluate pending higher-plane events,
+ * including when it is already past its event checks on the way into the
+ * guest. KVM_REQ_UNBLOCK lives in the request word the twins share, so
+ * it aborts that twin's vmentry, and the kick reaches whichever twin is
+ * running (or waiting). Nothing is kicked for an event that cannot be
+ * consumed.
+ */
+void kvm_kick_lower_twins(struct kvm_vcpu *vcpu)
+{
+ if (!vcpu->plane_level || !kvm_higher_plane_event_consumable(vcpu))
+ return;
+
+ kvm_make_request(KVM_REQ_UNBLOCK, vcpu);
+ kvm_vcpu_kick(vcpu);
+}
+
/*
* A pending interrupt for a higher plane preempts this one: the higher
* VTL's interrupts take priority over lower-VTL execution (TLFS 15.2.4),
@@ -8260,32 +8301,14 @@ static struct kvm_vcpu *kvm_higher_plane_with_event(struct kvm_vcpu *vcpu)
struct kvm_vcpu *higher;
int i;
- /*
- * A higher plane preempting a lower one is VTL semantics: without
- * KVM_HYPERV_HCALL_VTL_CALLS nothing consumes the event (no forced
- * entry, no VTL interrupt exit), so don't report one.
- */
if (!kvm_hv_hcall_feature(vcpu->kvm, KVM_HYPERV_HCALL_VTL_CALLS))
return NULL;
for (i = vcpu->plane_level + 1; i < KVM_MAX_PLANES; i++) {
higher = vcpu->common->vcpus[i];
- if (!higher)
+ if (!higher || !kvm_higher_plane_event_consumable(higher))
continue;
-#ifdef CONFIG_KVM_HYPERV
- /*
- * With in-kernel switching, an event on a plane the forced
- * entry would refuse (never run, user-held, parked on a
- * foreign completion) has no consumer until userspace
- * starts the plane; reporting it would wake a halted lower
- * VTL into an endless hlt/wake loop. With userspace
- * switching the event is consumable via the interrupt exit,
- * so it does count.
- */
- if (kvm_hv_hcall_feature(vcpu->kvm, KVM_HYPERV_HCALL_VTL_SWITCH) &&
- kvm_hv_vtl_entry_blocked(higher))
- continue;
-#endif
+
/*
* Forced entry is edge-triggered on the posting of an
* event, not level-triggered on the target's IRR, matching
diff --git a/arch/x86/kvm/x86.h b/arch/x86/kvm/x86.h
index e5a91c5cc969..649d4b8ac2d4 100644
--- a/arch/x86/kvm/x86.h
+++ b/arch/x86/kvm/x86.h
@@ -533,6 +533,8 @@ void __user *__x86_set_memory_region(struct kvm *kvm, int id, gpa_t gpa,
u32 size);
int memslot_rmap_alloc(struct kvm_memory_slot *slot, unsigned long npages);
+bool kvm_higher_plane_event_consumable(struct kvm_vcpu *higher);
+void kvm_kick_lower_twins(struct kvm_vcpu *vcpu);
bool kvm_vcpu_is_reset_bsp(struct kvm_vcpu *vcpu);
bool kvm_vcpu_is_bsp(struct kvm_vcpu *vcpu);
--
2.50.1