From: Yuchao Zhang <ndaugoing@gmail.com>
To: Marc Zyngier <maz@kernel.org>, Oliver Upton <oliver.upton@linux.dev>
Cc: Fuad Tabba <tabba@google.com>, James Morse <james.morse@arm.com>,
Suzuki K Poulose <suzuki.poulose@arm.com>,
Zenghui Yu <yuzenghui@huawei.com>,
Catalin Marinas <catalin.marinas@arm.com>,
Will Deacon <will@kernel.org>,
kvmarm@lists.linux.dev, linux-arm-kernel@lists.infradead.org,
linux-kernel@vger.kernel.org, Yuchao Zhang <ndaugoing@gmail.com>
Subject: [PATCH v3 0/1] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay
Date: Sun, 20 Sep 2026 09:49:10 +0800 [thread overview]
Message-ID: <20260920014911.58616-1-ndaugoing@gmail.com> (raw)
In-Reply-To: <20260918040214.85580-1-ndaugoing@gmail.com>
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
next prev parent reply other threads:[~2026-09-20 1:49 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Yuchao Zhang [this message]
2026-09-20 1:49 ` [PATCH v3] KVM: arm64: vgic: Drop last_lr_irq and serialize overflow EOI replay Yuchao Zhang
2026-09-20 23:47 ` [PATCH v3 0/1] " Marc Zyngier
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260920014911.58616-1-ndaugoing@gmail.com \
--to=ndaugoing@gmail.com \
--cc=catalin.marinas@arm.com \
--cc=james.morse@arm.com \
--cc=kvmarm@lists.linux.dev \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=maz@kernel.org \
--cc=oliver.upton@linux.dev \
--cc=suzuki.poulose@arm.com \
--cc=tabba@google.com \
--cc=will@kernel.org \
--cc=yuzenghui@huawei.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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®