Skip to contentSkip to conversation

KVM: arm64: vgic: fix UAF/crash on remote LPI disable

PATCH Superseded zjamg 5 participants 19 messages Sep 18 – Sep 22

Subsystems (5)
Stable branch KVM Kselftest arm64 +1 more subsystems
lore
Share & tools
lore

zjamg

author
#0 lore raw

v2 cover KVM: arm64: vgic: fix UAF/crash on remote LPI disable

From: Yuchao Zhang <[email protected]>

Hi Marc, Oliver, and KVM/arm64 maintainers,

By code inspection of commit 6da5e537f5af ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail"), a race condition exists when a remote vCPU disables LPIs while the target vCPU has an in-flight LPI in a List Register (LR).

Specifically:
- vgic_flush_pending_lpis() unconditionally unlinks all LPIs from ap_list without checking whether the interrupt is in an LR (irq->on_lr).
- If the LPI in the LR happened to be the last one populated, the per-CPU pointer *host_data_ptr(last_lr_irq) on the target vCPU is left dangling.
- When the target vCPU exits guest mode, vgic_v3_fold_lr_state() starts traversing ap_list via list_for_each_entry_continue() from this unlinked, poisoned (or freed) last_lr_irq, leading to UAF or an immediate panic when locking irq->irq_lock.

Solution & Scope:
This patch prevents unlinking LPIs that are currently on an LR in vgic_flush_pending_lpis(), ensures *host_data_ptr(last_lr_irq) is cleared after folding, skips the ap_list walk when eoicount is zero, and prevents the fold from resurrecting the pending state of an edge LPI once the redistributor has LPIs disabled.

Note: this closes the primary race (the last_lr_irq node itself is no longer unlinkable while in-flight), but the fold traversal can still race with a remote flush unlinking a subsequent non-LR node in the ap_list tail. Fully closing that window needs the fold side to take references before dropping locks (in the spirit of the prune-side fix in commit 7258770e5814 ("KVM: arm64: vgic: Handle race between interrupt affinity change and LPI disabling")) and is left as a follow-up.

Changes in v2:
- Added the fold-side guard: vgic_v3_fold_lr() no longer preserves the pending bit of an edge LPI folded while the redistributor has LPIs disabled. Without this, a flushed in-flight LPI is resurrected from the LR pending bit and re-injected while GICR_CTLR.EnableLPIs is 0 (spurious LPI delivery to the guest), since the injection path has no lpis_enabled gate. Thanks to the Sashiko AI review for pointing this out.

Yuchao Zhang (1):
KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable

 arch/arm64/kvm/vgic/vgic-v2.c |  3 +++
 arch/arm64/kvm/vgic/vgic-v3.c | 14 ++++++++++++--
 arch/arm64/kvm/vgic/vgic.c    | 10 +++++++---
 3 files changed, 22 insertions(+), 5 deletions(-)

--
2.53.0

author
#1 lore raw

PATCH v2 1/1 KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable

From: Yuchao Zhang <[email protected]>

By code inspection, a race condition and potential Use-After-Free/crash exists between remote LPI disabling and local LR folding when EOImode==0.

Commit 6da5e537f5af ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail") introduced tracking the last interrupt placed in a List Register in per-CPU host data (*host_data_ptr(last_lr_irq)) and traverses the remaining ap_list via list_for_each_entry_continue() in vgic_v3_fold_lr_state().

However, an interrupt loaded into an LR can be an LPI. While vCPU-B is running the guest, another vCPU-A can write to vCPU-B's redistributor GICR_CTLR to clear EnableLPIs, which dispatches vgic_flush_pending_lpis().

vgic_flush_pending_lpis() unconditionally removes all LPIs from ap_list without checking whether the interrupt is currently in an LR (irq->on_lr):
- It calls list_del(&irq->ap_list), setting ap_list.next to LIST_POISON1, and drops the AP-list reference.
- If the LPI is unmapped or its translation cache was invalidated, the vgic_irq refcount drops to zero and the object is freed via RCU.
- Meanwhile, vCPU-B's per-CPU *host_data_ptr(last_lr_irq) cannot be cleared by remote vCPUs and is left dangling.

When vCPU-B subsequently exits the guest:
1. vgic_v3_fold_lr_state() resumes using the unlinked last_lr_irq.
2. list_for_each_entry_continue() unconditionally evaluates list_next_entry(irq, ap_list) during loop initialization, accessing LIST_POISON1 (or freed memory).
3. If eoicount > 0, it attempts guard(raw_spinlock)(&irq->irq_lock) on the poisoned address, leading to an immediate host kernel panic.

Fix this by:
1. In vgic_flush_pending_lpis(), do not remove LPIs that are currently in-flight in an LR (irq->on_lr == true). They will be naturally pruned by the owning vCPU's vgic_prune_ap_list() after LR folding.
2. In vgic_fold_state(), clear *host_data_ptr(last_lr_irq) after folding so that no stale pointer survives past guest execution.
3. In vgic_v3_fold_lr_state() and vgic_v2_fold_lr_state(), bail out immediately if eoicount is zero, avoiding unnecessary list_next_entry() evaluation.
4. In vgic_v3_fold_lr(), do not resurrect the pending state of an edge LPI folded while the redistributor has LPIs disabled. vgic_flush_pending_lpis() cannot remotely clear the LRs of a running vCPU, so such an LPI would otherwise be requeued and re-injected while GICR_CTLR.EnableLPIs is 0, violating the flush semantics (and the architecture, which gives no expectation of the pending state being retained across a disable).

Note: this closes the primary race (the last_lr_irq node itself is no longer unlinkable while in-flight), but the fold traversal can still race with a remote flush unlinking a subsequent non-LR node in the ap_list tail. Fully closing that window needs the fold side to take references before dropping locks (in the spirit of the prune-side fix in commit 7258770e5814 ("KVM: arm64: vgic: Handle race between interrupt affinity change and LPI disabling")) and is left as a follow-up.

Fixes 6da5e537f5af ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail")
---
 arch/arm64/kvm/vgic/vgic-v2.c |  3 +++
 arch/arm64/kvm/vgic/vgic-v3.c | 14 ++++++++++++--
 arch/arm64/kvm/vgic/vgic.c    | 10 +++++++---
 3 files changed, 22 insertions(+), 5 deletions(-)
arch/arm64/kvm/vgic/vgic-v2.c +3 -0
index 7182f63fc938..7b6cd05ce32d 100644
@@ -122,6 +122,9 @@ void vgic_v2_fold_lr_state(struct kvm_vcpu *vcpu)
 	for (int lr = 0; lr < vgic_cpu->vgic_v2.used_lrs; lr++)
 		vgic_v2_fold_lr(vcpu, cpuif->vgic_lr[lr]);
 
