From: David Woodhouse <[email protected]>

All derived from the reference host's behaviour, and verified against
its VSM unit-test suite (still green) and Windows Server 2025 VBS
boot:

- Forced entry into a higher plane is edge-triggered, as on the
  reference host, which attempts one forced entry per posted
  interrupt. Delivery of a fixed vector or NMI to a suspended higher
  plane sets hv_apic_event_pending; the evaluation triggers on that
  marker and the consumable requests (stimer, NMI, APIC timer), not
  on a scan of the plane's IRR — a vector still latched after a
  refused entry waits for the target's next cooperative entry or next
  posted event instead of retrying on every run of the lower plane.
  The marker is cleared only by a run that actually reaches event
  evaluation; a run aborted by the VTL-exit or KVM_REQ_PLANE_RESCHED
  bail-outs leaves it set, since delivery never re-arms it. A vector
  masked by the higher VTL's TPR still forces exactly one entry.
- One exception to single-shot: a plane that suspends (VtlReturn or
  the run-state ioctl) with a vector still latched in its IRR re-arms
  its marker, via kvm_arch_vcpu_plane_stopped() from
  kvm_vcpu_set_plane_stopped(). Evaluating does not imply delivering:
  Windows hibernation posts vector 0xE0 to a twin mid-run with IF
  clear, the run consumes the marker without injecting, and the
  suspended twin's vector is stranded — VTL0 freezes all processors,
  spins on the secure kernel and wedges the VM at 100% host CPU. A
  re-armed marker cannot storm: the deliverability gate still applies,
  and a plane that reached suspension has run before, so userspace can
  enter it again.
- NMIs aimed at a higher plane fold back to plane 0 for delivery
  after the VTL returns; the higher plane only sees the forced entry.
  A real NMI inside VTL1 bugchecks the secure kernel (0x18B).
- VTL1 exempted from the reserved-bits check on Hyper-V vAPIC MSR
  writes: the secure kernel writes HV_X64_MSR_EOI with stale upper
  bits, and the reference host truncates for VTL1.
- KVM_REQ_HV_TLB_FLUSH made per-plane: a shared request could be
  consumed by the running twin, leaving the targeted plane's TLB
  stale.
- Lower-twin kicks on the stimer retry (vcpu_kick=false) paths.

Signed-off-by: David Woodhouse <[email protected]>
Assisted-by: LLM
---
 arch/x86/include/asm/kvm_host.h | 12 ++++-
 arch/x86/kvm/hyperv.c           |  3 +-
 arch/x86/kvm/lapic.c            | 39 ++++++++++++++-
 arch/x86/kvm/x86.c              | 89 +++++++++++++++++++++++++++++----
 include/linux/kvm_host.h        |  1 +
 virt/kvm/kvm_main.c             |  5 ++
 6 files changed, 135 insertions(+), 14 deletions(-)

diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index c54f47c52a59..834597791fe1 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -127,7 +127,8 @@
 #define KVM_REQ_MMU_FREE_OBSOLETE_ROOTS \
        KVM_ARCH_REQ_FLAGS(31, KVM_REQUEST_WAIT | KVM_REQUEST_NO_WAKEUP)
 #define KVM_REQ_HV_TLB_FLUSH \
-       KVM_ARCH_REQ_FLAGS(32, KVM_REQUEST_WAIT | KVM_REQUEST_NO_WAKEUP)
+       KVM_ARCH_REQ_FLAGS(32, KVM_REQUEST_WAIT | KVM_REQUEST_NO_WAKEUP | \
+                          KVM_REQUEST_PER_PLANE)
 #define KVM_REQ_UPDATE_PROTECTED_GUEST_STATE \
        KVM_ARCH_REQ_FLAGS(34, KVM_REQUEST_WAIT | KVM_REQUEST_PER_PLANE)
 
