* [PATCH v2 1/3] KVM: arm64: vgic-v2: Don't drop pending writes to active level IRQs
2026-10-28 5:01 [PATCH v2 0/3] KVM: arm64: Preserve level IRQ re-pending Karl Mehltretter
@ 2026-10-28 5:01 ` Karl Mehltretter
2026-10-28 5:01 ` [PATCH v2 2/3] KVM: arm64: vgic-v3: " Karl Mehltretter
2026-10-28 5:01 ` [PATCH v2 3/3] KVM: selftests: arm64: Test " Karl Mehltretter
2 siblings, 0 replies; 4+ messages in thread
From: Karl Mehltretter @ 2026-10-28 5:01 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Karl Mehltretter, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Mark Rutland, Eric Auger, Paolo Bonzini, Sean Christopherson,
Shuah Khan, linux-arm-kernel, kvmarm, kvm, linux-kselftest,
linux-kernel
A write to GICD_ISPENDR for an active level interrupt makes the
interrupt Active and Pending. It must be delivered again after it is
deactivated.
KVM loses the second pending state. vgic_v2_populate_lr() leaves
pending_latch set for a level interrupt, while vgic_v2_compute_lr()
keeps Pending and Active out of the same LR. vgic_v2_fold_lr() clears
the latch after the LR becomes empty. At that point it cannot tell the
original latch from one written while the interrupt was active, and
clears both.
Synchronize the target vCPU's LRs in vgic_mmio_write_spending() with
vgic_access_active_prepare() and vgic_access_active_finish() under
config_lock before setting pending_latch. When the interrupt is in an
LR on another vCPU, this folds any prior ack into the VGIC state before
the new pending state is recorded.
In vgic_v2_fold_lr(), clear the original latch when an LR populated for
an inactive interrupt no longer reports a pending state, and preserve a
latch written while the interrupt was active.
Fixes: 67b5b673ad4d ("KVM: arm/arm64: vgic: Disallow Active+Pending for level interrupts")
Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
arch/arm64/kvm/vgic/vgic-mmio.c | 76 +++++++++++++++++++--------------
arch/arm64/kvm/vgic/vgic-v2.c | 10 +++--
2 files changed, 50 insertions(+), 36 deletions(-)
diff --git a/arch/arm64/kvm/vgic/vgic-mmio.c b/arch/arm64/kvm/vgic/vgic-mmio.c
index fddb9da0403d5..e42eb10d502b2 100644
--- a/arch/arm64/kvm/vgic/vgic-mmio.c
+++ b/arch/arm64/kvm/vgic/vgic-mmio.c
@@ -350,11 +350,54 @@ static void __set_pending(struct kvm_vcpu *vcpu, gpa_t addr, unsigned int len,
}
}
+/*
+ * If we are fiddling with an IRQ's active state, we have to make sure the IRQ
+ * is not queued on some running VCPU's LRs, because then the change to the
+ * active state can be overwritten when the VCPU's state is synced coming back
+ * from the guest. Setting an IRQ pending also requires the target VCPU's LRs
+ * to be synced first so that any prior ack in the LR is folded
+ * before pending_latch is set.
+ *
+ * For shared interrupts as well as GICv3 private interrupts accessed from the
+ * non-owning CPU, we have to stop all the VCPUs because interrupts can be
+ * migrated while we don't hold the IRQ locks and we don't want to be chasing
+ * moving targets.
+ *
+ * For GICv2 private interrupts we don't have to do anything because
+ * userspace accesses to the VGIC state already require all VCPUs to be
+ * stopped, and only the VCPU itself can modify its private interrupts
+ * active state, which guarantees that the VCPU is not running.
+ */
+static void vgic_access_active_prepare(struct kvm_vcpu *vcpu, u32 intid)
+{
+ if ((vcpu->kvm->arch.vgic.vgic_model == KVM_DEV_TYPE_ARM_VGIC_V3 &&
+ vcpu != kvm_get_running_vcpu()) ||
+ intid >= VGIC_NR_PRIVATE_IRQS)
+ kvm_arm_halt_guest(vcpu->kvm);
+}
+
+/* See vgic_access_active_prepare */
+static void vgic_access_active_finish(struct kvm_vcpu *vcpu, u32 intid)
+{
+ if ((vcpu->kvm->arch.vgic.vgic_model == KVM_DEV_TYPE_ARM_VGIC_V3 &&
+ vcpu != kvm_get_running_vcpu()) ||
+ intid >= VGIC_NR_PRIVATE_IRQS)
+ kvm_arm_resume_guest(vcpu->kvm);
+}
+
void vgic_mmio_write_spending(struct kvm_vcpu *vcpu,
gpa_t addr, unsigned int len,
unsigned long val)
{
+ u32 intid = VGIC_ADDR_TO_INTID(addr, 1);
+
+ mutex_lock(&vcpu->kvm->arch.config_lock);
+ vgic_access_active_prepare(vcpu, intid);
+
__set_pending(vcpu, addr, len, val, false);
+
+ vgic_access_active_finish(vcpu, intid);
+ mutex_unlock(&vcpu->kvm->arch.config_lock);
}
int vgic_uaccess_write_spending(struct kvm_vcpu *vcpu,
@@ -452,39 +495,6 @@ int vgic_uaccess_write_cpending(struct kvm_vcpu *vcpu,
return 0;
}
-/*
- * If we are fiddling with an IRQ's active state, we have to make sure the IRQ
- * is not queued on some running VCPU's LRs, because then the change to the
- * active state can be overwritten when the VCPU's state is synced coming back
- * from the guest.
- *
- * For shared interrupts as well as GICv3 private interrupts accessed from the
- * non-owning CPU, we have to stop all the VCPUs because interrupts can be
- * migrated while we don't hold the IRQ locks and we don't want to be chasing
- * moving targets.
- *
- * For GICv2 private interrupts we don't have to do anything because
- * userspace accesses to the VGIC state already require all VCPUs to be
- * stopped, and only the VCPU itself can modify its private interrupts
- * active state, which guarantees that the VCPU is not running.
- */
-static void vgic_access_active_prepare(struct kvm_vcpu *vcpu, u32 intid)
-{
- if ((vcpu->kvm->arch.vgic.vgic_model == KVM_DEV_TYPE_ARM_VGIC_V3 &&
- vcpu != kvm_get_running_vcpu()) ||
- intid >= VGIC_NR_PRIVATE_IRQS)
- kvm_arm_halt_guest(vcpu->kvm);
-}
-
-/* See vgic_access_active_prepare */
-static void vgic_access_active_finish(struct kvm_vcpu *vcpu, u32 intid)
-{
- if ((vcpu->kvm->arch.vgic.vgic_model == KVM_DEV_TYPE_ARM_VGIC_V3 &&
- vcpu != kvm_get_running_vcpu()) ||
- intid >= VGIC_NR_PRIVATE_IRQS)
- kvm_arm_resume_guest(vcpu->kvm);
-}
-
static unsigned long __vgic_mmio_read_active(struct kvm_vcpu *vcpu,
gpa_t addr, unsigned int len)
{
diff --git a/arch/arm64/kvm/vgic/vgic-v2.c b/arch/arm64/kvm/vgic/vgic-v2.c
index 7182f63fc9382..f3ba3177dd97b 100644
--- a/arch/arm64/kvm/vgic/vgic-v2.c
+++ b/arch/arm64/kvm/vgic/vgic-v2.c
@@ -57,7 +57,7 @@ static void vgic_v2_fold_lr(struct kvm_vcpu *vcpu, u32 val)
{
u32 cpuid, intid = val & GICH_LR_VIRTUALID;
struct vgic_irq *irq;
- bool deactivated;
+ bool deactivated, was_active;
/* Extract the source vCPU id from the LR */
cpuid = FIELD_GET(GICH_LR_PHYSID_CPUID, val) & 7;
@@ -71,7 +71,8 @@ static void vgic_v2_fold_lr(struct kvm_vcpu *vcpu, u32 val)
scoped_guard(raw_spinlock, &irq->irq_lock) {
/* Always preserve the active bit, note deactivation */
- deactivated = irq->active && !(val & GICH_LR_ACTIVE_BIT);
+ was_active = irq->active;
+ deactivated = was_active && !(val & GICH_LR_ACTIVE_BIT);
irq->active = !!(val & GICH_LR_ACTIVE_BIT);
if (irq->active && vgic_irq_is_sgi(intid))
@@ -88,8 +89,11 @@ static void vgic_v2_fold_lr(struct kvm_vcpu *vcpu, u32 val)
/*
* Clear soft pending state when level irqs have been acked.
+ * A latch written while the irq was active is a new pending
+ * state and was not represented in the LR, so preserve it.
*/
- if (irq->config == VGIC_CONFIG_LEVEL && !(val & GICH_LR_STATE))
+ if (irq->config == VGIC_CONFIG_LEVEL &&
+ !(val & GICH_LR_PENDING_BIT) && !was_active)
irq->pending_latch = false;
/* Handle resampling for mapped interrupts if required */
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v2 2/3] KVM: arm64: vgic-v3: Don't drop pending writes to active level IRQs
2026-10-28 5:01 [PATCH v2 0/3] KVM: arm64: Preserve level IRQ re-pending Karl Mehltretter
2026-10-28 5:01 ` [PATCH v2 1/3] KVM: arm64: vgic-v2: Don't drop pending writes to active level IRQs Karl Mehltretter
@ 2026-10-28 5:01 ` Karl Mehltretter
2026-10-28 5:01 ` [PATCH v2 3/3] KVM: selftests: arm64: Test " Karl Mehltretter
2 siblings, 0 replies; 4+ messages in thread
From: Karl Mehltretter @ 2026-10-28 5:01 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Karl Mehltretter, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Mark Rutland, Eric Auger, Paolo Bonzini, Sean Christopherson,
Shuah Khan, linux-arm-kernel, kvmarm, kvm, linux-kselftest,
linux-kernel
The GICv3 backend also loses a level interrupt set pending through
GICD_ISPENDR or GICR_ISPENDR0 while it is active. Its LR handling has
the same split between pending_latch and an Active-only LR as GICv2,
and clears the new pending state after deactivation.
With vgic_mmio_write_spending() synchronizing the target vCPU's LRs
before setting pending_latch (which is a no-op when a vCPU writes its
own GICR_ISPENDR0), apply the GICv2 fold rule to vgic_v3_fold_lr().
Clear the old latch when an LR populated for an inactive interrupt
reports that the interrupt has been acknowledged, and preserve a latch
written while the interrupt was active.
GICv3 permits an implementation to clear register-set pending state on
ack. Doing so matches the GICv2 backend and makes a later ISPENDR write
a distinct pending state.
Fixes: 67b5b673ad4d ("KVM: arm/arm64: vgic: Disallow Active+Pending for level interrupts")
Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
arch/arm64/kvm/vgic/vgic-v3.c | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/arch/arm64/kvm/vgic/vgic-v3.c b/arch/arm64/kvm/vgic/vgic-v3.c
index 726e20a1da6e7..4d802fc9c6dd5 100644
--- a/arch/arm64/kvm/vgic/vgic-v3.c
+++ b/arch/arm64/kvm/vgic/vgic-v3.c
@@ -75,7 +75,7 @@ static void vgic_v3_fold_lr(struct kvm_vcpu *vcpu, u64 val)
{
struct vgic_irq *irq;
bool is_v2_sgi = false;
- bool deactivated;
+ bool deactivated, was_active;
u32 intid;
if (vcpu->kvm->arch.vgic.vgic_model == KVM_DEV_TYPE_ARM_VGIC_V3) {
@@ -93,7 +93,8 @@ static void vgic_v3_fold_lr(struct kvm_vcpu *vcpu, u64 val)
/* Always preserve the active bit for !LPIs, note deactivation */
if (irq->intid >= VGIC_MIN_LPI)
val &= ~ICH_LR_ACTIVE_BIT;
- deactivated = irq->active && !(val & ICH_LR_ACTIVE_BIT);
+ was_active = irq->active;
+ deactivated = was_active && !(val & ICH_LR_ACTIVE_BIT);
irq->active = !!(val & ICH_LR_ACTIVE_BIT);
/* Edge is the only case where we preserve the pending bit */
@@ -103,8 +104,11 @@ static void vgic_v3_fold_lr(struct kvm_vcpu *vcpu, u64 val)
/*
* Clear soft pending state when level irqs have been acked.
+ * A latch written while the irq was active is a new pending
+ * state and was not represented in the LR, so preserve it.
*/
- if (irq->config == VGIC_CONFIG_LEVEL && !(val & ICH_LR_STATE))
+ if (irq->config == VGIC_CONFIG_LEVEL &&
+ !(val & ICH_LR_PENDING_BIT) && !was_active)
irq->pending_latch = false;
if (is_v2_sgi) {
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v2 3/3] KVM: selftests: arm64: Test pending writes to active level IRQs
2026-10-28 5:01 [PATCH v2 0/3] KVM: arm64: Preserve level IRQ re-pending Karl Mehltretter
2026-10-28 5:01 ` [PATCH v2 1/3] KVM: arm64: vgic-v2: Don't drop pending writes to active level IRQs Karl Mehltretter
2026-10-28 5:01 ` [PATCH v2 2/3] KVM: arm64: vgic-v3: " Karl Mehltretter
@ 2026-10-28 5:01 ` Karl Mehltretter
2 siblings, 0 replies; 4+ messages in thread
From: Karl Mehltretter @ 2026-10-28 5:01 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Karl Mehltretter, Fuad Tabba, Joey Gouly, Steffen Eiden,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
Mark Rutland, Eric Auger, Paolo Bonzini, Sean Christopherson,
Shuah Khan, linux-arm-kernel, kvmarm, kvm, linux-kselftest,
linux-kernel
Add a two-vCPU GICv3 test for a level SPI set pending through
GICD_ISPENDR. Re-pend the interrupt while it is active, first from the
handling vCPU and then from the other vCPU. Require a second delivery
after deactivation in both cases.
Also check that the other vCPU observes the initial pending state
before the interrupt is acknowledged, and that a remote GICD_ISPENDR
write before the ack merges with the initial pending state without
causing a second delivery.
Without the VGIC fix, the pending state written while active is cleared
on deactivation and the test fails before the second delivery.
Assisted-by: LLM
Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
tools/testing/selftests/kvm/arm64/vgic_irq.c | 140 +++++++++++++++++++
1 file changed, 140 insertions(+)
diff --git a/tools/testing/selftests/kvm/arm64/vgic_irq.c b/tools/testing/selftests/kvm/arm64/vgic_irq.c
index 3cb85932ee85c..83091e0384c22 100644
--- a/tools/testing/selftests/kvm/arm64/vgic_irq.c
+++ b/tools/testing/selftests/kvm/arm64/vgic_irq.c
@@ -959,6 +959,145 @@ static void guest_code_timer_spi(struct test_args *args, int cpuid)
GUEST_DONE();
}
+#define REPEND_READ 1
+#define REPEND_READ_DONE 2
+#define REPEND_WRITE 3
+#define REPEND_PRE_ACK 4
+#define REPEND_PRE_ACK_DONE 5
+
+/*
+ * A level-sensitive SPI that is set pending through GICD_ISPENDR stays
+ * pending for every vCPU until it is acked. If it is set pending again while
+ * it is active, it becomes active and pending and must be taken a second time
+ * once it has been deactivated. The second pending state comes from the vCPU
+ * that handles the interrupt in the first round, and from the other vCPU in
+ * the second. A remote GICD_ISPENDR write before the interrupt is acked merges
+ * with the initial pending state without causing a second delivery.
+ */
+static void guest_code_level_repend(struct test_args *args, int cpuid)
+{
+ int i;
+
+ gic_init(GIC_V3, 2);
+
+ gic_set_eoi_split(1);
+ gic_set_priority_mask(CPU_PRIO_MASK);
+
+ if (cpuid == 0) {
+ u32 intid;
+
+ local_irq_disable();
+
+ gic_irq_set_config(MIN_SPI, false);
+ gic_set_priority(MIN_SPI, IRQ_DEFAULT_PRIO);
+ gic_irq_enable(MIN_SPI);
+
+ for (i = 0; i < 2; i++) {
+ gic_irq_set_pending(MIN_SPI);
+
+ if (i == 1) {
+ /* Not acked yet: the other vCPU sees it pending */
+ WRITE_ONCE(args->shared_data, REPEND_READ);
+ dsb(ishst);
+
+ do {
+ dsb(ishld);
+ } while (READ_ONCE(args->shared_data) != REPEND_READ_DONE);
+ }
+
+ GUEST_ASSERT(gic_irq_get_pending(MIN_SPI));
+ intid = wait_for_and_activate_irq();
+ GUEST_ASSERT_EQ(intid, MIN_SPI);
+
+ if (i == 0) {
+ gic_irq_set_pending(MIN_SPI);
+ } else {
+ WRITE_ONCE(args->shared_data, REPEND_WRITE);
+ dsb(ishst);
+
+ do {
+ dsb(ishld);
+ } while (READ_ONCE(args->shared_data) == REPEND_WRITE);
+ }
+
+ GUEST_ASSERT(gic_irq_get_active(MIN_SPI));
+ gic_set_eoi(intid);
+ gic_set_dir(intid);
+ isb();
+
+ /* The second pending state must not have been lost */
+ GUEST_ASSERT(gic_irq_get_pending(MIN_SPI));
+ intid = wait_for_and_activate_irq();
+ GUEST_ASSERT_EQ(intid, MIN_SPI);
+
+ gic_set_eoi(intid);
+ gic_set_dir(intid);
+ isb();
+
+ GUEST_ASSERT(!gic_irq_get_active(MIN_SPI));
+ GUEST_ASSERT(!gic_irq_get_pending(MIN_SPI));
+ GUEST_ASSERT_EQ(gic_get_and_ack_irq(), IAR_SPURIOUS);
+ }
+
+ /*
+ * A remote ISPENDR write before the interrupt is acked merges
+ * with the initial pending state and must not cause a second
+ * delivery after deactivation.
+ */
+ gic_irq_set_pending(MIN_SPI);
+
+ WRITE_ONCE(args->shared_data, REPEND_PRE_ACK);
+ dsb(ishst);
+
+ do {
+ dsb(ishld);
+ } while (READ_ONCE(args->shared_data) != REPEND_PRE_ACK_DONE);
+
+ GUEST_ASSERT(gic_irq_get_pending(MIN_SPI));
+ intid = wait_for_and_activate_irq();
+ GUEST_ASSERT_EQ(intid, MIN_SPI);
+
+ gic_set_eoi(intid);
+ gic_set_dir(intid);
+ isb();
+
+ GUEST_ASSERT(!gic_irq_get_active(MIN_SPI));
+ GUEST_ASSERT(!gic_irq_get_pending(MIN_SPI));
+ GUEST_ASSERT_EQ(gic_get_and_ack_irq(), IAR_SPURIOUS);
+ } else {
+ do {
+ dsb(ishld);
+ } while (READ_ONCE(args->shared_data) != REPEND_READ);
+
+ for (i = 0; i < 64; i++)
+ GUEST_ASSERT(gic_irq_get_pending(MIN_SPI));
+
+ WRITE_ONCE(args->shared_data, REPEND_READ_DONE);
+ dsb(ishst);
+
+ do {
+ dsb(ishld);
+ } while (READ_ONCE(args->shared_data) != REPEND_WRITE);
+
+ gic_irq_set_pending(MIN_SPI);
+
+ WRITE_ONCE(args->shared_data, 0);
+ dsb(ishst);
+
+ do {
+ dsb(ishld);
+ } while (READ_ONCE(args->shared_data) != REPEND_PRE_ACK);
+
+ GUEST_ASSERT(gic_irq_get_pending(MIN_SPI));
+ gic_irq_set_pending(MIN_SPI);
+
+ WRITE_ONCE(args->shared_data, REPEND_PRE_ACK_DONE);
+ dsb(ishst);
+ }
+
+ GUEST_DONE();
+}
+
static void *test_vcpu_run(void *arg)
{
struct kvm_vcpu *vcpu = arg;
@@ -1073,6 +1212,7 @@ int main(int argc, char **argv)
test_vgic_two_cpus(guest_code_asym_dir);
test_vgic_two_cpus(guest_code_group_en);
test_vgic_two_cpus(guest_code_timer_spi);
+ test_vgic_two_cpus(guest_code_level_repend);
} else {
test_vgic(nr_irqs, level_sensitive, eoi_split);
}
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 4+ messages in thread