+	if (!eoicount)
+		return;
+
 	/* See the GICv3 equivalent for the EOIcount handling rationale */
 	list_for_each_entry_continue(irq, &vgic_cpu->ap_list_head, ap_list) {
 		u32 lr;
arch/arm64/kvm/vgic/vgic-v3.c +12 -2
index 726e20a1da6e..dd1314f4543d 100644
@@ -96,9 +96,16 @@ static void vgic_v3_fold_lr(struct kvm_vcpu *vcpu, u64 val)
 		deactivated = irq->active && !(val & ICH_LR_ACTIVE_BIT);
 		irq->active = !!(val & ICH_LR_ACTIVE_BIT);
 
-		/* Edge is the only case where we preserve the pending bit */
+		/*
+		 * Edge is the only case where we preserve the pending bit.
+		 * Do not resurrect the pending state of LPIs once the
+		 * redistributor has them disabled: vgic_flush_pending_lpis()
+		 * has discarded them, and the LRs of a running vCPU cannot
+		 * be remotely cleared.
+		 */
 		if (irq->config == VGIC_CONFIG_EDGE &&
-		    (val & ICH_LR_PENDING_BIT))
+		    (val & ICH_LR_PENDING_BIT) &&
+		    (irq->intid < VGIC_MIN_LPI || vgic_lpis_enabled(vcpu)))
 			irq->pending_latch = true;
 
 		/*
@@ -155,6 +162,9 @@ void vgic_v3_fold_lr_state(struct kvm_vcpu *vcpu)
 	for (int lr = 0; lr < cpuif->used_lrs; lr++)
 		vgic_v3_fold_lr(vcpu, cpuif->vgic_lr[lr]);
 
+	if (!eoicount)
+		return;
+
 	/*
 	 * EOIMode=0: use EOIcount to emulate deactivation. We are
 	 * guaranteed to deactivate in reverse order of the activation, so
arch/arm64/kvm/vgic/vgic.c +7 -3
index b25303d9919f..0d7d75c3ac93 100644
@@ -205,10 +205,12 @@ void vgic_flush_pending_lpis(struct kvm_vcpu *vcpu)
 		if (irq_is_lpi(vcpu->kvm, irq->intid)) {
 			raw_spin_lock(&irq->irq_lock);
 			irq->pending_latch = false;
-			list_del(&irq->ap_list);
-			irq->vcpu = NULL;
+			if (!irq->on_lr) {
+				list_del(&irq->ap_list);
+				irq->vcpu = NULL;
+				deleted |= vgic_put_irq_norelease(vcpu->kvm, irq);
+			}
 			raw_spin_unlock(&irq->irq_lock);
-			deleted |= vgic_put_irq_norelease(vcpu->kvm, irq);
 		}
 	}
 
@@ -873,6 +875,8 @@ static void vgic_fold_state(struct kvm_vcpu *vcpu)
 		vgic_v2_fold_lr_state(vcpu);
 	else
 		vgic_v3_fold_lr_state(vcpu);
+
+	*host_data_ptr(last_lr_irq) = NULL;
 }
 
 /* Requires the irq_lock to be held. */
Signature · 1 line
2.53.0
reviewer
#2 lore raw

Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable

Hi Yuchao,

On Fri, 18 Sep 2026 12:02:14 +0800, Yuchao Zhang <[email protected]> wrote:

[...]

Quoted 8 lines

Note: this closes the primary race (the last_lr_irq node itself is no longer unlinkable while in-flight), but the fold traversal can still race with a remote flush unlinking a subsequent non-LR node in the ap_list tail. Fully closing that window needs the fold side to take references before dropping locks (in the spirit of the prune-side fix in commit 7258770e5814 ("KVM: arm64: vgic: Handle race between interrupt affinity change and LPI disabling")) and is left as a follow-up.

This is the same race Hyunwoo reported back in June [1]. You might want to have a look at that thread first: Oliver and Marc's view there was that the fix is to take the ap_list_lock in vgic_v3_fold_lr_state() [2][3], and Hyunwoo posted a draft of that [4].

Cheers,
/fuad

[1] https://lore.kernel.org/r/aiHrGM1f8czcUby4@v4bel
[2] https://lore.kernel.org/r/[email protected]
[3] https://lore.kernel.org/r/[email protected]
[4] https://lore.kernel.org/r/aiXvwGD1hS6vwLEd@v4bel

#3 lore raw

Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable

Hi Fuad,

Thanks a lot for pointing me to that thread! I was not aware of Hyunwoo's earlier report and the discussion with Oliver and Marc.

I'll read through the thread and their rationale on the ap_list_lock approach. I'm happy to defer to Hyunwoo's effort to avoid duplicate work.

Thanks again for the pointer!

Best regards,
Yuchao

Quoted 29 lines · Fuad Tabba <[email protected]> 于2026年9月18日周五 15:22写道:

Hi Yuchao,

On Fri, 18 Sep 2026 12:02:14 +0800, Yuchao Zhang <[email protected]> wrote:

[...]

Quoted 8 lines

Note: this closes the primary race (the last_lr_irq node itself is no longer unlinkable while in-flight), but the fold traversal can still race with a remote flush unlinking a subsequent non-LR node in the ap_list tail. Fully closing that window needs the fold side to take references before dropping locks (in the spirit of the prune-side fix in commit 7258770e5814 ("KVM: arm64: vgic: Handle race between interrupt affinity change and LPI disabling")) and is left as a follow-up.

This is the same race Hyunwoo reported back in June [1]. You might want to have a look at that thread first: Oliver and Marc's view there was that the fix is to take the ap_list_lock in vgic_v3_fold_lr_state() [2][3], and Hyunwoo posted a draft of that [4].

Cheers,
/fuad

[1] https://lore.kernel.org/r/aiHrGM1f8czcUby4@v4bel
[2] https://lore.kernel.org/r/[email protected]
[3] https://lore.kernel.org/r/[email protected]
[4] https://lore.kernel.org/r/aiXvwGD1hS6vwLEd@v4bel

reviewer
#4 lore raw

Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable

Hi Yuchao,

Quoted 9 lines · Yuchao Zhang

Hi Fuad,

Thanks a lot for pointing me to that thread! I was not aware of Hyunwoo's earlier report and the discussion with Oliver and Marc.

I'll read through the thread and their rationale on the ap_list_lock approach. I'm happy to defer to Hyunwoo's effort to avoid duplicate work.

I'm not sure you should defer to their effort. It doesn't seem like Hyunwoo has done any work on this for a while. I just wanted to point you to the existing discussion.

