[lvc-project] [PATCH] KVM: x86: Re-pend NMI in vNMI hardware when injection is blocked

Vasiliy Kovalev kovalev at altlinux.org
Wed Jul 29 01:40:41 MSK 2026


Sean,

On 7/28/26 04:54, Vasiliy Kovalev wrote:
>   [...]
> 
> Before falling back to a window, retry .set_vnmi_pending() as process_nmi()
> does. Hardware has already delivered the NMI it was holding, so the retry
> succeeds, nmi_pending drops to zero, and no window is requested; hardware
> delivers the re-pended NMI once the guest IRETs. On the first pass hardware
> still holds the earlier NMI, so the retry fails and single-stepping is
> unchanged. Without vNMI .set_vnmi_pending is NULL and the branch is a nop.
> 
> [...]
> --- a/arch/x86/kvm/x86.c
> +++ b/arch/x86/kvm/x86.c
> @@ -7724,6 +7724,9 @@ static int kvm_check_and_inject_events(struct kvm_vcpu *vcpu,
>   			can_inject = false;
>   			WARN_ON(kvm_x86_call(nmi_allowed)(vcpu, true) < 0);
>   		}
> +		if (vcpu->arch.nmi_pending &&
> +		    (kvm_x86_call(set_vnmi_pending)(vcpu)))
> +			vcpu->arch.nmi_pending--;
>   		if (vcpu->arch.nmi_pending)
>   			kvm_x86_call(enable_nmi_window)(vcpu);
>   	}


Fuzzing on 6.12 (AMD EPYC 9754, vNMI) still hits this WARN with this patch
applied, no stable reproducer. I dumped the vNMI state on entry to the NMI
block in kvm_check_and_inject_events(), plus a line in the re-pend branch
the patch adds:

diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 35ba3551f..f7615674f 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -34,6 +34,8 @@
  #include "xen.h"
  #include "smm.h"

+#include "svm/svm.h"
+
  #include <linux/clocksource.h>
  #include <linux/interrupt.h>
  #include <linux/kvm.h>
@@ -10485,6 +10487,16 @@ static int kvm_check_and_inject_events(struct 
kvm_vcpu *vcpu,
  #endif

         if (vcpu->arch.nmi_pending) {
+               {
+                       struct vcpu_svm *svm = to_svm(vcpu);
+                       pr_warn("NMIdbg: entry pending=%u int_ctl=%#x 
VB=%d VP=%d shadow=%d allowed=%d\n",
+                               vcpu->arch.nmi_pending,
+                               svm->vmcb->control.int_ctl,
+                               !!(svm->vmcb->control.int_ctl & 
V_NMI_BLOCKING_MASK),
+                               !!(svm->vmcb->control.int_ctl & 
V_NMI_PENDING_MASK),
+                               !!(svm->vmcb->control.int_state & 
SVM_INTERRUPT_SHADOW_MASK),
+                               kvm_x86_call(nmi_allowed)(vcpu, false));
+               }
                 r = can_inject ? kvm_x86_call(nmi_allowed)(vcpu, true) :
                                  -EBUSY;
                 if (r < 0)
@@ -10498,7 +10510,11 @@ static int kvm_check_and_inject_events(struct 
kvm_vcpu *vcpu,
                 }
                 if (vcpu->arch.nmi_pending &&
                     (kvm_x86_call(set_vnmi_pending)(vcpu)))
+               {
                         vcpu->arch.nmi_pending--;
+                       pr_warn("NMIdbg: re-pended, pending now %u\n",
+                                vcpu->arch.nmi_pending);
+               }
                 if (vcpu->arch.nmi_pending)
                         kvm_x86_call(enable_nmi_window)(vcpu);
         }

The WARN fires with V_NMI_BLOCKING and V_NMI_PENDING both set and an NMI
still queued - and no "re-pended" line precedes it, i.e. set_vnmi_pending()
returned false because the slot is already occupied:

	NMIdbg: entry pending=1 int_ctl=0x7000a00 VB=0 VP=1 shadow=1 allowed=0 
  (repeated)
	NMIdbg: entry pending=1 int_ctl=0x7001a00 VB=1 VP=1 shadow=0 allowed=0
	WARNING: CPU: 3 at arch/x86/kvm/svm/svm.c:4027 
svm_enable_nmi_window+0x703/0x8e0
	 kvm_check_and_inject_events+0x10ed/0x16a0
	 vcpu_run+0x1922/0x4ed0
	 kvm_vcpu_ioctl+0x469/0x1590

So V_NMI_PENDING is already set: there's nowhere to re-pend, nmi_pending
can't be drained, and the window is still requested. The patch fixes the
interrupt-shadow path (original reproducer no longer triggers) but not this
one. I couldn't minimize the exact sequence, but it looks like
KVM_SET_VCPU_EVENTS setting nmi.masked while a vNMI is pending
(svm_set_nmi_mask() sets V_NMI_BLOCKING without touching V_NMI_PENDING).

In this state svm_enable_nmi_window() just takes the early return and
hardware delivers the pending vNMI on IRET, so nothing malfunctions -
b4bd55646747 makes the same "still function correctly" note for the
STI-shadow/GIF=0 cases. Drop the WARN_ON_ONCE(), or is reaching this state
(V_NMI_BLOCKING + V_NMI_PENDING set, nmi_pending != 0) itself a bug to fix
elsewhere?

-- 
Thanks,
Vasiliy



More information about the lvc-project mailing list