@@ -854,6 +855,15 @@ int kvm_arch_vcpu_common_init(struct kvm_vcpu_common 
*common);
 void kvm_arch_vcpu_common_destroy(struct kvm_vcpu_common *common);
 
 struct kvm_vcpu_arch {
+       /*
+        * Hyper-V VSM: an APIC event (fixed vector or NMI) was delivered to
+        * this plane while it was suspended.  Set at delivery, cleared when
+        * the plane next runs; a lower plane's run loop uses it to force a
+        * VTL entry even when the vector is currently masked by this
+        * plane's TPR (matching the reference host's apic_pending_vtls).
+        */
+       bool hv_apic_event_pending;
+
        /*
         * rip and regs accesses must go through
         * kvm_{register,rip}_{read,write} functions.
diff --git a/arch/x86/kvm/hyperv.c b/arch/x86/kvm/hyperv.c
index d4e44ae3f6e8..0d239f054856 100644
--- a/arch/x86/kvm/hyperv.c
+++ b/arch/x86/kvm/hyperv.c
@@ -605,8 +605,9 @@ static void stimer_mark_pending(struct kvm_vcpu_hv_stimer 
*stimer,
         * 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.
+        * This applies to retry paths too (vcpu_kick == false).
         */
-       if (vcpu_kick && vcpu->plane_level) {
+       if (vcpu->plane_level) {
                struct kvm_vcpu *twin;
                int p;
 
diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
index f89156c26972..32cace3c89f4 100644
--- a/arch/x86/kvm/lapic.c
+++ b/arch/x86/kvm/lapic.c
@@ -1448,6 +1448,7 @@ static int __apic_accept_irq(struct kvm_lapic *apic, int 
delivery_mode,
                        struct kvm_vcpu *twin;
                        int p;
 
+                       WRITE_ONCE(vcpu->arch.hv_apic_event_pending, true);
                        for (p = 0; p < vcpu->plane_level; p++) {
                                twin = vcpu->common->vcpus[p];
                                if (twin) {
@@ -1474,8 +1475,31 @@ 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: the reference host never delivers
+                * an NMI inside a higher VTL, it re-queues it to VTL0's
+                * context for delivery after the VTL returns.
+                */
+               if (vcpu->plane_level) {
+                       struct kvm_vcpu *twin;
+                       int p;
+
+                       WRITE_ONCE(vcpu->arch.hv_apic_event_pending, true);
+                       if (vcpu->common->vcpus[0])
+                               kvm_inject_nmi(vcpu->common->vcpus[0]);
+                       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);
+                               }
+                       }
+               } else {
+                       kvm_inject_nmi(vcpu);
+                       kvm_vcpu_kick(vcpu);
+               }
                break;
 
        case APIC_DM_INIT:
@@ -3535,6 +3559,17 @@ int kvm_hv_vapic_msr_write(struct kvm_vcpu *vcpu, u32 
reg, u64 data)
        if (!lapic_in_kernel(vcpu))
                return 1;
 
+       /*
+        * Bits 63:32 are reserved in non-ICR registers, but the Windows
+        * secure kernel leaves stale data in the high bits when writing
+        * the Hyper-V vAPIC MSRs from a higher VTL.  The reference host
+        * exempts those writes and truncates; a #GP here is a VTL1
+        * bugcheck.  The exemption covers only this PV path: a native
+        * x2APIC WRMSR takes the architectural #GP whatever the plane.
+        */
+       if (vcpu->plane_level && reg != APIC_ICR)
+               data = (u32)data;
+
        return kvm_lapic_msr_write(vcpu->arch.apic, reg, data);
 }
 
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 10e14ab3e2ca..3e2465b2fa2b 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -11229,18 +11229,26 @@ static struct kvm_vcpu 
*kvm_higher_plane_with_event(struct kvm_vcpu *vcpu)
                        continue;
 #endif
                /*
-                * Test raw pending-ness, not injectability:
-                * kvm_cpu_has_injectable_intr() reports false on an
-                * APICv-active vCPU, and a suspended plane may carry a
-                * stale apicv_active until it next runs.  A pending
-                * synthetic-timer request counts as well: it can only be
-                * processed (and its interrupt delivered) once the higher
-                * plane runs, so it would otherwise never fire while the
-                * plane is suspended.
+                * Forced entry is edge-triggered on the posting of an
+                * event, not level-triggered on the target's IRR, matching
+                * the reference host's apic_pending_vtls bitmap: one entry
+                * attempt per posted interrupt, via the hv_apic_event_pending
+                * marker __apic_accept_irq sets.  A vector that stays
+                * latched after a refused attempt (no VP assist page yet)
+                * or that was masked by the plane's TPR waits for the next
+                * cooperative entry or the next posted event; scanning the
+                * IRR here instead would replay it the moment the assist
+                * page is re-enabled.  A pending synthetic-timer request
+                * counts as well: it can only be processed (and its
+                * interrupt delivered) once the higher plane runs, so it
+                * would otherwise never fire while the plane is suspended.
                 */
-               if (kvm_cpu_has_extint(higher) ||
-                   kvm_apic_has_interrupt(higher) != -1 ||
+               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 ||
                    (higher->arch.apic &&
                     atomic_read(&higher->arch.apic->lapic_timer.pending)))
                        return higher;
@@ -11265,6 +11273,14 @@ static bool kvm_hv_vtl_interrupt_exit(struct kvm_vcpu 
*vcpu)
        if (!higher)
                return false;
 
+       /*
+        * Single-shot, as in the reference host: if userspace refuses
+        * the entry (e.g. the target VTL has no VP assist page yet),
+        * the event waits in the target's IRR for a cooperative entry
+        * rather than re-triggering an exit storm.
+        */
+       WRITE_ONCE(higher->arch.hv_apic_event_pending, false);
+
        vcpu->run->exit_reason = KVM_EXIT_HYPERV;
        vcpu->run->hyperv.type = KVM_EXIT_HYPERV_VTL_INTERRUPT;
        vcpu->run->hyperv.plane = higher->plane_level;
@@ -11286,6 +11302,50 @@ static inline bool kvm_check_plane0_events(struct 
kvm_vcpu *vcpu)
                vcpu_plane0->arch.smi_pending;
 }
 
+/*
+ * A higher plane that suspends with an interrupt still latched in its IRR
+ * must keep its hv_apic_event_pending marker armed.  The running plane
+ * consumes the marker in vcpu_enter_guest() before evaluating events, but
+ * evaluating does not imply delivering: a plane that keeps IF clear until
+ * it suspends (e.g. the secure kernel finishing a VTL round trip), or
+ * whose PPR masks the vector for the whole run, parks the vector in its
+ * IRR with the marker already consumed.  Nothing re-posts the interrupt,
+ * so the deliverability-gated forced entry in kvm_higher_plane_with_event()
+ * would never fire again and the plane would never see the vector; a lower
+ * VTL spinning on the suspended VTL's response then wedges the VM.
+ *
+ * Re-arming cannot re-create the refused-entry exit storm the single-shot
+ * marker guards against: the forced-entry gate still requires the vector
+ * to be deliverable by the plane's own priority state.
+ */
+void kvm_arch_vcpu_plane_stopped(struct kvm_vcpu *vcpu)
+{
+       /*
+        * The hv_apic_event_pending marker drives the Hyper-V VSM forced
+        * entry (see kvm_higher_plane_with_event()).  This hook also runs
+        * from __sev_snp_run_vmpl() for guest-architectural VMPL switches:
+        * without the Hyper-V gate, every SNP VMPL switch away from a
+        * plane with a latched IRR would feed VSM-flavoured wakeups and
+        * exits to a VM that has no VSM at all.
+        */
+       if (!vcpu->arch.hyperv_enabled)
+               return;
+
+       /*
+        * A plane that reached suspension normally has run before; the
+        * exception is userspace parking a never-run plane it had already
+        * injected an interrupt into.  Forced entry into a never-run
+        * plane is always refused, so don't arm an attempt that cannot
+        * succeed; the IRR is evaluated on the plane's first real entry.
+        */
+       if (vcpu->arch.last_vmentry_cpu == -1)
+               return;
+
+       if (vcpu->plane_level && lapic_in_kernel(vcpu) &&
+           kvm_lapic_find_highest_irr(vcpu) != -1)
+               WRITE_ONCE(vcpu->arch.hv_apic_event_pending, true);
+}
+
 static void process_nmi(struct kvm_vcpu *vcpu)
 {
        unsigned int limit;
@@ -11771,6 +11831,15 @@ static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
                goto out;
        }
 
+       /*
+        * This plane is running and about to evaluate its events: its
+        * pended-while-suspended marker is consumed.  The clear must come
+        * after the VTL-exit and plane-reschedule bail-outs above: a run
+        * aborted there never evaluates the pended event, and nothing
+        * re-arms the marker for a TPR-masked vector.
+        */
+       WRITE_ONCE(vcpu->arch.hv_apic_event_pending, false);
+
        if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win ||
            kvm_xen_has_interrupt(vcpu)) {
                ++vcpu->stat.req_event;
diff --git a/include/linux/kvm_host.h b/include/linux/kvm_host.h
index cb8c4798df12..30dbc28885fb 100644
--- a/include/linux/kvm_host.h
+++ b/include/linux/kvm_host.h
@@ -493,6 +493,7 @@ static inline void kvm_vcpu_cancel_vtl_fpu_carry(struct 
kvm_vcpu_common *common)
 
 void kvm_vcpu_set_plane_runnable(struct kvm_vcpu *vcpu);
 void kvm_vcpu_set_plane_stopped(struct kvm_vcpu *vcpu);
+void kvm_arch_vcpu_plane_stopped(struct kvm_vcpu *vcpu);
 struct kvm_vcpu *kvm_vcpu_select_plane(struct kvm_vcpu *vcpu);
 
 static inline bool kvm_vcpu_wants_to_run(struct kvm_vcpu *vcpu)
diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
index 5856672bbf6d..fac30b8b6518 100644
--- a/virt/kvm/kvm_main.c
+++ b/virt/kvm/kvm_main.c
@@ -5347,9 +5347,14 @@ void kvm_vcpu_set_plane_runnable(struct kvm_vcpu *vcpu)
 }
 EXPORT_SYMBOL_FOR_KVM_INTERNAL(kvm_vcpu_set_plane_runnable);
 
+void __weak kvm_arch_vcpu_plane_stopped(struct kvm_vcpu *vcpu)
+{
+}
+
 void kvm_vcpu_set_plane_stopped(struct kvm_vcpu *vcpu)
 {
        vcpu->plane_state = STOPPED;
+       kvm_arch_vcpu_plane_stopped(vcpu);
        kvm_make_request(KVM_REQ_PLANE_RESCHED, vcpu);
 }
 EXPORT_SYMBOL_FOR_KVM_INTERNAL(kvm_vcpu_set_plane_stopped);
-- 
2.50.1


Reply via email to