* [PATCH v2 0/1] KVM: arm64: vgic: fix UAF/crash on remote LPI disable
@ 2026-09-18 4:02 zjamg
2026-09-18 4:02 ` [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable zjamg
2026-09-20 1:49 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Yuchao Zhang
0 siblings, 2 replies; 14+ messages in thread
From: zjamg @ 2026-09-18 4:02 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: James Morse, Suzuki K Poulose, Zenghui Yu, Catalin Marinas,
Will Deacon, kvmarm, linux-arm-kernel, linux-kernel,
Yuchao Zhang
From: Yuchao Zhang <ndaugoing@gmail.com>
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
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable
2026-09-18 4:02 [PATCH v2 0/1] KVM: arm64: vgic: fix UAF/crash on remote LPI disable zjamg
@ 2026-09-18 4:02 ` zjamg
2026-09-18 7:22 ` Fuad Tabba
2026-09-20 1:49 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Yuchao Zhang
1 sibling, 1 reply; 14+ messages in thread
From: zjamg @ 2026-09-18 4:02 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: James Morse, Suzuki K Poulose, Zenghui Yu, Catalin Marinas,
Will Deacon, kvmarm, linux-arm-kernel, linux-kernel,
Yuchao Zhang, stable
From: Yuchao Zhang <ndaugoing@gmail.com>
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")
Cc: stable@vger.kernel.org
Signed-off-by: Yuchao Zhang <ndaugoing@gmail.com>
---
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(-)
diff --git a/arch/arm64/kvm/vgic/vgic-v2.c b/arch/arm64/kvm/vgic/vgic-v2.c
index 7182f63fc938..7b6cd05ce32d 100644
--- a/arch/arm64/kvm/vgic/vgic-v2.c
+++ b/arch/arm64/kvm/vgic/vgic-v2.c
@@ -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;
diff --git a/arch/arm64/kvm/vgic/vgic-v3.c b/arch/arm64/kvm/vgic/vgic-v3.c
index 726e20a1da6e..dd1314f4543d 100644
--- a/arch/arm64/kvm/vgic/vgic-v3.c
+++ b/arch/arm64/kvm/vgic/vgic-v3.c
@@ -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
diff --git a/arch/arm64/kvm/vgic/vgic.c b/arch/arm64/kvm/vgic/vgic.c
index b25303d9919f..0d7d75c3ac93 100644
--- a/arch/arm64/kvm/vgic/vgic.c
+++ b/arch/arm64/kvm/vgic/vgic.c
@@ -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. */
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable
2026-09-18 4:02 ` [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable zjamg
@ 2026-09-18 7:22 ` Fuad Tabba
2026-09-18 11:50 ` Yuchao Zhang
0 siblings, 1 reply; 14+ messages in thread
From: Fuad Tabba @ 2026-09-18 7:22 UTC (permalink / raw)
To: zjamg
Cc: Marc Zyngier, Oliver Upton, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, kvmarm,
linux-arm-kernel, linux-kernel, stable
Hi Yuchao,
On Fri, 18 Sep 2026 12:02:14 +0800, Yuchao Zhang <ndaugoing@gmail.com> wrote:
[...]
> 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/aiJi5a3JJ-TbWL-s@kernel.org
[3] https://lore.kernel.org/r/87a4t99z9n.wl-maz@kernel.org
[4] https://lore.kernel.org/r/aiXvwGD1hS6vwLEd@v4bel
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable
2026-09-18 7:22 ` Fuad Tabba
@ 2026-09-18 11:50 ` Yuchao Zhang
2026-09-18 11:58 ` Fuad Tabba
0 siblings, 1 reply; 14+ messages in thread
From: Yuchao Zhang @ 2026-09-18 11:50 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, kvmarm,
linux-arm-kernel, linux-kernel, stable
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
Fuad Tabba <fuad.tabba@linux.dev> 于2026年9月18日周五 15:22写道:
>
> Hi Yuchao,
>
> On Fri, 18 Sep 2026 12:02:14 +0800, Yuchao Zhang <ndaugoing@gmail.com> wrote:
>
> [...]
>
> > 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/aiJi5a3JJ-TbWL-s@kernel.org
> [3] https://lore.kernel.org/r/87a4t99z9n.wl-maz@kernel.org
> [4] https://lore.kernel.org/r/aiXvwGD1hS6vwLEd@v4bel
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable
2026-09-18 11:50 ` Yuchao Zhang
@ 2026-09-18 11:58 ` Fuad Tabba
2026-09-18 19:15 ` Oliver Upton
0 siblings, 1 reply; 14+ messages in thread
From: Fuad Tabba @ 2026-09-18 11:58 UTC (permalink / raw)
To: Yuchao Zhang
Cc: Marc Zyngier, Oliver Upton, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, kvmarm,
linux-arm-kernel, linux-kernel, stable
Hi Yuchao,
On Fri, 18 Sept 2026 at 12:51, Yuchao Zhang <ndaugoing@gmail.com> wrote:
>
> 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
> Thanks again for the pointer!
>
> Best regards,
> Yuchao
>
> Fuad Tabba <fuad.tabba@linux.dev> 于2026年9月18日周五 15:22写道:
> >
> > Hi Yuchao,
> >
> > On Fri, 18 Sep 2026 12:02:14 +0800, Yuchao Zhang <ndaugoing@gmail.com> wrote:
> >
> > [...]
> >
> > > 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/aiJi5a3JJ-TbWL-s@kernel.org
> > [3] https://lore.kernel.org/r/87a4t99z9n.wl-maz@kernel.org
> > [4] https://lore.kernel.org/r/aiXvwGD1hS6vwLEd@v4bel
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable
2026-09-18 11:58 ` Fuad Tabba
@ 2026-09-18 19:15 ` Oliver Upton
2026-09-20 23:54 ` Marc Zyngier
0 siblings, 1 reply; 14+ messages in thread
From: Oliver Upton @ 2026-09-18 19:15 UTC (permalink / raw)
To: Fuad Tabba
Cc: Yuchao Zhang, Marc Zyngier, Oliver Upton, James Morse,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
kvmarm, linux-arm-kernel, linux-kernel, stable
On Fri, Sep 18, 2026 at 12:58:17PM +0100, Fuad Tabba wrote:
> Hi Yuchao,
>
> On Fri, 18 Sept 2026 at 12:51, Yuchao Zhang <ndaugoing@gmail.com> wrote:
> >
> > 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.
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
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay
2026-09-18 4:02 [PATCH v2 0/1] KVM: arm64: vgic: fix UAF/crash on remote LPI disable zjamg
2026-09-18 4:02 ` [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable zjamg
@ 2026-09-20 1:49 ` Yuchao Zhang
2026-09-20 1:49 ` [PATCH v3] " Yuchao Zhang
2026-09-20 23:47 ` [PATCH v3 0/1] " Marc Zyngier
1 sibling, 2 replies; 14+ messages in thread
From: Yuchao Zhang @ 2026-09-20 1:49 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Fuad Tabba, James Morse, Suzuki K Poulose, Zenghui Yu,
Catalin Marinas, Will Deacon, kvmarm, linux-arm-kernel,
linux-kernel, 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").
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/aiJi5a3JJ-TbWL-s@kernel.org
[3] https://lore.kernel.org/r/87a4t99z9n.wl-maz@kernel.org
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(-)
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v3] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay
2026-09-20 1:49 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Yuchao Zhang
@ 2026-09-20 1:49 ` Yuchao Zhang
2026-09-20 23:47 ` [PATCH v3 0/1] " Marc Zyngier
1 sibling, 0 replies; 14+ messages in thread
From: Yuchao Zhang @ 2026-09-20 1:49 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Fuad Tabba, James Morse, Suzuki K Poulose, Zenghui Yu,
Catalin Marinas, Will Deacon, kvmarm, linux-arm-kernel,
linux-kernel, Yuchao Zhang, stable
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")
Cc: stable@vger.kernel.org
Signed-off-by: Yuchao Zhang <ndaugoing@gmail.com>
---
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(-)
diff --git a/arch/arm64/include/asm/kvm_host.h b/arch/arm64/include/asm/kvm_host.h
index 27fe0cd5b2d7..c4355ae23e98 100644
--- a/arch/arm64/include/asm/kvm_host.h
+++ b/arch/arm64/include/asm/kvm_host.h
@@ -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);
diff --git a/arch/arm64/kvm/vgic/vgic-v2.c b/arch/arm64/kvm/vgic/vgic-v2.c
index 7182f63fc938..4a0ad5bd1cf6 100644
--- a/arch/arm64/kvm/vgic/vgic-v2.c
+++ b/arch/arm64/kvm/vgic/vgic-v2.c
@@ -115,37 +115,71 @@ void vgic_v2_fold_lr_state(struct kvm_vcpu *vcpu)
struct vgic_cpu *vgic_cpu = &vcpu->arch.vgic_cpu;
struct vgic_v2_cpu_if *cpuif = &vgic_cpu->vgic_v2;
u32 eoicount = FIELD_GET(GICH_HCR_EOICOUNT, cpuif->vgic_hcr);
- struct vgic_irq *irq = *host_data_ptr(last_lr_irq);
+ struct vgic_irq *targets[32];
+ u32 lr_intids[VGIC_V2_MAX_LRS];
+ int nr_lrs = min_t(int, vgic_cpu->vgic_v2.used_lrs, ARRAY_SIZE(lr_intids));
+ u32 max_targets;
+ int nr_targets = 0;
DEBUG_SPINLOCK_BUG_ON(!irqs_disabled());
- for (int lr = 0; lr < vgic_cpu->vgic_v2.used_lrs; lr++)
- vgic_v2_fold_lr(vcpu, cpuif->vgic_lr[lr]);
+ if (!vgic_cpu->vgic_v2.used_lrs && !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;
+ for (int lr = 0; lr < nr_lrs; lr++) {
+ u32 val = cpuif->vgic_lr[lr];
+
+ lr_intids[lr] = val & GICH_LR_VIRTUALID;
+ vgic_v2_fold_lr(vcpu, val);
+ }
+
+ cpuif->used_lrs = 0;
+
+ if (likely(!eoicount))
+ return;
+
+ max_targets = min_t(u32, eoicount, ARRAY_SIZE(targets));
+
+ /*
+ * EOIMode=0: replay deactivations for overflow active interrupts.
+ * Walk ap_list under ap_list_lock and pin candidate interrupts so we
+ * can process them outside the lock without risking lock inversion.
+ */
+ scoped_guard(raw_spinlock, &vgic_cpu->ap_list_lock) {
+ struct vgic_irq *irq;
- if (!eoicount) {
- break;
- } else {
- guard(raw_spinlock)(&irq->irq_lock);
+ list_for_each_entry(irq, &vgic_cpu->ap_list_head, ap_list) {
+ if (nr_targets == max_targets)
+ break;
- if (!(likely(vgic_target_oracle(irq) == vcpu) &&
- irq->active))
+ if (intid_in_lrs(irq->intid, lr_intids, nr_lrs))
continue;
+ scoped_guard(raw_spinlock, &irq->irq_lock) {
+ if (likely(vgic_target_oracle(irq) == vcpu) &&
+ irq->active) {
+ vgic_get_irq_ref(irq);
+ targets[nr_targets++] = irq;
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < nr_targets; i++) {
+ struct vgic_irq *irq = targets[i];
+ u32 lr;
+
+ scoped_guard(raw_spinlock, &irq->irq_lock) {
lr = vgic_v2_compute_lr(vcpu, irq) & ~GICH_LR_ACTIVE_BIT;
}
if (lr & GICH_LR_HW)
writel_relaxed(FIELD_GET(GICH_LR_PHYSID_CPUID, lr),
kvm_vgic_global_state.gicc_base + GIC_CPU_DEACTIVATE);
+
vgic_v2_fold_lr(vcpu, lr);
- eoicount--;
+ vgic_put_irq(vcpu->kvm, irq);
}
-
- cpuif->used_lrs = 0;
}
void vgic_v2_deactivate(struct kvm_vcpu *vcpu, u32 val)
diff --git a/arch/arm64/kvm/vgic/vgic-v3.c b/arch/arm64/kvm/vgic/vgic-v3.c
index 726e20a1da6e..7e573860f3e1 100644
--- a/arch/arm64/kvm/vgic/vgic-v3.c
+++ b/arch/arm64/kvm/vgic/vgic-v3.c
@@ -148,37 +148,65 @@ void vgic_v3_fold_lr_state(struct kvm_vcpu *vcpu)
struct vgic_cpu *vgic_cpu = &vcpu->arch.vgic_cpu;
struct vgic_v3_cpu_if *cpuif = &vgic_cpu->vgic_v3;
u32 eoicount = FIELD_GET(ICH_HCR_EL2_EOIcount, cpuif->vgic_hcr);
- struct vgic_irq *irq = *host_data_ptr(last_lr_irq);
+ struct vgic_irq *targets[32];
+ u32 lr_intids[VGIC_V3_MAX_LRS];
+ int nr_lrs = min_t(int, cpuif->used_lrs, ARRAY_SIZE(lr_intids));
+ u32 max_targets;
+ int nr_targets = 0;
DEBUG_SPINLOCK_BUG_ON(!irqs_disabled());
- for (int lr = 0; lr < cpuif->used_lrs; lr++)
- vgic_v3_fold_lr(vcpu, cpuif->vgic_lr[lr]);
+ if (!cpuif->used_lrs && !eoicount)
+ return;
+
+ for (int lr = 0; lr < nr_lrs; lr++) {
+ u64 val = cpuif->vgic_lr[lr];
+
+ if (vcpu->kvm->arch.vgic.vgic_model == KVM_DEV_TYPE_ARM_VGIC_V3)
+ lr_intids[lr] = val & ICH_LR_VIRTUAL_ID_MASK;
+ else
+ lr_intids[lr] = val & GICH_LR_VIRTUALID;
+
+ vgic_v3_fold_lr(vcpu, val);
+ }
+
+ cpuif->used_lrs = 0;
+
+ if (likely(!eoicount))
+ return;
+
+ max_targets = min_t(u32, eoicount, ARRAY_SIZE(targets));
/*
- * EOIMode=0: use EOIcount to emulate deactivation. We are
- * guaranteed to deactivate in reverse order of the activation, so
- * just pick one active interrupt after the other in the tail part
- * of the ap_list, past the LRs, and replay the deactivation as if
- * the CPU was doing it. We also rely on priority drop to have taken
- * place, and the list to be sorted by priority.
+ * EOIMode=0: replay deactivations for overflow active interrupts.
+ * Walk ap_list under ap_list_lock and pin candidate interrupts so we
+ * can process them outside the lock without risking lock inversion.
*/
- list_for_each_entry_continue(irq, &vgic_cpu->ap_list_head, ap_list) {
- u64 lr;
+ scoped_guard(raw_spinlock, &vgic_cpu->ap_list_lock) {
+ struct vgic_irq *irq;
- /*
- * I would have loved to write this using a scoped_guard(),
- * but using 'continue' here is a total train wreck.
- */
- if (!eoicount) {
- break;
- } else {
- guard(raw_spinlock)(&irq->irq_lock);
+ list_for_each_entry(irq, &vgic_cpu->ap_list_head, ap_list) {
+ if (nr_targets == max_targets)
+ break;
- if (!(likely(vgic_target_oracle(irq) == vcpu) &&
- irq->active))
+ if (intid_in_lrs(irq->intid, lr_intids, nr_lrs))
continue;
+ scoped_guard(raw_spinlock, &irq->irq_lock) {
+ if (likely(vgic_target_oracle(irq) == vcpu) &&
+ irq->active) {
+ vgic_get_irq_ref(irq);
+ targets[nr_targets++] = irq;
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < nr_targets; i++) {
+ struct vgic_irq *irq = targets[i];
+ u64 lr;
+
+ scoped_guard(raw_spinlock, &irq->irq_lock) {
lr = vgic_v3_compute_lr(vcpu, irq) & ~ICH_LR_ACTIVE_BIT;
}
@@ -186,10 +214,8 @@ void vgic_v3_fold_lr_state(struct kvm_vcpu *vcpu)
vgic_v3_deactivate_phys(FIELD_GET(ICH_LR_PHYS_ID_MASK, lr));
vgic_v3_fold_lr(vcpu, lr);
- eoicount--;
+ vgic_put_irq(vcpu->kvm, irq);
}
-
- cpuif->used_lrs = 0;
}
void vgic_v3_deactivate(struct kvm_vcpu *vcpu, u64 val)
diff --git a/arch/arm64/kvm/vgic/vgic.c b/arch/arm64/kvm/vgic/vgic.c
index b25303d9919f..966948fd3ddd 100644
--- a/arch/arm64/kvm/vgic/vgic.c
+++ b/arch/arm64/kvm/vgic/vgic.c
@@ -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)
diff --git a/arch/arm64/kvm/vgic/vgic.h b/arch/arm64/kvm/vgic/vgic.h
index b71d486ae514..35a07b320be4 100644
--- a/arch/arm64/kvm/vgic/vgic.h
+++ b/arch/arm64/kvm/vgic/vgic.h
@@ -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);
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay
2026-09-20 1:49 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Yuchao Zhang
2026-09-20 1:49 ` [PATCH v3] " Yuchao Zhang
@ 2026-09-20 23:47 ` Marc Zyngier
2026-09-22 10:16 ` Yuchao Zhang
` (2 more replies)
1 sibling, 3 replies; 14+ messages in thread
From: Marc Zyngier @ 2026-09-20 23:47 UTC (permalink / raw)
To: Yuchao Zhang
Cc: Oliver Upton, Fuad Tabba, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, kvmarm,
linux-arm-kernel, linux-kernel
On Sun, 20 Sep 2026 02:49:10 +0100,
Yuchao Zhang <ndaugoing@gmail.com> wrote:
>
> 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.
Why can't this be solved by simply taking a reference on the object
pointed to by last_lr_irq?
>
> 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.
> 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?
> 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.
>
> 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.
> 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...
> 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.
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
--
Jazz isn't dead. It just smells funny.
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable
2026-09-18 19:15 ` Oliver Upton
@ 2026-09-20 23:54 ` Marc Zyngier
0 siblings, 0 replies; 14+ messages in thread
From: Marc Zyngier @ 2026-09-20 23:54 UTC (permalink / raw)
To: Oliver Upton
Cc: Fuad Tabba, Yuchao Zhang, Oliver Upton, James Morse,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
kvmarm, linux-arm-kernel, linux-kernel, stable
On Fri, 18 Sep 2026 20:15:56 +0100,
Oliver Upton <oupton@kernel.org> wrote:
>
> On Fri, Sep 18, 2026 at 12:58:17PM +0100, Fuad Tabba wrote:
> > Hi Yuchao,
> >
> > On Fri, 18 Sept 2026 at 12:51, Yuchao Zhang <ndaugoing@gmail.com> wrote:
> > >
> > > 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.
>
> 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.
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.
> 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.
--
Jazz isn't dead. It just smells funny.
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay
2026-09-20 23:47 ` [PATCH v3 0/1] " Marc Zyngier
@ 2026-09-22 10:16 ` Yuchao Zhang
2026-09-22 10:16 ` [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race Yuchao Zhang
2026-09-22 17:05 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Fuad Tabba
2 siblings, 0 replies; 14+ messages in thread
From: Yuchao Zhang @ 2026-09-22 10:16 UTC (permalink / raw)
To: Marc Zyngier
Cc: Oliver Upton, Fuad Tabba, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, kvmarm,
linux-arm-kernel, linux-kernel, Yuchao Zhang
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
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race
2026-09-20 23:47 ` [PATCH v3 0/1] " Marc Zyngier
2026-09-22 10:16 ` Yuchao Zhang
@ 2026-09-22 10:16 ` Yuchao Zhang
2026-09-22 17:05 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Fuad Tabba
2 siblings, 0 replies; 14+ messages in thread
From: Yuchao Zhang @ 2026-09-22 10:16 UTC (permalink / raw)
To: Marc Zyngier
Cc: Oliver Upton, Fuad Tabba, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, kvmarm,
linux-arm-kernel, linux-kernel, Yuchao Zhang
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.
Signed-off-by: Yuchao Zhang <ndaugoing@gmail.com>
---
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
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 96bab7002d39..cf0baec3f6c7 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -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
diff --git a/tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c b/tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c
new file mode 100644
index 000000000000..078134919228
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/vgic_lpi_disable.c
@@ -0,0 +1,401 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * vgic_lpi_disable - Test cross-vCPU LPI disabling race condition
+ *
+ * Copyright (c) 2026 Yuchao Zhang <ndaugoing@gmail.com>
+ *
+ * This test verifies that disabling LPIs from a remote vCPU while the
+ * target vCPU has in-flight/overflowing LPIs does not lead to use-after-free
+ * or kernel panic.
+ */
+
+#include <linux/sizes.h>
+#include <pthread.h>
+#include <stdatomic.h>
+#include <sys/sysinfo.h>
+
+#include "kvm_util.h"
+#include "delay.h"
+#include "gic.h"
+#include "gic_v3.h"
+#include "gic_v3_its.h"
+#include "processor.h"
+#include "ucall.h"
+#include "vgic.h"
+
+#define TEST_MEMSLOT_INDEX 1
+#define GIC_LPI_OFFSET 8192
+
+#define TARGET_VCPU_ID 0
+#define DISABLER_VCPU_ID 1
+
+static size_t nr_iterations = 200;
+static gpa_t gpa_base;
+
+static struct kvm_vm *vm;
+static struct kvm_vcpu **vcpus;
+static int its_fd;
+
+static struct test_data {
+ bool request_vcpus_stop;
+ u32 nr_cpus;
+ u32 nr_devices;
+ u32 nr_event_ids;
+
+ gpa_t device_table;
+ gpa_t collection_table;
+ gpa_t cmdq_base;
+ void *cmdq_base_va;
+ gpa_t itt_tables;
+
+ gpa_t lpi_prop_table;
+ gpa_t lpi_pend_tables;
+} test_data = {
+ .nr_cpus = 2,
+ .nr_devices = 1,
+ .nr_event_ids = 64,
+};
+
+static void guest_irq_handler(struct ex_regs *regs)
+{
+ u32 intid = gic_get_and_ack_irq();
+
+ if (intid == IAR_SPURIOUS)
+ return;
+
+ GUEST_ASSERT(intid >= GIC_LPI_OFFSET);
+ gic_set_eoi(intid);
+}
+
+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 */
+ for (device_id = 0; device_id < nr_devices; device_id++) {
+ gpa_t itt_base = test_data.itt_tables + (device_id * SZ_64K);
+
+ its_send_mapd_cmd(test_data.cmdq_base_va, device_id,
+ itt_base, SZ_64K, true);
+
+ for (event_id = 0; event_id < nr_events; event_id++) {
+ its_send_mapti_cmd(test_data.cmdq_base_va, device_id,
+ event_id, TARGET_VCPU_ID, intid++);
+ }
+ }
+}
+
+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);
+}
+
+static void guest_setup_gic(void)
+{
+ static atomic_int nr_cpus_ready;
+ u32 cpuid = guest_get_vcpuid();
+
+ gic_init(GIC_V3, test_data.nr_cpus);
+ gic_rdist_enable_lpis(test_data.lpi_prop_table, SZ_64K,
+ test_data.lpi_pend_tables + (cpuid * SZ_64K));
+
+ atomic_fetch_add(&nr_cpus_ready, 1);
+
+ if (cpuid > 0)
+ return;
+
+ while (atomic_load(&nr_cpus_ready) < test_data.nr_cpus)
+ cpu_relax();
+
+ its_init(test_data.collection_table, SZ_64K,
+ test_data.device_table, SZ_64K,
+ test_data.cmdq_base, SZ_64K);
+
+ 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);
+}
+
+static inline void *test_gicr_base_cpu(u32 cpu)
+{
+ return (void *)(GICR_BASE_GPA + cpu * SZ_64K * 2);
+}
+
+static void test_gicv3_gicr_wait_for_rwp(u32 cpu)
+{
+ unsigned int count = 100000;
+
+ while (readl(test_gicr_base_cpu(cpu) + GICR_CTLR) & GICR_CTLR_RWP) {
+ GUEST_ASSERT(count--);
+ udelay(10);
+ }
+}
+
+static void guest_code(size_t nr_lpis)
+{
+ u32 cpuid = guest_get_vcpuid();
+
+ guest_setup_gic();
+
+ if (cpuid == TARGET_VCPU_ID) {
+ local_irq_enable();
+ GUEST_SYNC(0);
+
+ while (!READ_ONCE(test_data.request_vcpus_stop))
+ cpu_relax();
+ } else {
+ GUEST_SYNC(0);
+
+ for (size_t i = 0; i < nr_iterations; i++) {
+ /* Remotely disable LPIs on target vCPU */
+ writel(0, test_gicr_base_cpu(TARGET_VCPU_ID) + GICR_CTLR);
+ test_gicv3_gicr_wait_for_rwp(TARGET_VCPU_ID);
+
+ for (int d = 0; d < 50; d++)
+ cpu_relax();
+
+ /* Remotely re-enable LPIs on target vCPU */
+ writel(GICR_CTLR_ENABLE_LPIS,
+ test_gicr_base_cpu(TARGET_VCPU_ID) + GICR_CTLR);
+ test_gicv3_gicr_wait_for_rwp(TARGET_VCPU_ID);
+ }
+
+ WRITE_ONCE(test_data.request_vcpus_stop, true);
+ }
+
+ GUEST_DONE();
+}
+
+static void setup_memslot(void)
+{
+ size_t pages;
+ size_t sz;
+
+ sz = (3 + test_data.nr_devices) * SZ_64K;
+ sz += (1 + test_data.nr_cpus) * SZ_64K;
+
+ pages = sz / vm->page_size;
+ gpa_base = ((vm_compute_max_gfn(vm) + 1) * vm->page_size) - sz;
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, gpa_base,
+ TEST_MEMSLOT_INDEX, pages, 0);
+}
+
+#define LPI_PROP_DEFAULT_PRIO 0xa0
+
+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;
+ }
+}
+
+static void setup_test_data(void)
+{
+ size_t pages_per_64k = vm_calc_num_guest_pages(vm->mode, SZ_64K);
+ u32 nr_devices = test_data.nr_devices;
+ u32 nr_cpus = test_data.nr_cpus;
+ gpa_t cmdq_base;
+
+ test_data.device_table = vm_phy_pages_alloc(vm, pages_per_64k,
+ gpa_base,
+ TEST_MEMSLOT_INDEX);
+
+ test_data.collection_table = vm_phy_pages_alloc(vm, pages_per_64k,
+ gpa_base,
+ TEST_MEMSLOT_INDEX);
+
+ cmdq_base = vm_phy_pages_alloc(vm, pages_per_64k, gpa_base,
+ TEST_MEMSLOT_INDEX);
+ virt_map(vm, cmdq_base, cmdq_base, pages_per_64k);
+ test_data.cmdq_base = cmdq_base;
+ test_data.cmdq_base_va = (void *)cmdq_base;
+
+ test_data.itt_tables = vm_phy_pages_alloc(vm, pages_per_64k * nr_devices,
+ gpa_base, TEST_MEMSLOT_INDEX);
+
+ test_data.lpi_prop_table = vm_phy_pages_alloc(vm, pages_per_64k,
+ gpa_base, TEST_MEMSLOT_INDEX);
+ configure_lpis();
+
+ test_data.lpi_pend_tables = vm_phy_pages_alloc(vm, pages_per_64k * nr_cpus,
+ gpa_base, TEST_MEMSLOT_INDEX);
+
+ sync_global_to_guest(vm, test_data);
+}
+
+static void setup_gic(void)
+{
+ its_fd = vgic_its_setup(vm);
+}
+
+static void signal_lpi(u32 device_id, u32 event_id)
+{
+ gpa_t db_addr = GITS_BASE_GPA + GITS_TRANSLATER;
+
+ 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);
+}
+
+static pthread_barrier_t test_setup_barrier;
+
+static atomic_bool stop_lpi_thread;
+
+static void *lpi_worker_thread(void *data)
+{
+ u32 device_id = (size_t)data;
+ u32 event_id;
+
+ pthread_barrier_wait(&test_setup_barrier);
+
+ while (!atomic_load(&stop_lpi_thread)) {
+ for (event_id = 0; event_id < test_data.nr_event_ids; event_id++)
+ signal_lpi(device_id, event_id);
+ usleep(100);
+ }
+
+ return NULL;
+}
+
+static void *vcpu_worker_thread(void *data)
+{
+ struct kvm_vcpu *vcpu = data;
+ struct ucall uc;
+
+ while (true) {
+ vcpu_run(vcpu);
+
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_SYNC:
+ pthread_barrier_wait(&test_setup_barrier);
+ continue;
+ case UCALL_DONE:
+ if (vcpu == vcpus[DISABLER_VCPU_ID]) {
+ atomic_store(&stop_lpi_thread, true);
+ write_guest_global(vm, test_data.request_vcpus_stop, true);
+ }
+ return NULL;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ break;
+ default:
+ TEST_FAIL("Unknown ucall: %lu", uc.cmd);
+ }
+ }
+
+ return NULL;
+}
+
+static void run_test(void)
+{
+ pthread_t *vcpu_threads;
+ pthread_t lpi_thread;
+ u32 i;
+
+ pthread_barrier_init(&test_setup_barrier, NULL, test_data.nr_cpus + 1);
+
+ vcpu_threads = malloc(sizeof(pthread_t) * test_data.nr_cpus);
+ TEST_ASSERT(vcpu_threads, "Failed to allocate vcpu_threads");
+
+ for (i = 0; i < test_data.nr_cpus; i++)
+ pthread_create(&vcpu_threads[i], NULL, vcpu_worker_thread, vcpus[i]);
+
+ pthread_create(&lpi_thread, NULL, lpi_worker_thread, (void *)(size_t)0);
+
+ pthread_join(lpi_thread, NULL);
+ for (i = 0; i < test_data.nr_cpus; i++)
+ pthread_join(vcpu_threads[i], NULL);
+
+ free(vcpu_threads);
+}
+
+static void setup_vm(void)
+{
+ int i;
+
+ vm = vm_create_with_vcpus(test_data.nr_cpus, guest_code, vcpus);
+
+ vm_init_descriptor_tables(vm);
+ for (i = 0; i < test_data.nr_cpus; i++)
+ vcpu_init_descriptor_tables(vcpus[i]);
+
+ vm_install_exception_handler(vm, VECTOR_IRQ_CURRENT, guest_irq_handler);
+
+ setup_memslot();
+ setup_gic();
+ setup_test_data();
+}
+
+static void destroy_vm(void)
+{
+ close(its_fd);
+ kvm_vm_free(vm);
+}
+
+static void help(const char *name)
+{
+ pr_info("Usage: %s [-i iterations] [-e event_ids]\n", name);
+ pr_info(" -i: number of iterations to toggle GICR_CTLR.EnableLPIs (default %lu)\n",
+ nr_iterations);
+ pr_info(" -e: number of event IDs/LPIs to inject (default %u)\n",
+ test_data.nr_event_ids);
+}
+
+int main(int argc, char **argv)
+{
+ int opt;
+
+ TEST_REQUIRE(kvm_supports_vgic_v3());
+
+ while ((opt = getopt(argc, argv, "i:e:h")) != -1) {
+ switch (opt) {
+ case 'i':
+ nr_iterations = atoi_positive("iterations", optarg);
+ break;
+ case 'e':
+ test_data.nr_event_ids = atoi_positive("event_ids", optarg);
+ break;
+ case 'h':
+ default:
+ help(argv[0]);
+ exit(opt == 'h' ? 0 : 1);
+ }
+ }
+
+ vcpus = malloc(sizeof(struct kvm_vcpu *) * test_data.nr_cpus);
+ TEST_ASSERT(vcpus, "Failed to allocate vcpus array");
+
+ setup_vm();
+ run_test();
+ destroy_vm();
+
+ free(vcpus);
+
+ pr_info("Completed %lu iterations of remote LPI disable successfully\n",
+ nr_iterations);
+
+ return 0;
+}
--
2.53.0
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay
2026-09-20 23:47 ` [PATCH v3 0/1] " Marc Zyngier
2026-09-22 10:16 ` Yuchao Zhang
2026-09-22 10:16 ` [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race Yuchao Zhang
@ 2026-09-22 17:05 ` Fuad Tabba
2 siblings, 0 replies; 14+ messages in thread
From: Fuad Tabba @ 2026-09-22 17:05 UTC (permalink / raw)
To: Marc Zyngier
Cc: Yuchao Zhang, Oliver Upton, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, kvmarm,
linux-arm-kernel, linux-kernel, Fuad Tabba
Hi Marc,
On Mon, 21 Sep 2026 00:47:20 +0100, Marc Zyngier <maz@kernel.org> wrote:
[...]
> 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.
Reviewed-by: Fuad Tabba <fuad.tabba@linux.dev>
Tested-by: Fuad Tabba <fuad.tabba@linux.dev>
Cheers,
/fuad
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race
[not found] <20260922102959.41FFA1F000FF@smtp.kernel.org>
@ 2026-09-22 10:53 ` Yuchao Zhang
0 siblings, 0 replies; 14+ messages in thread
From: Yuchao Zhang @ 2026-09-22 10:53 UTC (permalink / raw)
To: Marc Zyngier
Cc: Oliver Upton, Fuad Tabba, James Morse, Suzuki K Poulose,
Zenghui Yu, Catalin Marinas, Will Deacon, sashiko-reviews,
kvmarm, linux-arm-kernel, linux-kernel, Yuchao Zhang
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
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-09-22 17:05 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-18 4:02 [PATCH v2 0/1] KVM: arm64: vgic: fix UAF/crash on remote LPI disable zjamg
2026-09-18 4:02 ` [PATCH v2] KVM: arm64: vgic: Do not remove in-flight LPIs from AP list on disable zjamg
2026-09-18 7:22 ` Fuad Tabba
2026-09-18 11:50 ` Yuchao Zhang
2026-09-18 11:58 ` Fuad Tabba
2026-09-18 19:15 ` Oliver Upton
2026-09-20 23:54 ` Marc Zyngier
2026-09-20 1:49 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Yuchao Zhang
2026-09-20 1:49 ` [PATCH v3] " Yuchao Zhang
2026-09-20 23:47 ` [PATCH v3 0/1] " Marc Zyngier
2026-09-22 10:16 ` Yuchao Zhang
2026-09-22 10:16 ` [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race Yuchao Zhang
2026-09-22 17:05 ` [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Fuad Tabba
[not found] <20260922102959.41FFA1F000FF@smtp.kernel.org>
2026-09-22 10:53 ` [PATCH] KVM: selftests: arm64: Add test for cross-vCPU LPI disable race Yuchao Zhang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®