* [PATCH v4 0/2] KVM: arm64: vgic: Fix racy LPI release and re-registration handling
@ 2026-07-03 12:25 Carlos López
2026-07-03 12:26 ` [PATCH v4 1/2] KVM: arm64: vgic: Fix race between LPI release and re-registration Carlos López
2026-07-03 12:26 ` [PATCH v4 2/2] KVM: arm64: vgic: Mitigate potential LPI registration failure Carlos López
0 siblings, 2 replies; 5+ messages in thread
From: Carlos López @ 2026-07-03 12:25 UTC (permalink / raw)
To: kvmarm, linux-kernel
Cc: maz, oupton, joey.gouly, seiden, suzuki.poulose, yuzenghui,
catalin.marinas, will, linux-arm-kernel, Carlos López
Fix a couple of potential issues that could arise from racy LPI release
and re-registration for the same INTID.
The issue fixed in patch 1 can manifest itself through either a
leaked LPI structure, or a prematurely deleted LPI. The issue fixed
in patch 2 could materialize as a spurious -ENOMEM failure when
registering an LPI.
v4:
* Add __GFP_ACCOUNT to patch 2 (Sashiko).
v3:
* Use refcount_dec_and_lock_irqsave() instead of unconditionally
grabbing the xarray lock in patch 1.
* Add patch 2.
v2:
* Address Sashiko's review. Fix the direct release path by decrementing
the refcount under the xarray spinlock, preventing a UAF that would
have been introduced in v1.
Carlos López (2):
KVM: arm64: vgic: Fix race between LPI release and re-registration
KVM: arm64: vgic: Mitigate potential LPI registration failure
arch/arm64/kvm/vgic/vgic-its.c | 11 ++++++++++-
arch/arm64/kvm/vgic/vgic.c | 10 ++++++----
2 files changed, 16 insertions(+), 5 deletions(-)
base-commit: 1ee27dacbe5dc4def481794d899d67b0d4570094
--
2.51.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v4 1/2] KVM: arm64: vgic: Fix race between LPI release and re-registration
2026-07-03 12:25 [PATCH v4 0/2] KVM: arm64: vgic: Fix racy LPI release and re-registration handling Carlos López
@ 2026-07-03 12:26 ` Carlos López
2026-07-03 22:13 ` Oliver Upton
2026-07-03 12:26 ` [PATCH v4 2/2] KVM: arm64: vgic: Mitigate potential LPI registration failure Carlos López
1 sibling, 1 reply; 5+ messages in thread
From: Carlos López @ 2026-07-03 12:26 UTC (permalink / raw)
To: kvmarm, linux-kernel
Cc: maz, oupton, joey.gouly, seiden, suzuki.poulose, yuzenghui,
catalin.marinas, will, linux-arm-kernel, Carlos López
Fix a potential race between decrementing an LPI's reference count and
evicting that structure from the LPI xarray.
LPI structures are maintained in the VGIC LPI xarray (dist->lpi_xa).
When the reference count of an LPI structure drops to zero,
vgic_release_lpi_locked() removes the structure from the xarray and
frees it under the xarray lock.
However, the release of an LPI can race with a concurrent LPI
re-registration with the same INTID via vgic_add_lpi() on another CPU,
since the reference count drop and the xarray eviction are not performed
in a single atomic step. This can happen e.g. if the guest issues a
DISCARD while the LPI is still referenced from a vCPU's active-pending
list (ap_list), and the same INTID is re-mapped via MAPTI.
Particularly, vgic_release_lpi_locked() is called from two distinct
paths: direct release via vgic_put_irq(), and deferred release via
vgic_release_deleted_lpis(). During direct release, the issue can result
in deleting a newly registered LPI from the xarray:
CPU0 (Releasing LPI) CPU1 (Adding new LPI)
==================== =====================
vgic_put_irq()
__vgic_put_irq()
refcount_dec_and_test()
vgic_add_lpi()
xa_lock_irqsave()
old_irq = xa_load(.., intid)
vgic_try_get_irq_ref(old_irq) == false
new IRQ inserted --> __xa_store(.., intid, ..)
xa_unlock_irqrestore()
xa_lock_irqsave();
vgic_release_lpi_locked()
__xa_erase(.., irq->intid) <-- BUG: new IRQ is erased
kfree_rcu(old_irq)
During the deferred release path, the old IRQ can be leaked:
CPU0 (Releasing LPI) CPU1 (Adding new LPI)
==================== =====================
vgic_put_irq_norelease()
__vgic_put_irq()
refcount_dec_and_test()
irq->pending_release = true
vgic_add_lpi()
xa_lock_irqsave()
old_irq = xa_load(.., intid)
vgic_try_get_irq_ref(oldirq) == false
BUG: old IRQ overwritten --> __xa_store(.., intid, ..)
xa_unlock_irqrestore()
vgic_release_deleted_lpis()
xa_lock_irqsave()
xa_for_each() { .. } <-- old IRQ with pending_release = true
is gone, so it cannot be released
To fix the direct release path, move the reference count drop inside
the xarray lock, making sure that vgic_add_lpi() never encounters the
to-be-released LPI.
To fix the deferred release path, since the refcount drop must happen
under a raw spinlock, the same solution does not work. Instead, update
vgic_add_lpi(), so that if it evicts a non-NULL refcount=0 LPI from the
xarray, it takes on the responsibility of releasing it. If this happens,
vgic_release_deleted_lpis() will iterate the xarray normally and will
simply not find the already released structure.
Reported-by: Claude:claude-opus-4-6
Fixes: 3a08a6ca7c37 ("KVM: arm64: vgic-v3: Use bare refcount for VGIC LPIs")
Fixes: d54594accf73 ("KVM: arm64: vgic-v3: Erase LPIs from xarray outside of raw spinlocks")
Signed-off-by: Carlos López <clopez@suse.de>
---
arch/arm64/kvm/vgic/vgic-its.c | 10 +++++++++-
arch/arm64/kvm/vgic/vgic.c | 10 ++++++----
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c
index 67d107e9a77d..577286069368 100644
--- a/arch/arm64/kvm/vgic/vgic-its.c
+++ b/arch/arm64/kvm/vgic/vgic-its.c
@@ -116,7 +116,15 @@ static struct vgic_irq *vgic_add_lpi(struct kvm *kvm, u32 intid,
kfree(irq);
irq = oldirq;
} else {
- ret = xa_err(__xa_store(&dist->lpi_xa, intid, irq, 0));
+ /*
+ * The entry is either empty or contains a dead LPI (refcount=0)
+ * from the deferred release path, pending cleanup by
+ * vgic_release_deleted_lpis(). Evict and free it if present.
+ */
+ oldirq = __xa_store(&dist->lpi_xa, intid, irq, 0);
+ ret = xa_err(oldirq);
+ if (!ret && oldirq)
+ kfree_rcu(oldirq, rcu);
}
xa_unlock_irqrestore(&dist->lpi_xa, flags);
diff --git a/arch/arm64/kvm/vgic/vgic.c b/arch/arm64/kvm/vgic/vgic.c
index 5a4768d8cd4f..cc09e0c45b46 100644
--- a/arch/arm64/kvm/vgic/vgic.c
+++ b/arch/arm64/kvm/vgic/vgic.c
@@ -167,12 +167,14 @@ void vgic_put_irq(struct kvm *kvm, struct vgic_irq *irq)
guard(spinlock_irqsave)(&dist->lpi_xa.xa_lock);
}
- if (!__vgic_put_irq(kvm, irq))
+ if (!irq_is_lpi(kvm, irq->intid))
return;
- xa_lock_irqsave(&dist->lpi_xa, flags);
- vgic_release_lpi_locked(dist, irq);
- xa_unlock_irqrestore(&dist->lpi_xa, flags);
+ if (refcount_dec_and_lock_irqsave(&irq->refcount,
+ &dist->lpi_xa.xa_lock, &flags)) {
+ vgic_release_lpi_locked(dist, irq);
+ xa_unlock_irqrestore(&dist->lpi_xa, flags);
+ }
}
static void vgic_release_deleted_lpis(struct kvm *kvm)
--
2.51.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v4 2/2] KVM: arm64: vgic: Mitigate potential LPI registration failure
2026-07-03 12:25 [PATCH v4 0/2] KVM: arm64: vgic: Fix racy LPI release and re-registration handling Carlos López
2026-07-03 12:26 ` [PATCH v4 1/2] KVM: arm64: vgic: Fix race between LPI release and re-registration Carlos López
@ 2026-07-03 12:26 ` Carlos López
1 sibling, 0 replies; 5+ messages in thread
From: Carlos López @ 2026-07-03 12:26 UTC (permalink / raw)
To: kvmarm, linux-kernel
Cc: maz, oupton, joey.gouly, seiden, suzuki.poulose, yuzenghui,
catalin.marinas, will, linux-arm-kernel, Carlos López,
Sashiko
Mitigate a potential failure when inserting a new LPI into the VGIC LPI
xarray.
When vgic_add_lpi() is preparing to register a new LPI, it pre-allocates
an xarray entry using xa_reserve_irq(), so that it can later perform the
insertion under the xarray lock without allocating.
However, since xa_reserve_irq() is called before acquiring such lock,
there is a potential race where xa_reserve_irq() observes a populated
entry, thus not performing the allocation, and another CPU removes that
entry before the xarray lock is grabbed to perform the insertion.
CPU0 (Adding new LPI) CPU1 (Releasing LPI)
===================== ===================
vgic_add_lpi()
/* Entry populated, does not allocate */
xa_reserve_irq(.., intid, ..)
vgic_release_deleted_lpis()
xa_lock_irqsave()
vgic_release_lpi_locked()
xarray node freed --> __xa_erase(.., intid)
xa_unlock_irqrestore()
xa_lock_irqsave()
xa_load(.., intid) == NULL
vgic_try_get_irq_ref(NULL) == false
__xa_store(.., intid, irq, 0) <-- xarray node was freed, gfp=0
cannot allocate, returns -ENOMEM
This can happen e.g. if the guest issues a DISCARD while the LPI is
still referenced from a vCPU's active-pending list (ap_list), and the
same INTID is re-mapped via MAPTI.
Mitigate this by passing GFP_NOWAIT to __xa_store(), so that the
allocation can happen under the lock in the rare case that this
condition is hit. Add __GFP_ACCOUNT as well to match xa_reserve_irq()'s
flags.
Reported-by: Sashiko <sashiko-bot@kernel.org>
Fixes: 1d6f83f60f79 ("KVM: arm64: vgic: Store LPIs in an xarray")
Signed-off-by: Carlos López <clopez@suse.de>
---
arch/arm64/kvm/vgic/vgic-its.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c
index 577286069368..327418477c51 100644
--- a/arch/arm64/kvm/vgic/vgic-its.c
+++ b/arch/arm64/kvm/vgic/vgic-its.c
@@ -121,7 +121,8 @@ static struct vgic_irq *vgic_add_lpi(struct kvm *kvm, u32 intid,
* from the deferred release path, pending cleanup by
* vgic_release_deleted_lpis(). Evict and free it if present.
*/
- oldirq = __xa_store(&dist->lpi_xa, intid, irq, 0);
+ oldirq = __xa_store(&dist->lpi_xa, intid, irq,
+ GFP_NOWAIT | __GFP_ACCOUNT);
ret = xa_err(oldirq);
if (!ret && oldirq)
kfree_rcu(oldirq, rcu);
--
2.51.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v4 1/2] KVM: arm64: vgic: Fix race between LPI release and re-registration
2026-07-03 12:26 ` [PATCH v4 1/2] KVM: arm64: vgic: Fix race between LPI release and re-registration Carlos López
@ 2026-07-03 22:13 ` Oliver Upton
2026-07-03 22:19 ` Oliver Upton
0 siblings, 1 reply; 5+ messages in thread
From: Oliver Upton @ 2026-07-03 22:13 UTC (permalink / raw)
To: Carlos López
Cc: kvmarm, linux-kernel, maz, joey.gouly, seiden, suzuki.poulose,
yuzenghui, catalin.marinas, will, linux-arm-kernel
Hi Carlos,
Please wait a few days between spins, we were in the middle of a
conversation and I didn't have a chance to reply to you since yesterday.
On Fri, Jul 03, 2026 at 02:26:00PM +0200, Carlos López wrote:
> diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c
> index 67d107e9a77d..577286069368 100644
> --- a/arch/arm64/kvm/vgic/vgic-its.c
> +++ b/arch/arm64/kvm/vgic/vgic-its.c
> @@ -116,7 +116,15 @@ static struct vgic_irq *vgic_add_lpi(struct kvm *kvm, u32 intid,
> kfree(irq);
> irq = oldirq;
> } else {
> - ret = xa_err(__xa_store(&dist->lpi_xa, intid, irq, 0));
> + /*
> + * The entry is either empty or contains a dead LPI (refcount=0)
> + * from the deferred release path, pending cleanup by
> + * vgic_release_deleted_lpis(). Evict and free it if present.
> + */
> + oldirq = __xa_store(&dist->lpi_xa, intid, irq, 0);
> + ret = xa_err(oldirq);
> + if (!ret && oldirq)
> + kfree_rcu(oldirq, rcu);
We need to assert the assumption here that the evicted IRQ has a nonzero
refcount and is pending release. Otherwise I feel like possibly leaking
memory is better than potentially introducing a UAF.
Untested diff below.
Thanks,
Oliver
diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c
index 4477f870c7b3..621a83399184 100644
--- a/arch/arm64/kvm/vgic/vgic-its.c
+++ b/arch/arm64/kvm/vgic/vgic-its.c
@@ -116,18 +116,28 @@ static struct vgic_irq *vgic_add_lpi(struct kvm *kvm, u32 intid,
kfree(irq);
irq = oldirq;
} else {
- ret = xa_err(__xa_store(&dist->lpi_xa, intid, irq, 0));
- }
-
- xa_unlock_irqrestore(&dist->lpi_xa, flags);
+ oldirq = __xa_store(&dist->lpi_xa, intid, irq, 0);
+ ret = xa_err(oldirq);
+ if (ret) {
+ xa_unlock_irqrestore(&dist->lpi_xa, flags);
+ kfree(irq);
- if (ret) {
- xa_release(&dist->lpi_xa, intid);
- kfree(irq);
+ return ERR_PTR(ret);
+ }
- return ERR_PTR(ret);
+ /*
+ * The expectation is that only LPIs which are pending release
+ * could possibly be evicted by storing the new entry. Free the
+ * old IRQ only if this is the case, as otherwise someone else
+ * could still hold a pointer on the outgoing IRQ.
+ */
+ if (oldirq && !WARN_ON_ONCE(refcount_read(&oldirq->refcount) &&
+ oldirq->pending_release))
+ kfree_rcu(oldirq, rcu);
}
+ xa_unlock_irqrestore(&dist->lpi_xa, flags);
+
/*
* We "cache" the configuration table entries in our struct vgic_irq's.
* However we only have those structs for mapped IRQs, so we read in
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v4 1/2] KVM: arm64: vgic: Fix race between LPI release and re-registration
2026-07-03 22:13 ` Oliver Upton
@ 2026-07-03 22:19 ` Oliver Upton
0 siblings, 0 replies; 5+ messages in thread
From: Oliver Upton @ 2026-07-03 22:19 UTC (permalink / raw)
To: Carlos López
Cc: kvmarm, linux-kernel, maz, joey.gouly, seiden, suzuki.poulose,
yuzenghui, catalin.marinas, will, linux-arm-kernel
On Fri, Jul 03, 2026 at 03:13:34PM -0700, Oliver Upton wrote:
> Hi Carlos,
>
> Please wait a few days between spins, we were in the middle of a
> conversation and I didn't have a chance to reply to you since yesterday.
>
> On Fri, Jul 03, 2026 at 02:26:00PM +0200, Carlos López wrote:
> > diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c
> > index 67d107e9a77d..577286069368 100644
> > --- a/arch/arm64/kvm/vgic/vgic-its.c
> > +++ b/arch/arm64/kvm/vgic/vgic-its.c
> > @@ -116,7 +116,15 @@ static struct vgic_irq *vgic_add_lpi(struct kvm *kvm, u32 intid,
> > kfree(irq);
> > irq = oldirq;
> > } else {
> > - ret = xa_err(__xa_store(&dist->lpi_xa, intid, irq, 0));
> > + /*
> > + * The entry is either empty or contains a dead LPI (refcount=0)
> > + * from the deferred release path, pending cleanup by
> > + * vgic_release_deleted_lpis(). Evict and free it if present.
> > + */
> > + oldirq = __xa_store(&dist->lpi_xa, intid, irq, 0);
> > + ret = xa_err(oldirq);
> > + if (!ret && oldirq)
> > + kfree_rcu(oldirq, rcu);
>
> We need to assert the assumption here that the evicted IRQ has a nonzero
s/nonzero/zero
Obviously not reading what the hell I'm writing :)
> refcount and is pending release. Otherwise I feel like possibly leaking
> memory is better than potentially introducing a UAF.
>
> Untested diff below.
>
> Thanks,
> Oliver
>
> diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c
> index 4477f870c7b3..621a83399184 100644
> --- a/arch/arm64/kvm/vgic/vgic-its.c
> +++ b/arch/arm64/kvm/vgic/vgic-its.c
> @@ -116,18 +116,28 @@ static struct vgic_irq *vgic_add_lpi(struct kvm *kvm, u32 intid,
> kfree(irq);
> irq = oldirq;
> } else {
> - ret = xa_err(__xa_store(&dist->lpi_xa, intid, irq, 0));
> - }
> -
> - xa_unlock_irqrestore(&dist->lpi_xa, flags);
> + oldirq = __xa_store(&dist->lpi_xa, intid, irq, 0);
> + ret = xa_err(oldirq);
> + if (ret) {
> + xa_unlock_irqrestore(&dist->lpi_xa, flags);
> + kfree(irq);
>
> - if (ret) {
> - xa_release(&dist->lpi_xa, intid);
> - kfree(irq);
> + return ERR_PTR(ret);
> + }
>
> - return ERR_PTR(ret);
> + /*
> + * The expectation is that only LPIs which are pending release
> + * could possibly be evicted by storing the new entry. Free the
> + * old IRQ only if this is the case, as otherwise someone else
> + * could still hold a pointer on the outgoing IRQ.
> + */
> + if (oldirq && !WARN_ON_ONCE(refcount_read(&oldirq->refcount) &&
> + oldirq->pending_release))
if (oldirq && !WARN_ON_ONCE(refcount_read(&oldirq->refcount) ||
!oldirq->pending_release))
Thanks,
Oliver
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-07-03 22:19 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-03 12:25 [PATCH v4 0/2] KVM: arm64: vgic: Fix racy LPI release and re-registration handling Carlos López
2026-07-03 12:26 ` [PATCH v4 1/2] KVM: arm64: vgic: Fix race between LPI release and re-registration Carlos López
2026-07-03 22:13 ` Oliver Upton
2026-07-03 22:19 ` Oliver Upton
2026-07-03 12:26 ` [PATCH v4 2/2] KVM: arm64: vgic: Mitigate potential LPI registration failure Carlos López
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.