[RFC PATCH v2 19/31] KVM: x86: Fold an NMI for a higher plane back to plane 0
From: David Woodhouse
Date: Fri Nov 13 2026 - 17:06:24 EST
From: David Woodhouse <dwmw@xxxxxxxxxxxx>
An NMI aimed at a higher plane is injected into that plane, so the
secure kernel takes a real NMI inside VTL1 and bugchecks (0x18B).
For a non-zero plane, inject the NMI into the plane-0 twin instead, to
be delivered once the VTL returns. Set the higher plane's
hv_apic_event_pending marker so the NMI still forces one entry into
it, and kick through __kvm_kick_lower_twins(), the ungated half of
kvm_kick_lower_twins(), since plane 0 takes the NMI whether or not
anything consumes the higher plane's event.
kvm_higher_plane_with_event() counts a higher plane's pending NMI state
(KVM_REQ_NMI, nmi_queued, nmi_pending) as an event as well: the fold
never leaves NMI state there, but KVM_SET_VCPU_EVENTS on the plane's
own fd can.
When the targeted plane is the selected one and halted, the fold must
still break its HLT, as an NMI would on hardware. Its own event checks
see no NMI and plane 0's events are only examined on guest entry, so
kvm_arch_vcpu_runnable() counts the posting marker the fold leaves on
a non-zero plane, and kvm_check_plane0_events() counts plane 0's
nmi_queued, where the folded NMI waits until it is processed.
Plane 0 is looked up without the locks that serialise twin creation,
so its slot is read once, with READ_ONCE(), to pair with its
publication.
Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
---
arch/x86/kvm/lapic.c | 26 ++++++++++++++++++++++++--
arch/x86/kvm/x86.c | 35 ++++++++++++++++++++++++++++++-----
arch/x86/kvm/x86.h | 1 +
3 files changed, 55 insertions(+), 7 deletions(-)
diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
index 81bf72e83e8c..d33226c44488 100644
--- a/arch/x86/kvm/lapic.c
+++ b/arch/x86/kvm/lapic.c
@@ -1476,8 +1476,30 @@ static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode,
case APIC_DM_NMI:
result = 1;
- kvm_inject_nmi(vcpu);
- kvm_vcpu_kick(vcpu);
+ /*
+ * An NMI aimed at a higher plane forces entry into that
+ * plane (reason INTERRUPT, below) but the NMI itself is
+ * folded back to plane 0 and delivered there after the VTL
+ * returns. Matches the Nitro VSM implementation, which never
+ * delivers an NMI inside a higher VTL.
+ */
+ if (vcpu->plane_level) {
+ struct kvm_vcpu *twin;
+
+ WRITE_ONCE(vcpu->arch.hv_apic_event_pending, true);
+ twin = READ_ONCE(vcpu->common->vcpus[0]);
+ if (twin)
+ kvm_inject_nmi(twin);
+ /*
+ * Plane 0 takes the NMI whether or not VTL semantics
+ * are on, so don't gate this kick the way
+ * kvm_kick_lower_twins() does.
+ */
+ __kvm_kick_lower_twins(vcpu);
+ } else {
+ kvm_inject_nmi(vcpu);
+ kvm_vcpu_kick(vcpu);
+ }
break;
case APIC_DM_INIT:
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 165eb105e112..c2c9b5a181b9 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -8301,16 +8301,22 @@ bool kvm_higher_plane_event_consumable(struct kvm_vcpu *higher)
* 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.
+ * running (or waiting). kvm_kick_lower_twins() kicks nothing for an
+ * event that cannot be consumed; __kvm_kick_lower_twins() is the ungated
+ * kick, for an event that a lower plane takes itself.
*/
+void __kvm_kick_lower_twins(struct kvm_vcpu *vcpu)
+{
+ kvm_make_request(KVM_REQ_UNBLOCK, vcpu);
+ kvm_vcpu_kick(vcpu);
+}
+
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);
+ __kvm_kick_lower_twins(vcpu);
}
/*
@@ -8353,6 +8359,9 @@ static struct kvm_vcpu *kvm_higher_plane_with_event(struct kvm_vcpu *vcpu)
if (READ_ONCE(higher->arch.hv_apic_event_pending) ||
kvm_cpu_has_extint(higher) ||
kvm_test_request(KVM_REQ_HV_STIMER, higher) ||
+ kvm_test_request(KVM_REQ_NMI, higher) ||
+ atomic_read(&higher->arch.nmi_queued) ||
+ higher->arch.nmi_pending ||
kvm_cpu_has_pending_timer(higher))
return higher;
}
@@ -8403,6 +8412,7 @@ static inline bool kvm_check_plane0_events(struct kvm_vcpu *vcpu)
return kvm_cpu_has_injectable_intr(vcpu_plane0) ||
vcpu_plane0->arch.nmi_pending ||
+ atomic_read(&vcpu_plane0->arch.nmi_queued) ||
vcpu_plane0->arch.smi_pending;
}
@@ -9254,11 +9264,26 @@ bool kvm_vcpu_has_events(struct kvm_vcpu *vcpu)
}
EXPORT_SYMBOL_FOR_KVM_INTERNAL(kvm_vcpu_has_events);
+/*
+ * An NMI aimed at a higher plane is folded to plane 0 and leaves only the
+ * plane's own posting marker behind (see __apic_accept_irq()). When that
+ * plane is the selected one and sits in HLT, nothing else wakes it: its
+ * own event checks see no NMI, and plane 0's events are only examined
+ * when the plane enters the guest. The marker is consumed on that entry,
+ * so the NMI breaks the HLT once, as it would on hardware.
+ */
+static bool kvm_plane_nmi_folded(struct kvm_vcpu *vcpu)
+{
+ return vcpu->plane_level &&
+ READ_ONCE(vcpu->arch.hv_apic_event_pending);
+}
+
int kvm_arch_vcpu_runnable(struct kvm_vcpu *vcpu)
{
return kvm_vcpu_running(vcpu) || vcpu->arch.pv.pv_unhalted ||
kvm_vcpu_has_events(vcpu) ||
- kvm_higher_plane_with_event(vcpu);
+ kvm_higher_plane_with_event(vcpu) ||
+ kvm_plane_nmi_folded(vcpu);
}
/* Called within kvm->srcu read side. */
diff --git a/arch/x86/kvm/x86.h b/arch/x86/kvm/x86.h
index 649d4b8ac2d4..00dc866c08c3 100644
--- a/arch/x86/kvm/x86.h
+++ b/arch/x86/kvm/x86.h
@@ -534,6 +534,7 @@ void __user *__x86_set_memory_region(struct kvm *kvm, int id, gpa_t gpa,
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);
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