[PATCH] KVM: x86: Reevaluate pending interrupts when enabling ExtINT through LVT0

From: Matthew Leach

Date: Thu Oct 08 2026 - 08:53:49 EST


If the PIC output becomes asserted while LVT0 is masked, unmasking LVT0
in ExtINT mode makes that interrupt injectable without setting
KVM_REQ_EVENT. Without another event triggering reevaluation, the
interrupt remains pending and a guest waiting for timer ticks can hang.

Fix by setting KVM_REQ_EVENT on an LVT0 write where pic interrupts
become acceptable.

Signed-off-by: Matthew Leach <matthew.leach@xxxxxxxxxxxxx>
---
During calibrate_delay_converge() we spin waiting for jiffies to increment on a
tick interrupt. However, if KVM fails to deliver the interrupt this causes the
guest to hang, spinning on the while loop.

Reproduce with:

qemu-system-x86_64 \
-cpu host \
-accel kvm \
-smp 1 \
-machine pc,hpet=off \
-kernel <bzimage> \
-nographic \
-append "console=ttyS0,115200 noapic apic=debug no-kvmclock clearcpuid=tsc"

Resulting in a hang at:

printk: legacy console [ttyS0] enabled
printk: legacy console [ttyS0] enabled
printk: legacy bootconsole [uart8250] disabled
printk: legacy bootconsole [uart8250] disabled
ACPI: Core revision 20260408
ACPI: setting ELCR to 0200 (from 0c00)
APIC: Switch to symmetric I/O mode setup
Not enabling interrupt remapping due to skipped IO-APIC setup
Enabled ExtINT on CPU#0

Note that the bug manifests itself only if a PIC interrupt is asserting during
the mask window, so it is timing based. Using nested virt helps, or apply the
following patch to the guest to provide enough time for a tick after masking:

--8<---------------cut here---------------start------------->8---
diff --git a/arch/x86/kernel/apic/apic.c b/arch/x86/kernel/apic/apic.c
index aa1e19979aa8..ddee09360dd0 100644
--- a/arch/x86/kernel/apic/apic.c
+++ b/arch/x86/kernel/apic/apic.c
@@ -1509,7 +1509,7 @@ static void apic_clear_isr(void)
static void setup_local_APIC(void)
{
int cpu = smp_processor_id();
- unsigned int value;
+ unsigned int value, i;

if (apic_is_disabled) {
disable_ioapic_support();
@@ -1527,6 +1527,9 @@ static void setup_local_APIC(void)
value &= ~APIC_SPIV_APIC_ENABLED;
apic_write(APIC_SPIV, value);

+ for (i = 0; i < 10000000UL; i++)
+ cpu_relax();
+
#ifdef CONFIG_X86_32
/* Pound the ESR really hard over the head with a big hammer - mbligh */
if (lapic_is_integrated() && apic->disable_esr) {
--8<---------------cut here---------------end--------------->8---
---
arch/x86/kvm/lapic.c | 8 ++++++++
1 file changed, 8 insertions(+)

diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
index e1f3cea14765..ce7fbd6c45c2 100644
--- a/arch/x86/kvm/lapic.c
+++ b/arch/x86/kvm/lapic.c
@@ -2520,6 +2520,14 @@ static int kvm_lapic_reg_write(struct kvm_lapic *apic, u32 reg, u32 val)
val |= APIC_LVT_MASKED;
val &= apic_lvt_mask[index];
kvm_lapic_set_reg(apic, reg, val);
+
+ /*
+ * Enabling ExtINT can make an already-asserted PIC interrupt
+ * deliverable without another transition on the PIC output.
+ */
+ if (reg == APIC_LVT0 && kvm_apic_accept_pic_intr(apic->vcpu))
+ kvm_make_request(KVM_REQ_EVENT, apic->vcpu);
+
break;
}


---
base-commit: a90ee4305c4a5df72c11b31dacfdc76e00fcf78a
change-id: 20261008-lapic-extint-lvt0-unmask-fix-52d2328af682

Best regards,
--
Matt