Cheers,
/fuad

Quoted 35 lines

Thanks again for the pointer!

Best regards,
Yuchao

Quoted 29 lines · Fuad Tabba <[email protected]> 于2026年9月18日周五 15:22写道:

Hi Yuchao,

On Fri, 18 Sep 2026 12:02:14 +0800, Yuchao Zhang <[email protected]> wrote:

[...]

Quoted 8 lines

Note: this closes the primary race (the last_lr_irq node itself is no longer unlinkable while in-flight), but the fold traversal can still race with a remote flush unlinking a subsequent non-LR node in the ap_list tail. Fully closing that window needs the fold side to take references before dropping locks (in the spirit of the prune-side fix in commit 7258770e5814 ("KVM: arm64: vgic: Handle race between interrupt affinity change and LPI disabling")) and is left as a follow-up.

This is the same race Hyunwoo reported back in June [1]. You might want to have a look at that thread first: Oliver and Marc's view there was that the fix is to take the ap_list_lock in vgic_v3_fold_lr_state() [2][3], and Hyunwoo posted a draft of that [4].

Cheers,
/fuad

[1] https://lore.kernel.org/r/aiHrGM1f8czcUby4@v4bel
[2] https://lore.kernel.org/r/[email protected]
[3] https://lore.kernel.org/r/[email protected]
[4] https://lore.kernel.org/r/aiXvwGD1hS6vwLEd@v4bel

maintainer
#5 lore raw

Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable

Quoted 9 lines · Fuad Tabba

Hi Yuchao,

Quoted 6 lines · Yuchao Zhang

Hi Fuad,

Thanks a lot for pointing me to that thread! I was not aware of Hyunwoo's earlier report and the discussion with Oliver and Marc.

Quoted 7 lines
Quoted 3 lines

I'll read through the thread and their rationale on the ap_list_lock approach. I'm happy to defer to Hyunwoo's effort to avoid duplicate work.

I'm not sure you should defer to their effort. It doesn't seem like Hyunwoo has done any work on this for a while. I just wanted to point you to the existing discussion.

Yuchao if you have cycles I would definitely appreciate it if you can pursue a fix. My view hasn't changed since before: let's make that traversal of the ap_list is done under the ap_list_lock, as this is not intended to be walked lock-free.

Taking a step back, the whole cross-vCPU LPI disabling always leaves me feeling ill... Really when RWP=0 becomes visible from another vCPU we need to guarantee that the LPIs have been actually retired, meaning we can't have one sitting in an LR. Even with the locking fix I think we miss this.

Given how unlikely it is for well-behaved software to disable LPIs remotely in the first place, I wonder if we should just halt the VM similar to how we handle accesses to the active state. That's a really big hammer but we've had a lot of bugs in this department and I'm somewhat biased towards an obviously correct solution.

We wouldn't need to do this for a vCPU disabling LPIs on its own redistributor since we've already exited the guest.

I'll think on it a bit more.

Oliver

maintainer
#6 lore raw

Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable

Quoted 30 lines · Oliver Upton
Quoted 9 lines · Fuad Tabba

Hi Yuchao,

Quoted 6 lines · Yuchao Zhang

Hi Fuad,

Thanks a lot for pointing me to that thread! I was not aware of Hyunwoo's earlier report and the discussion with Oliver and Marc.

Quoted 7 lines
Quoted 3 lines

I'll read through the thread and their rationale on the ap_list_lock approach. I'm happy to defer to Hyunwoo's effort to avoid duplicate work.

I'm not sure you should defer to their effort. It doesn't seem like Hyunwoo has done any work on this for a while. I just wanted to point you to the existing discussion.

Yuchao if you have cycles I would definitely appreciate it if you can pursue a fix. My view hasn't changed since before: let's make that traversal of the ap_list is done under the ap_list_lock, as this is not intended to be walked lock-free.

Taking a step back, the whole cross-vCPU LPI disabling always leaves me feeling ill... Really when RWP=0 becomes visible from another vCPU we need to guarantee that the LPIs have been actually retired, meaning we can't have one sitting in an LR. Even with the locking fix I think we miss this.

Quoted 5 lines

Given how unlikely it is for well-behaved software to disable LPIs remotely in the first place, I wonder if we should just halt the VM similar to how we handle accesses to the active state. That's a really big hammer but we've had a lot of bugs in this department and I'm somewhat biased towards an obviously correct solution.

Irrespective of the whole ap_list_lock issue, I think this is the only valid option. Messing with the ap_list of another vcpu while it is running can never result in something that actually works. There's a hack doing that in my tree.

Quoted 2 lines

We wouldn't need to do this for a vCPU disabling LPIs on its own redistributor since we've already exited the guest.

In general, we could stop the target vcpu only. But this is making things more complex, and I quite like the idea of a large hammer... ;-)

	M.
Signature · 1 line
Jazz isn't dead. It just smells funny.
#7 lore raw

v3 cover KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay

In reply to zjamg

Hi Marc, Oliver, Fuad, and KVM/arm64 maintainers,

Following up on the discussion around the remote LPI disable vs LR fold race [1], this series addresses the issue at its root: the last_lr_irq cursor introduced in commit 6da5e537f5af ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail").

Problem:
vgic_v3_fold_lr_state() / vgic_v2_fold_lr_state() walk the overflow tail of the ap_list starting from *host_data_ptr(last_lr_irq) without holding ap_list_lock. Caching this raw pointer across the entire guest execution leaves it vulnerable to concurrent modification: when a remote vCPU disables LPIs via GICR_CTLR, vgic_flush_pending_lpis() unlinks the node with list_del() and drops its reference, leaving last_lr_irq pointing to a poisoned or freed object. When the vCPU exits, the walk dereferences corrupted memory, causing a kernel panic or UAF.

Oliver and Marc suggested taking ap_list_lock in vgic_v3_fold_lr_state() [2][3]. I tried that approach first, but it runs into the following lock-order problems:
1. kvm_notify_acked_irq() grabs regular spinlocks and can re-enter vgic_queue_irq_unlock() (which takes ap_list_lock), causing deadlock.
2. vgic_put_irq() is a no-op for SPI/PPI, but for LPIs it calls refcount_dec_and_lock_irqsave() which acquires dist->lpi_xa.xa_lock when dropping the last reference. That lock sits above ap_list_lock in the lock ordering, so calling vgic_put_irq() under ap_list_lock causes lock inversion.
Addressing these under a global fold lock requires deferring all EOI'ed SPI notifications to a stack bitmap and deferring LPI releases with vgic_put_irq_norelease(), penalizing the fast path for all exits even though folding hardware LRs does not touch ap_list at all. It also still requires pinning and clearing the per-CPU last_lr_irq pointer.

Changes since v2:
- Replaced the skip-unlink approach of v2 with dropping the last_lr_irq cursor entirely and serializing only the overflow EOI replay under ap_list_lock (per Oliver and Marc's suggestion [2][3]).

The cleaner approach in this patch:
1. Drop the fragile last_lr_irq per-CPU cursor entirely.
2. The common fast path (folding hardware LRs) runs natively without ap_list_lock. We record the INTIDs of the used LRs in a small stack array (VGIC_V3_MAX_LRS / VGIC_V2_MAX_LRS entries).
3. If eoicount == 0 (the vast majority of guest exits), clear cpuif->used_lrs = 0 and return immediately without taking ap_list_lock.
4. If unlikely(eoicount > 0), acquire ap_list_lock only to scan the ap_list and pin (via vgic_get_irq_ref) up to eoicount active interrupts that were not in hardware LRs. The scan is a linear walk over at most 16/64 LR INTIDs per candidate, on the rare eoicount > 0 path - bounded and acceptable. lr_intids[] only records INTIDs from the used_lrs range; the extraction mask mirrors vgic_fold_lr() exactly (ICH_LR_VIRTUAL_ID_MASK for GICv3, GICH_LR_VIRTUALID for GICv2), so no stale or invalid slot can produce a false match.
5. Drop ap_list_lock immediately, and then replay their deactivations outside the lock, naturally eliminating both eventfd re-entrancy and lpi_xa lock inversions without changing any function signatures. vgic_fold_lr() has no error path, so cpuif->used_lrs = 0 is always reached after a complete fold, with no risk of partial cleanup.

Note on EOIcount hardware limits:
ICH_HCR_EL2.EOIcount (GICv3) and GICH_HCR.EOICount (GICv2) are both 5-bit fields, giving a maximum value of 31. The targets[32] stack array and min_t(u32, eoicount, ARRAY_SIZE(targets)) bound together ensure no overflow even if hardware writes an unexpected value.

Note on EOIcount source:
For GICv3, eoicount is read from cpuif->vgic_hcr, which is populated by __vgic_v3_save_state right before ICH_HCR_EL2 is cleared in hardware. For GICv2, vgic_v2_save_state reads GICH_HCR via MMIO into the same cpuif->vgic_hcr field (when LRENPIE is set) before writing 0 to GICH_HCR. In both cases the software copy is the only valid source; reading the hardware register after save would return 0.

Note on scope:
This series fixes the use-after-free in the ap_list traversal caused by last_lr_irq. It does not address the separate concern raised by Oliver in [2] about a pending LPI still sitting in an LR when RWP=0 becomes visible to another vCPU; that may require a stronger approach (e.g. halting the VM) and I am happy to follow up separately.

[1] https://lore.kernel.org/r/aiHrGM1f8czcUby4@v4bel
[2] https://lore.kernel.org/r/[email protected]
[3] https://lore.kernel.org/r/[email protected]

Yuchao Zhang (1):
KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay

 arch/arm64/include/asm/kvm_host.h |  3 --
 arch/arm64/kvm/vgic/vgic-v2.c     | 64 +++++++++++++++++++++++--------
 arch/arm64/kvm/vgic/vgic-v3.c     | 74 ++++++++++++++++++++++++++-----
 arch/arm64/kvm/vgic/vgic.c        |  9 +---
 arch/arm64/kvm/vgic/vgic.h        | 14 ++++++++
 5 files changed, 114 insertions(+), 50 deletions(-)
Signature · 1 line
2.53.0
#8 lore raw

PATCH v3 1/1 KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay

Commit 6da5e537f5af ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail") introduced tracking the last interrupt placed in a List Register in per-CPU host data (*host_data_ptr(last_lr_irq)) to resume the EOIcount-based deactivation walk in the overflow tail of the ap_list.

However, caching this raw pointer in per-CPU host data spans the entire guest execution without holding ap_list_lock or a reference count. A concurrent vCPU can disable LPIs by writing to GICR_CTLR.EnableLPIs, triggering vgic_flush_pending_lpis() which removes LPIs with list_del() and drops their reference. When the victim vCPU exits, the lock-free list_for_each_entry_continue() starting from last_lr_irq dereferences a poisoned next pointer or freed object, causing a host panic or UAF.

Rather than taking ap_list_lock across the entire fold path (which forces deferring kvm_notify_acked_irq() to avoid eventfd deadlocks and deferring vgic_put_irq() to avoid lock inversion with lpi_xa), solve this by dropping the fragile last_lr_irq cursor entirely:

1. In vgic_flush_lr_state(), do not track last_lr_irq.
2. In vgic_v3_fold_lr_state() / vgic_v2_fold_lr_state(), fold the hardware LRs natively without ap_list_lock. Record the INTIDs of the used LRs in a small stack array (at most 16/64 entries).
3. If eoicount == 0 (the vast majority of exits), clear cpuif->used_lrs and return immediately without taking ap_list_lock on the fast path.
4. If unlikely(eoicount > 0), acquire ap_list_lock only to scan the ap_list and pin up to eoicount active interrupts that were not in hardware LRs. The scan is a linear walk over at most 16/64 LR INTIDs per candidate, on the rare eoicount > 0 path - bounded and acceptable.
Then drop ap_list_lock before replaying their deactivations, naturally avoiding both eventfd re-entrancy and lpi_xa lock inversions.

Fixes 6da5e537f5af ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail")
---
 arch/arm64/include/asm/kvm_host.h |  3 --
 arch/arm64/kvm/vgic/vgic-v2.c     | 64 +++++++++++++++++++-------
 arch/arm64/kvm/vgic/vgic-v3.c     | 74 +++++++++++++++++++++----------
 arch/arm64/kvm/vgic/vgic.c        |  9 +---
 arch/arm64/kvm/vgic/vgic.h        | 14 ++++++
 5 files changed, 114 insertions(+), 50 deletions(-)
arch/arm64/include/asm/kvm_host.h +0 -3
index 27fe0cd5b2d7..c4355ae23e98 100644
@@ -800,9 +800,6 @@ struct kvm_host_data {
 	unsigned int debug_brps;
 	unsigned int debug_wrps;
 
-	/* Last vgic_irq part of the AP list recorded in an LR */
-	struct vgic_irq *last_lr_irq;
-
 	/* PPI state tracking for GICv5-based guests */
 	struct {
 		DECLARE_BITMAP(pendr, VGIC_V5_NR_PRIVATE_IRQS);
arch/arm64/kvm/vgic/vgic-v2.c +49 -15

86 lines not on this page · raw message

arch/arm64/kvm/vgic/vgic-v3.c +50 -24

97 lines not on this page · raw message

arch/arm64/kvm/vgic/vgic.c +1 -8
index b25303d9919f..966948fd3ddd 100644
@@ -866,9 +866,6 @@ static void vgic_fold_state(struct kvm_vcpu *vcpu)
 		return;
 	}
 
-	if (!*host_data_ptr(last_lr_irq))
-		return;
-
 	if (kvm_vgic_global_state.type == VGIC_V2)
 		vgic_v2_fold_lr_state(vcpu);
 	else
@@ -1015,14 +1012,10 @@ static void vgic_flush_lr_state(struct kvm_vcpu *vcpu)
 	if (irqs_outside_lrs(&als))
 		vgic_sort_ap_list(vcpu);
 
-	*host_data_ptr(last_lr_irq) = NULL;
-
 	list_for_each_entry(irq, &vgic_cpu->ap_list_head, ap_list) {
 		scoped_guard(raw_spinlock,  &irq->irq_lock) {
-			if (likely(vgic_target_oracle(irq) == vcpu)) {
+			if (likely(vgic_target_oracle(irq) == vcpu))
 				vgic_populate_lr(vcpu, irq, count++);
-				*host_data_ptr(last_lr_irq) = irq;
-			}
 		}
 
 		if (count == kvm_vgic_global_state.nr_lr)
arch/arm64/kvm/vgic/vgic.h +14 -0
index b71d486ae514..35a07b320be4 100644
@@ -334,6 +334,20 @@ static inline void vgic_get_irq_ref(struct vgic_irq *irq)
 	WARN_ON_ONCE(!vgic_try_get_irq_ref(irq));
 }
 
+/*
+ * Linear scan over at most VGIC_V3_MAX_LRS / VGIC_V2_MAX_LRS entries.
+ * Only called on the rare eoicount > 0 path, so the O(n) cost is
+ * acceptable and avoids the complexity of a bitmap.
+ */
+static inline bool intid_in_lrs(u32 intid, const u32 *lr_intids, int nr_lrs)
+{
+	for (int i = 0; i < nr_lrs; i++) {
+		if (lr_intids[i] == intid)
+			return true;
+	}
+	return false;
+}
+
 void vgic_v3_fold_lr_state(struct kvm_vcpu *vcpu);
 void vgic_v3_populate_lr(struct kvm_vcpu *vcpu, struct vgic_irq *irq, int lr);
 void vgic_v3_clear_lr(struct kvm_vcpu *vcpu, int lr);
Signature · 1 line
2.53.0
maintainer
#9 lore raw

Re: [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay

In reply to Yuchao Zhang
Quoted 8 lines · Yuchao Zhang

Hi Marc, Oliver, Fuad, and KVM/arm64 maintainers,

Following up on the discussion around the remote LPI disable vs LR fold race [1], this series addresses the issue at its root: the last_lr_irq cursor introduced in commit 6da5e537f5af ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail").

Quoted 9 lines

Problem:
vgic_v3_fold_lr_state() / vgic_v2_fold_lr_state() walk the overflow tail of the ap_list starting from *host_data_ptr(last_lr_irq) without holding ap_list_lock. Caching this raw pointer across the entire guest execution leaves it vulnerable to concurrent modification: when a remote vCPU disables LPIs via GICR_CTLR, vgic_flush_pending_lpis() unlinks the node with list_del() and drops its reference, leaving last_lr_irq pointing to a poisoned or freed object. When the vCPU exits, the walk dereferences corrupted memory, causing a kernel panic or UAF.

Why can't this be solved by simply taking a reference on the object pointed to by last_lr_irq?

Quoted 11 lines

Oliver and Marc suggested taking ap_list_lock in vgic_v3_fold_lr_state() [2][3]. I tried that approach first, but it runs into the following lock-order problems:
1. kvm_notify_acked_irq() grabs regular spinlocks and can re-enter vgic_queue_irq_unlock() (which takes ap_list_lock), causing deadlock.
2. vgic_put_irq() is a no-op for SPI/PPI, but for LPIs it calls refcount_dec_and_lock_irqsave() which acquires dist->lpi_xa.xa_lock when dropping the last reference. That lock sits above ap_list_lock in the lock ordering, so calling vgic_put_irq() under ap_list_lock causes lock inversion.

Which is why we have vgic_put_irq_norelease() and vgic_release_deleted_lpis(), which allow deferring the release until we're in a suitable context. But that's beside the point.

Quoted 2 lines

Addressing these under a global fold lock requires deferring all EOI'ed SPI notifications to a stack bitmap and deferring LPI releases with

Which stack bitmap?

Quoted 3 lines

vgic_put_irq_norelease(), penalizing the fast path for all exits even though folding hardware LRs does not touch ap_list at all. It also still requires pinning and clearing the per-CPU last_lr_irq pointer.

An uncontended atomic access is hardly an overhead, is it? Where is the overhead? And I don't understand what you're saying about the LRs not affecting the ap_list... They *always* do.

Quoted 11 lines

Changes since v2:
- Replaced the skip-unlink approach of v2 with dropping the last_lr_irq cursor entirely and serializing only the overflow EOI replay under ap_list_lock (per Oliver and Marc's suggestion [2][3]).

The cleaner approach in this patch:
1. Drop the fragile last_lr_irq per-CPU cursor entirely.
2. The common fast path (folding hardware LRs) runs natively without ap_list_lock. We record the INTIDs of the used LRs in a small stack array (VGIC_V3_MAX_LRS / VGIC_V2_MAX_LRS entries).

Why is v2 even under consideration? LPIs are strictly v3 (ignoring v5 here), and non-LPIs are statically allocated, meaning they can't vanish under your feet.

Quoted 6 lines

3. If eoicount == 0 (the vast majority of guest exits), clear cpuif->used_lrs = 0 and return immediately without taking ap_list_lock.
4. If unlikely(eoicount > 0), acquire ap_list_lock only to scan the ap_list and pin (via vgic_get_irq_ref) up to eoicount active interrupts that were not in hardware LRs. The scan is a linear walk over at most 16/64 LR INTIDs per candidate, on the rare eoicount > 0 path - bounded and

What makes you think this is acceptable? It really isn't. The point is that this is not limited to 16 entries. That's the whole point of EOIcount, which spans up to 31 simultaneously active priorities.

I have no idea what you describe is achieving, TBH. And looking at the patch, I see a quadratic behaviour, which doesn't strike me as low overhead...

Quoted 24 lines

acceptable. lr_intids[] only records INTIDs from the used_lrs range; the extraction mask mirrors vgic_fold_lr() exactly (ICH_LR_VIRTUAL_ID_MASK for GICv3, GICH_LR_VIRTUALID for GICv2), so no stale or invalid slot can produce a false match.
5. Drop ap_list_lock immediately, and then replay their deactivations outside the lock, naturally eliminating both eventfd re-entrancy and lpi_xa lock inversions without changing any function signatures. vgic_fold_lr() has no error path, so cpuif->used_lrs = 0 is always reached after a complete fold, with no risk of partial cleanup.

Note on EOIcount hardware limits:
ICH_HCR_EL2.EOIcount (GICv3) and GICH_HCR.EOICount (GICv2) are both 5-bit fields, giving a maximum value of 31. The targets[32] stack array and min_t(u32, eoicount, ARRAY_SIZE(targets)) bound together ensure no overflow even if hardware writes an unexpected value.

Note on EOIcount source:
For GICv3, eoicount is read from cpuif->vgic_hcr, which is populated by __vgic_v3_save_state right before ICH_HCR_EL2 is cleared in hardware. For GICv2, vgic_v2_save_state reads GICH_HCR via MMIO into the same cpuif->vgic_hcr field (when LRENPIE is set) before writing 0 to GICH_HCR. In both cases the software copy is the only valid source; reading the hardware register after save would return 0.

Quoted 6 lines

Note on scope:
This series fixes the use-after-free in the ap_list traversal caused by last_lr_irq. It does not address the separate concern raised by Oliver in [2] about a pending LPI still sitting in an LR when RWP=0 becomes visible to another vCPU; that may require a stronger approach (e.g. halting the VM) and I am happy to follow up separately.

This has all the hallmarks of an AI gone wild.

The problem is correctly described (last_lr_irq doesn't hold a reference on the irq), but the proposed solution is completely ignoring it, and implements... something else.

I've hacked something at [1], which:

- changes the behaviour of last_lr_irq to only be non-NULL when the LRs are full.

- take a reference on the irq flagged as last_lr_irq, and drop this reference in vgic_prune_ap_list(), contributing to the LPIs being freed once the ap_list_lock is dropped.

- stop the world when a vcpu disable LPIs. We could do slightly better, but it isn't worth the hassle for something that *never* happens.

I only boot tested a small nested guest (L1 + L2) with lockdep on my laptop, and nothing caught fire. Please give it a go.

You also seem to have a reproducer for this, it'd be good if you could turn it into a selftest.

Thanks,

	M.

[1] https://web.git.kernel.org/pub/scm/linux/kernel/git/maz/arm-platforms.git/log/?h=kvm-arm64/vgic-last_lr_irq-fixes

Signature · 1 line
Jazz isn't dead. It just smells funny.
#10 lore raw

Re: [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay

Hi Marc,

Thank you for the candid feedback and for cutting right to the heart of the problem with the 3-patch series on your branch.

Your series is vastly simpler and elegant:
1. Only caching last_lr_irq when LRs are genuinely full eliminates the overhead for non-overflow exits.
2. Holding a proper reference on last_lr_irq and releasing it under ap_list_lock in vgic_prune_ap_list() cleanly addresses the lifetime hazard without introducing lock-order inversions during folding.
3. Halting the VM (kvm_arm_halt_guest) on remote GICR_CTLR.EnableLPIs=0 completely avoids cross-vCPU concurrency for an event that almost never happens in practice.

As requested, I wrote a KVM arm64 selftest for this scenario:
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c

The test:
- Configures an ITS with multiple LPIs targeting vCPU 0.
- Keeps vCPU 0 in guest mode with EOImode=0 while injecting bursts of MSIs to force its List Registers to overflow into the ap_list.
- Concurrently, vCPU 1 repeatedly toggles GICR_CTLR.EnableLPIs on vCPU 0's redistributor.

Regarding testing: I currently no longer have access to KVM-capable hardware (my test setup is no longer available), so I cannot provide a Tested-by for your series. The race itself was originally identified through code inspection rather than a standalone reproducer, which is why I turned the theoretical scenario directly into this selftest.

The selftest builds cleanly as part of the full arm64 kvm selftest suite, and its framework plumbing was smoke-tested under QEMU TCG, which by construction cannot exercise the race itself. I'd be grateful if you could give it a spin on your setup.

The selftest patch is based on top of your kvm-arm64/vgic-last_lr_irq-fixes branch and has been sent in reply to this thread.

Thanks,
Yuchao

#11 lore raw

PATCH 1/1 KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

In reply to Marc Zyngier

Add a selftest that validates the behavior of remote LPI disabling while the target vCPU has in-flight/overflowing LPIs.

The test configures an ITS with multiple LPIs targeting vCPU 0, which receives a continuous stream of MSIs forcing its List Registers to overflow into the ap_list. Concurrently, vCPU 1 repeatedly toggles GICR_CTLR.EnableLPIs on vCPU 0's redistributor.

On unpatched kernels, this race can lead to a use-after-free or host kernel panic in vgic_fold_lr_state() due to a dangling last_lr_irq pointer. With the fix in place (stopping the VM and holding a refcount on last_lr_irq), the test runs to completion without errors.

---
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 .../selftests/kvm/arm64/vgic_lpi_disable.c    | 401 ++++++++++++++++++
 2 files changed, 402 insertions(+)
 create mode 100644 tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c
tools/testing/selftests/kvm/Makefile.kvm +1 -0
index 96bab7002d39..cf0baec3f6c7 100644
@@ -190,6 +190,7 @@ TEST_GEN_PROGS_arm64 += arm64/vcpu_width_config
 TEST_GEN_PROGS_arm64 += arm64/vgic_init
 TEST_GEN_PROGS_arm64 += arm64/vgic_irq
 TEST_GEN_PROGS_arm64 += arm64/vgic_lpi_stress
+TEST_GEN_PROGS_arm64 += arm64/vgic_lpi_disable
 TEST_GEN_PROGS_arm64 += arm64/vgic_v5
 TEST_GEN_PROGS_arm64 += arm64/vpmu_counter_access
 TEST_GEN_PROGS_arm64 += arm64/no-vgic
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +401 -0

401 lines not on this page · raw message

Signature · 1 line
2.53.0
#12 lore raw

Re: [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Medium] kvm: selftests: buffer overflow in configure_lpis() via large event_ids - [Medium] kvm…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] kvm: selftests: buffer overflow in configure_lpis() via large event_ids
- [Medium] kvm: selftests: ITS command error due to INVALL/SYNC on unmapped collection
- [Medium] kvm: selftests: silently ignored MSI injection failures in signal_lpi() --

commit c5682a23bdbe7c62a7e95d362e2d340ff25c1e42
Author: Yuchao Zhang <[email protected]>

KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

This commit adds a selftest to validate the behavior of remote LPI disabling while the target vCPU has in-flight or overflowing LPIs. It configures an ITS with multiple LPIs targeting vCPU 0, while vCPU 1 toggles GICR_CTLR.EnableLPIs.

Quoted 6 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +0 -0
new file mode 100644
index 0000000000000..0781349192288
@@ -0,0 +1,401 @@

[ ... ]

Quoted 10 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +10 -0
+static void guest_setup_its_mappings(void)
+{
+	u32 device_id, event_id, intid = GIC_LPI_OFFSET;
+	u32 nr_events = test_data.nr_event_ids;
+	u32 nr_devices = test_data.nr_devices;
+
+	/* Map collection 0 to TARGET_VCPU_ID */
+	its_send_mapc_cmd(test_data.cmdq_base_va, TARGET_VCPU_ID, TARGET_VCPU_ID, true);
+
+	/* Map all LPIs to TARGET_VCPU_ID to force LR overflow */

[ ... ]

tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c line 99

Quoted 7 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +7 -0
+static void guest_invalidate_all_rdists(void)
+{
+	int i;
+
+	for (i = 0; i < test_data.nr_cpus; i++)
+		its_send_invall_cmd(test_data.cmdq_base_va, i);
+}

[Severity: Medium]
Will sending an INVALL command for CPU ID 1 act as a command error since collection 1 was never mapped?

In guest_setup_its_mappings(), only collection 0 is mapped to TARGET_VCPU_ID, but this loop iterates over all CPUs (collections 0 and 1) and sends INVALL.

[ ... ]

Quoted 2 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +2 -0
+static void guest_setup_gic(void)
+{

[ ... ]

tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c line 128

Quoted 7 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +7 -0
+	guest_setup_its_mappings();
+	guest_invalidate_all_rdists();
+
+	/* SYNC to ensure ITS setup is complete */
+	for (cpuid = 0; cpuid < test_data.nr_cpus; cpuid++)
+		its_send_sync_cmd(test_data.cmdq_base_va, cpuid);
+}

[Severity: Medium]
Similarly, does issuing a SYNC command for cpuid 1 cause an architectural command error here, given that collection 1 remains unmapped?

This might cause the virtual ITS command queue to stall.

[ ... ]

tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c line 207

Quoted 12 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +12 -0
+static void configure_lpis(void)
+{
+	size_t nr_lpis = test_data.nr_devices * test_data.nr_event_ids;
+	u8 *tbl = addr_gpa2hva(vm, test_data.lpi_prop_table);
+	size_t i;
+
+	for (i = 0; i < nr_lpis; i++) {
+		tbl[i] = LPI_PROP_DEFAULT_PRIO |
+			 LPI_PROP_GROUP1 |
+			 LPI_PROP_ENABLED;
+	}
+}

[Severity: Medium]
Can this loop overflow the lpi_prop_table allocation?

The table is allocated a fixed 64KB size in setup_test_data(), but test_data.nr_event_ids is read directly from the command line argument in main() via atoi_positive(). A large user input could cause an out-of-bounds write here since the value isn't bounds-checked to fit within the region.

[ ... ]

Quoted 4 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +4 -0
+static void signal_lpi(u32 device_id, u32 event_id)
+{
+	gpa_t db_addr = GITS_BASE_GPA + GITS_TRANSLATER;
+

tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c line 261

Quoted 10 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +10 -0
+	struct kvm_msi msi = {
+		.address_lo	= db_addr,
+		.address_hi	= db_addr >> 32,
+		.data		= event_id,
+		.devid		= device_id,
+		.flags		= KVM_MSI_VALID_DEVID,
+	};
+
+	__vm_ioctl(vm, KVM_SIGNAL_MSI, &msi);
+}

[Severity: Medium]
Is it safe to ignore the return value of __vm_ioctl() here?

If KVM_SIGNAL_MSI fails (for example, if the ITS command queue stalls due to the unmapped collection mentioned earlier), the failure is silently masked. This could cause the test to spin without triggering the intended overflow path, falsely reporting success on a broken test environment.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
#13 lore raw

Re: [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

Thanks for the Sashiko review. All three points are legitimate; v2 addresses them:

1. INVALL/SYNC on unmapped collection:
Agreed - INVALL for collection 1 was a command error that stalls the virtual ITS queue. v2 only sends INVALL and SYNC for TARGET_VCPU_ID (the only mapped collection and the only vCPU receiving ITS commands); unmapped collections and untouched vCPUs are skipped.

2. configure_lpis() overflow via -e:
Agreed. v2 bounds-checks the -e argument in main() (must fit in one 64K ITT page) and adds an explicit assertion in configure_lpis() that nr_lpis <= SZ_64K to guarantee the property table is never overrun.

3. Silently ignored KVM_SIGNAL_MSI failures:
Agreed in spirit. Injection failures are expected during the brief window where the disable path has invalidated the ITS caches and the guest has not yet remapped them. v2 counts successful injections (checking KVM_SIGNAL_MSI return value > 0) and asserts at the end that at least one succeeded, so a permanently broken environment can no longer yield a false pass.

Additionally, v2 re-establishes the ITS mappings after every EnableLPIs toggle. Without this, the disable path's cache invalidation kills all mappings after the first iteration and the MSI flood goes silent - now the overflow window exists on every iteration, matching the re-initialisation sequence a real guest performs on re-enable.

Tested status is unchanged: no KVM-capable hardware available, so compile- and TCG-plumbing-tested only; no Tested-by.

pw-bot: cr

Thanks,
Yuchao

#14 lore raw

PATCH v2 1/1 KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

In reply to [email protected]

Add a selftest that validates the behavior of remote LPI disabling while the target vCPU has in-flight/overflowing LPIs.

The test configures an ITS with multiple LPIs targeting vCPU 0, which receives a continuous stream of MSIs forcing its List Registers to overflow into the ap_list. Concurrently, vCPU 1 repeatedly toggles GICR_CTLR.EnableLPIs on vCPU 0's redistributor.

On unpatched kernels, this race can lead to a use-after-free or host kernel panic in vgic_fold_lr_state() due to a dangling last_lr_irq pointer. With the fix in place (stopping the VM and holding a refcount on last_lr_irq), the test runs to completion without errors.

---
v2:
- Only send INVALL and SYNC for the mapped collection / target vCPU; commands targeting unmapped collections or untouched vCPUs are unnecessary and can stall the virtual ITS queue (Sashiko review).
- Re-establish the ITS mappings after every EnableLPIs toggle, since the disable path invalidates the ITS caches and the MSI flood would otherwise go silent after the first iteration.
- Bounds-check the -e argument and assert nr_lpis <= SZ_64K in configure_lpis() so it cannot overflow the 64K ITT or LPI prop tables (Sashiko review).
- Correctly check KVM_SIGNAL_MSI return value (>0 on success) and count successful injections, asserting at the end that at least one succeeded (Sashiko review).

 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 .../selftests/kvm/arm64/vgic_lpi_disable.c    | 430 ++++++++++++++++++
 2 files changed, 431 insertions(+)
 create mode 100644 tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c
tools/testing/selftests/kvm/Makefile.kvm +1 -0
index 96bab7002d39..cf0baec3f6c7 100644
@@ -190,6 +190,7 @@ TEST_GEN_PROGS_arm64 += arm64/vcpu_width_config
 TEST_GEN_PROGS_arm64 += arm64/vgic_init
 TEST_GEN_PROGS_arm64 += arm64/vgic_irq
 TEST_GEN_PROGS_arm64 += arm64/vgic_lpi_stress
+TEST_GEN_PROGS_arm64 += arm64/vgic_lpi_disable
 TEST_GEN_PROGS_arm64 += arm64/vgic_v5
 TEST_GEN_PROGS_arm64 += arm64/vpmu_counter_access
 TEST_GEN_PROGS_arm64 += arm64/no-vgic
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +430 -0

430 lines not on this page · raw message

Signature · 1 line
2.53.0
#15 lore raw

Re: [PATCH v2] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] selftests: kvm: missing mandatory kvm_arch_has_default_irqchip() check --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests: kvm: missing mandatory kvm_arch_has_default_irqchip() check --

commit 41b2f5071c9e0e643d02d3f1806d75461776914a
Author: Yuchao Zhang <[email protected]>

KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

This patch adds a selftest to validate the behavior of remote LPI disabling while the target vCPU has in-flight or overflowing LPIs. It configures an ITS with multiple LPIs targeting vCPU 0, forcing its List Registers to overflow, while vCPU 1 repeatedly toggles GICR_CTLR.EnableLPIs on vCPU 0's redistributor.

Quoted 5 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +0 -0
new file mode 100644
index 0000000000000..d3480369d7607

[ ... ]

tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c line 398

Quoted 5 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +5 -0
+int main(int argc, char **argv)
+{
+	int opt;
+
+	TEST_REQUIRE(kvm_supports_vgic_v3());

[Severity: Medium]
Should this code also include a check for kvm_arch_has_default_irqchip()?

The KVM subsystem guidelines mandate that any selftest requiring an in-kernel IRQ chip must call TEST_REQUIRE(kvm_arch_has_default_irqchip()). This ensures the test is correctly skipped on platforms without default IRQ chip support.

Quoted 3 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +3 -0
+
+	while ((opt = getopt(argc, argv, "i:e:h")) != -1) {
+		switch (opt) {
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
#16 lore raw

Re: [PATCH v2] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

Thanks for the review.

The test already gates on TEST_REQUIRE(kvm_supports_vgic_v3()), which is the check used by every vgic test in arm64/ (e.g. vgic_lpi_stress.c, vgic_irq.c, arch_timer.c).

On arm64, kvm_arch_has_default_irqchip() is request_vgic && kvm_supports_vgic_v3(), and the VM here is created with vm_create_with_vcpus(), so request_vgic is set and the two checks are equivalent. The arch-neutral helper is for the top-level cross-arch tests; no change needed.

Thanks,
Yuchao

Quoted 41 lines · <[email protected]> 于2026年9月22日周二 19:03写道:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests: kvm: missing mandatory kvm_arch_has_default_irqchip() check --

commit 41b2f5071c9e0e643d02d3f1806d75461776914a
Author: Yuchao Zhang <[email protected]>

KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

This patch adds a selftest to validate the behavior of remote LPI disabling while the target vCPU has in-flight or overflowing LPIs. It configures an ITS with multiple LPIs targeting vCPU 0, forcing its List Registers to overflow, while vCPU 1 repeatedly toggles GICR_CTLR.EnableLPIs on vCPU 0's redistributor.

Quoted 5 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +0 -0
new file mode 100644
index 0000000000000..d3480369d7607

[ ... ]

tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c line 398

Quoted 5 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +5 -0
+int main(int argc, char **argv)
+{
+     int opt;
+
+     TEST_REQUIRE(kvm_supports_vgic_v3());

[Severity: Medium]
Should this code also include a check for kvm_arch_has_default_irqchip()?

The KVM subsystem guidelines mandate that any selftest requiring an in-kernel IRQ chip must call TEST_REQUIRE(kvm_arch_has_default_irqchip()). This ensures the test is correctly skipped on platforms without default IRQ chip support.

tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c line 401

Quoted 3 lines
tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c new file +3 -0
+
+     while ((opt = getopt(argc, argv, "i:e:h")) != -1) {
+             switch (opt) {

--
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1

reviewer
#17 lore raw

Re: [PATCH v2] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race

In reply to Yuchao Zhang

Hi Yuchao,

The test passes on an unpatched 7.3-rc3 for me, so it doesn't reproduce the bug yet.

On Tue, 22 Sep 2026 11:53:31 +0100, Yuchao Zhang <[email protected]> wrote:
[...]

Quoted 4 lines

On unpatched kernels, this race can lead to a use-after-free or host kernel panic in vgic_fold_lr_state() due to a dangling last_lr_irq pointer. With the fix in place (stopping the VM and holding a refcount on last_lr_irq), the test runs to completion without errors.

With only LPIs in flight EOIcount stays 0, so the fold breaks out of the ap_list walk before it dereferences anything past the stale last_lr_irq. To crash, the target vcpu has to EOI a non-LPI interrupt that's active outside the LRs, with the remote disable landing while it's still in the guest.

Cheers,
/fuad

reviewer Reviewed-by Tested-by
#18 lore raw

Re: [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay

In reply to Marc Zyngier

Hi Marc,

On Mon, 21 Sep 2026 00:47:20 +0100, Marc Zyngier <[email protected]> wrote:
[...]

Quoted 2 lines

I only boot tested a small nested guest (L1 + L2) with lockdep on my laptop, and nothing caught fire. Please give it a go.

FWIW, looks good to me. Tested on QEMU (VHE and nVHE hosts, KASAN and lockdep on) and on an M4 (pKVM nVHE host on Apple's nested virt): the pKVM guest boots are fine and Yuchao's selftest passes, though the selftest passes on plain rc3 as well, so it doesn't exercise the fix yet. I'll reply on the selftest thread with why.

Cheers,
/fuad