All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/2] KVM: arm64: Fix spurious warn, null ptr deref on S2 teardown race
@ 2026-08-12 13:31 Lorenzo Stoakes (ARM)
  2026-08-12 13:31 ` [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 " Lorenzo Stoakes (ARM)
  2026-08-12 13:31 ` [PATCH 2/2] KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown Lorenzo Stoakes (ARM)
  0 siblings, 2 replies; 6+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-12 13:31 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, Fuad Tabba, Joey Gouly, Steffen Eiden,
	Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
	Jintack Lim, Christoffer Dall, Christoffer Dall
  Cc: linux-arm-kernel, kvmarm, linux-kernel, Lorenzo Stoakes (ARM),
	stable

When GFNs are invalidated in L0 an MMU notifier triggers
kvm_unmap_gfn_range() which tears down all of the stage 2 shadow page
tables for nested guests via kvm_nested_s2_unmap().

To avoid lockup, the kvm->mmu_lock is dropped while doing this and the task
rescheduled once for each block of physical address space (32 MiB for 16
KiB page size), with the lock being reacquired once the task is scheduled
again.

This results in a potential race between this L0 tear down and tear down of
the guest itself in kvm_flush_shadow_all(), a race which has been observed
on real hardware.

When this race occurs it causes an invalid kernel warning when the PGT of a
nested MMU is cleared by kvm_flush_shadow_all() ->
kvm_arch_flush_shadow_all() -> kvm_free_stage2_pgd().

Patch 1 fixes this by having stage2_apply_range() no longer return an error
when it has experienced a benign race with pgt teardown when it drops the
lock.

Patch 2 addresses something more serious - bad timing can turn this spurious
warning into a NULL pointer dereference.

kvm_arch_flush_shadow_all() calls kvm_uninit_stage2_mmu() which calls
kvm_free_stage2_pgd() on the canonical kvm->arch.mmu for that guest's S2
mappings, making it NULL.

This is problematic if it happens before stage2_apply_range() reacquires
the kvm->mmu_lock, as it ultimately returns to kvm_nested_s2_unmap() which
dereferences kvm->arch.mmu.pgt with the mmu lock held under the incorrect
assumption that it means it's valid, resulting in a NULL pointer
dereference.

Fix that by checking if kvm->arch.mmu.pgt is NULL before dereferencing it
in kvm_nested_s2_unmap().

Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
Lorenzo Stoakes (ARM) (2):
      KVM: arm64: Fix spurious warning for benign stage 2 teardown race
      KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown

 arch/arm64/kvm/mmu.c    | 10 ++++++++--
 arch/arm64/kvm/nested.c |  6 +++++-
 2 files changed, 13 insertions(+), 3 deletions(-)
---
base-commit: ed8384586cf9937df7ed018fcfc625b85e0ece81
change-id: 20260811-kvm-arm-nested-virt-fix-031e9ab1be87

Best regards,
-- 
Lorenzo Stoakes (ARM) <ljs@kernel.org>


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 teardown race
  2026-08-12 13:31 [PATCH 0/2] KVM: arm64: Fix spurious warn, null ptr deref on S2 teardown race Lorenzo Stoakes (ARM)
@ 2026-08-12 13:31 ` Lorenzo Stoakes (ARM)
  2026-08-12 13:46   ` Lorenzo Stoakes (ARM)
  2026-08-12 13:54   ` sashiko-bot
  2026-08-12 13:31 ` [PATCH 2/2] KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown Lorenzo Stoakes (ARM)
  1 sibling, 2 replies; 6+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-12 13:31 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, Fuad Tabba, Joey Gouly, Steffen Eiden,
	Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
	Jintack Lim, Christoffer Dall, Christoffer Dall
  Cc: linux-arm-kernel, kvmarm, linux-kernel, Lorenzo Stoakes (ARM),
	stable

A batch of kernel warnings were triggered in the L0 host kernel when using
kvmtool to experiment with nested virtualisation.

The issue was observed on an M2 macbook pro with 24 GiB of RAM running
asahi linux 7.1.6-400.asahi.fc44.aarch64+16k with 16 KiB page size.

The bug is confirmed to exist in mainline and does not appear to be
impacted by any downstream patches carried by asahi.

kvmtool was used to establish an L1 guest with 8 CPUs and 8 GiB of RAM.

kvmtool was then run again within the L1 guest to establish an L2 guest
with 4 CPUs and 4 GiB of RAM.

Then it was run again to establish another inner guest with 2 CPUs and 2
GiB of RAM at L3.

Both the L2 and L3 guests were then exited and another L2 guest was
established with the same characteristics.

Sufficient memory pressure was present in the L0 host to trigger indirect
reclaim, waking kcompactd up and triggering migration.

Shortly afterwards the L2 guest was stopped, at which point three warnings
were observed in the L0 host in quick succession with the second and the
third occurring 114us and 189us after the first, respectively.

In each case the call trace was the same:

WARNING: arch/arm64/kvm/mmu.c:336 at __unmap_stage2_range+0x64/0x80,
CPU#5: kcompactd0/66
...
__unmap_stage2_range (arch/arm64/kvm/mmu.c:335 (discriminator 3)) (P)
kvm_stage2_unmap_range (arch/arm64/kvm/mmu.c:346)
kvm_nested_s2_unmap (arch/arm64/kvm/nested.c:1168 (discriminator 14))
kvm_unmap_gfn_range (arch/arm64/kvm/mmu.c:2416)
kvm_mmu_notifier_invalidate_range_start (arch/arm64/kvm/../../../virt/kvm/kvm_main.c:718 ...)
mn_hlist_invalidate_range_start (mm/mmu_notifier.c:525)
__mmu_notifier_invalidate_range_start (mm/mmu_notifier.c:580)
try_to_migrate_one (./include/linux/mmu_notifier.h:478 ./include/linux/mmu_notifier.h:471 mm/rmap.c:2459)
rmap_walk_anon (mm/rmap.c:3001)
rmap_walk (mm/rmap.c:3106 mm/rmap.c:3101)
try_to_migrate (mm/rmap.c:2774)
migrate_folio_unmap (mm/migrate.c:1330 (discriminator 3))
migrate_pages_batch (mm/migrate.c:1909)
migrate_pages_sync (mm/migrate.c:2026)
migrate_pages (mm/migrate.c:2135)
compact_zone (mm/compaction.c:2663)
compact_node (mm/compaction.c:2932)
kcompactd (mm/compaction.c:3230)
kthread (kernel/kthread.c:436)
ret_from_fork (arch/arm64/kernel/entry.S:858)

Analysing this:

try_to_migrate() performs the first part of migration on a folio -
establishing migration entries in all page tables mapping it - calling
try_to_migrate_one() for each VMA the folio is mapped in.

Immediately prior to installing the migration entry, try_to_migrate_one()
calls mmu_notifier_invalidate_range_start() to signal to notifiers that the
existing page table entry is about to be unmapped.

Since this range happened to contain folios used by the virtual machine
(given its memory consumption this was highly likely to be a target) this
in turn triggers arm64 kvm code via an MMU notifier:

mmu_notifier_invalidate_range_start()
  -> ... -> kvm_mmu_notifier_invalidate_range_start()
    -> kvm_mmu_unmap_gfn_range()
      -> kvm_unmap_gfn_range()
        -> kvm_nested_s2_unmap()
          -> kvm_stage2_unmap_range()
            -> __unmap_stage2_range()
               -> stage2_apply_range()
       	       <- -EINVAL, triggering a WARN_ON()

Since commit ec14c272408a ("KVM: arm64: nv: Unmap/flush shadow stage 2 page
tables") kvm_unmap_gfn_range() invokes kvm_nested_s2_unmap() which takes
the rather drastic step of evicting the entirety of the stage 2 shadow page
tables for each nested guest:

	for (i = 0; i < kvm->arch.nested_mmus_size; i++) {
		struct kvm_s2_mmu *mmu = &kvm->arch.nested_mmus[i];

		if (kvm_s2_mmu_valid(mmu))
			kvm_stage2_unmap_range(mmu, 0, kvm_phys_size(mmu), may_block);
	}

This will iterate through every valid MMU.

mmu_notifier_invalidate_range_start() sets the MMU_NOTIFIER_RANGE_BLOCKABLE
flag when signalling notifiers, so may_block is true here.

__unmap_stage2_range() wraps invocation of stage2_apply_range() in a
WARN_ON():

	WARN_ON(stage2_apply_range(mmu, start, end, KVM_PGT_FN(kvm_pgtable_stage2_unmap),
				   may_block));

Which is precisely the triggered warning and means stage2_apply_range() is
returning an error.

In arm64 return values are stored in the x0 register and in each splat w0
(since it's a 32-bit value) is 0xffffffea which, by two's complement, is
-0b00010110 or -EINVAL.

The only way in which stage2_apply_range() can return an error is if either
the passed in walk function (kvm_pgtable_stage2_unmap()) returns an error
or mmu->pgt is NULL:

	static int stage2_apply_range(...)
	{
		do {
			struct kvm_pgtable *pgt = mmu->pgt;
			if (!pgt)
				return -EINVAL;

			next = stage2_range_addr_end(addr, end);
			ret = fn(pgt, addr, next - addr);
			if (ret)
				break;
			if (resched && next != end)
				cond_resched_rwlock_write(&kvm->mmu_lock);
		} while (addr = next, addr != end);

		return ret;
	}

This loop is batched by stage2_range_addr_end() at the granularity of
kvm_granule_size(KVM_PGTABLE_MIN_BLOCK_LEVEL), which for a 16 KiB page size
kernel is 32 MiB.

kvm_pgtable_stage2_unmap() only returns an error if
kvm_pgtable_walk_begin() or _kvm_pgtable_walk() return an error. On arm64
the former doesn't ever do so, and the latter only returns -EINVAL if
either pgd is NULL (we gate that already) or an invalid pgt->start_level is
specified (not the case).

So the cause of the warning is that mmu->pgt is NULL here.

This field is protected by kvm->mmu_lock, but after each invocation of the
walk function, if the end of the range has not yet been reached, the lock
is dropped.

This is done by calling cond_resched_rwlock_write() which drops
kvm->mmu_lock when yielding the time slice before reacquiring it upon being
scheduled again:

		if (resched && next != end)
			cond_resched_rwlock_write(&kvm->mmu_lock);

Note that resched = may_block here and is always true for these walks.

This points to something else racing this code to clearing the pgt, and
brings us back to the observation above that the issue occurs when tearing
down the L2 guest.

Upon L0's userspace mm_struct teardown, stage 2 mappings for IPAs mapped by
the guest are unmapped via mmu notifier:

exit_mm()
  -> mmput()
    -> __mmput()
      -> exit_mmap()
        -> mmu_notifier_release()
	  -> ... -> kvm_mmu_notifier_release()
            -> kvm_flush_shadow_all()
              -> kvm_arch_flush_shadow_all()
	        -> kvm_free_stage2_pgd()
		  -> [ acquire kvm->mmu_lock for write ]
		  -> mmu->pgt = NULL [ among other tasks ]
		  -> [ release kvm->mmu_lock for write ]

So there is a benign race resulting in a spurious warning:

	Thread A - notify: migration   | Thread B - notify: release
	-------------------------------|---------------------------------
	< kvm->mmu_lock held >         |
	stage2_apply_range()           |
	  get mmu->pgt, check !NULL    |
	  ...                          | kvm_arch_flush_shadow_all()
	  cond_resched_rwlock_write(); |   < contend, sleep kvm->mmu_lock >
	< drop kvm->mmu_lock >         |   < acquire kvm->mmu_lock>
                                       |   ...
				       |   kvm_free_stage2_pgd()
                                       |     mmu->pgt = NULL
				       |   < invalidate MMU >
				       |   ...
				       |   < release kvm->mmu_lock >
	[ scheduled ]		       |
	stage2_apply_range()           |
	  < loop to next >             |
	  get, mmu->pgt, check !NULL   |
	  is NULL, return -EINVAL      |
        __unmap_stage2_range()         |
	  WARN_ON(-EINVAL) <--- entirely spurious - the race was handled
                                 correctly.

This does not hide any other errors as the only other error that can arise
is from kvm_pgtable_stage2_unmap() which calls kvm_pgtable_walk()
first which gates on !NULL pgt->pgd returning -EINVAL if it is NULL anyway.

On reflection this race seems rather likely when there is host memory
pressure concurrent with nested guest teardown - the lock release will
cause interleaving warnings once for every valid MMU as long as at least 1
block remains to be unmapped by stage2_apply_range().

Fix the spurious warning by updating stage2_apply_range() to no longer
treat concurrent PGT teardown on lock release as an error - whether the
walker is tearing down page tables or doing something else this is a
legitimate reason to abort the operation without error.

This keeps the warning in place for all other circumstances.

In practice only __unmap_stage2_range() actually does anything with the
error so this only impacts that.

Fixes: ec14c272408a ("KVM: arm64: nv: Unmap/flush shadow stage 2 page tables")
Cc: stable@vger.kernel.org
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
 arch/arm64/kvm/mmu.c | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/arch/arm64/kvm/mmu.c b/arch/arm64/kvm/mmu.c
index 74e7e7f7564c..31e049ded093 100644
--- a/arch/arm64/kvm/mmu.c
+++ b/arch/arm64/kvm/mmu.c
@@ -59,19 +59,25 @@ static phys_addr_t stage2_range_addr_end(phys_addr_t addr, phys_addr_t end)
  * long will also starve other vCPUs. We have to also make sure that the page
  * tables are not freed while we released the lock.
  */
-static int stage2_apply_range(struct kvm_s2_mmu *mmu, phys_addr_t addr,
+static int stage2_apply_range(struct kvm_s2_mmu *mmu, phys_addr_t start,
 			      phys_addr_t end,
 			      int (*fn)(struct kvm_pgtable *, u64, u64),
 			      bool resched)
 {
 	struct kvm *kvm = kvm_s2_mmu_to_kvm(mmu);
+	phys_addr_t addr = start;
 	int ret;
 	u64 next;
 
 	do {
 		struct kvm_pgtable *pgt = mmu->pgt;
+		/*
+		 * We may be raced on PGT teardown when we release the
+		 * kvm->mmu_lock. That's fine as the PGT is legitimately no
+		 * longer present.
+		 */
 		if (!pgt)
-			return -EINVAL;
+			return resched && addr > start ? 0 : -EINVAL;
 
 		next = stage2_range_addr_end(addr, end);
 		ret = fn(pgt, addr, next - addr);

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/2] KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown
  2026-08-12 13:31 [PATCH 0/2] KVM: arm64: Fix spurious warn, null ptr deref on S2 teardown race Lorenzo Stoakes (ARM)
  2026-08-12 13:31 ` [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 " Lorenzo Stoakes (ARM)
@ 2026-08-12 13:31 ` Lorenzo Stoakes (ARM)
  2026-08-12 14:01   ` sashiko-bot
  1 sibling, 1 reply; 6+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-12 13:31 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, Fuad Tabba, Joey Gouly, Steffen Eiden,
	Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
	Jintack Lim, Christoffer Dall, Christoffer Dall
  Cc: linux-arm-kernel, kvmarm, linux-kernel, Lorenzo Stoakes (ARM),
	stable

Commit 7270cc9157f4 ("KVM: arm64: nv: Handle VNCR_EL2 invalidation from MMU
notifiers") introduced VNCR_EL2 invalidation in kvm_nested_s2_unmap().

However at the point of this being performed concurrent stage 2 teardown of
a nested guest can cause kvm->arch.mmu.pgt to be set to NULL.

This happens in kvm_flush_shadow_all() -> kvm_arch_flush_shadow_all() ->
kvm_free_stage2_pgd() and is performed under the kvm->mmu_lock.

Commit ec14c272408a ("KVM: arm64: nv: Unmap/flush shadow stage 2 page
tables") introduced the teardown of the entire nested MMU range, which then
invokes stage2_apply_range() with resched=true:

mmu_notifier_invalidate_range_start()
  -> ... -> kvm_mmu_notifier_invalidate_range_start()
    -> kvm_mmu_unmap_gfn_range()
      -> kvm_unmap_gfn_range()
        -> kvm_nested_s2_unmap()
          -> kvm_stage2_unmap_range()
            -> __unmap_stage2_range()
                -> stage2_apply_range()

This means that stage2_apply_range() can drop the kvm->mmu_lock and thus
concurrent progress can be made in lockstep with
kvm_arch_flush_shadow_all().

If kvm_arch_flush_shadow_all() advances ahead of stage2_apply_range() and
completes its operation it guarantees a NULL pointer deref.

Since kvm_free_stage2_pgd() is performed under the kvm->mmu_lock this will
either be observed NULL or not and serialised against
kvm_free_stage2_pgd().

Resolve the issue by explicitly checking for a NULL value. Since this
shouldn't be possible if blocking is not allowed, raise a warning if it is
ever NULL in this case.

Fixes: 7270cc9157f4 ("KVM: arm64: nv: Handle VNCR_EL2 invalidation from MMU notifiers")
Cc: stable@vger.kernel.org
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
 arch/arm64/kvm/nested.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/arch/arm64/kvm/nested.c b/arch/arm64/kvm/nested.c
index 17123f0b6dab..9fc0b1696dcb 100644
--- a/arch/arm64/kvm/nested.c
+++ b/arch/arm64/kvm/nested.c
@@ -1295,7 +1295,11 @@ void kvm_nested_s2_unmap(struct kvm *kvm, bool may_block)
 			kvm_stage2_unmap_range(mmu, 0, kvm_phys_size(mmu), may_block);
 	}
 
-	kvm_invalidate_vncr_ipa(kvm, 0, BIT(kvm->arch.mmu.pgt->ia_bits));
+	/* NULL pgt should only be possible if raced when mmu_lock dropped. */
+	if (kvm->arch.mmu.pgt)
+		kvm_invalidate_vncr_ipa(kvm, 0, BIT(kvm->arch.mmu.pgt->ia_bits));
+	else
+		WARN_ON(!may_block);
 }
 
 void kvm_nested_s2_flush(struct kvm *kvm)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 teardown race
  2026-08-12 13:31 ` [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 " Lorenzo Stoakes (ARM)
@ 2026-08-12 13:46   ` Lorenzo Stoakes (ARM)
  2026-08-12 13:54   ` sashiko-bot
  1 sibling, 0 replies; 6+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-12 13:46 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, Fuad Tabba, Joey Gouly, Steffen Eiden,
	Suzuki K Poulose, Zenghui Yu, Catalin Marinas, Will Deacon,
	Jintack Lim, Christoffer Dall, Christoffer Dall
  Cc: linux-arm-kernel, kvmarm, linux-kernel, stable

The splats:

------------[ cut here ]------------
WARNING: arch/arm64/kvm/mmu.c:336 at __unmap_stage2_range+0x64/0x80, CPU#5: kcompactd0/66
...
Tainted: [S]=CPU_OUT_OF_SPEC
Hardware name: Apple Inc. Mac14,7/J493, BIOS 2026.04 04/01/2026
pstate: 61401009 (nZCv daif +PAN -UAO -TCO +DIT +SSBS BTYPE=--)
pc : __unmap_stage2_range (arch/arm64/kvm/mmu.c:335 (discriminator 3))
lr : __unmap_stage2_range (arch/arm64/kvm/mmu.c:336 (discriminator 5))
sp : ffff8000856e7450
x29: ffff8000856e7450 x28: ffff0000404b2000 x27: 0000000000000030
x26: ffffffffffffffe0 x25: 0000000000000001 x24: ffff00000eaa5920
x23: ffff0000404b2000 x22: 0000000000000000 x21: 0000000000000040
x20: 0000000000000001 x19: 0000000000000001 x18: 0000000000000006
x17: 0000000000000000 x16: 0000000000000002 x15: ffff800083adbed0
x14: ffff800083adbed0 x13: 0000000000000008 x12: 0000b00000000000
x11: 0000000000000001 x10: 0000006000000000 x9 : ffff8000817334fc
x8 : 0000000000200000 x7 : ffff0000c59ea000 x6 : 0000000000000800
x5 : 0000000038000000 x4 : 0000000000000000 x3 : ffff0000404b2000
x2 : 0000000000000001 x1 : 0000000038000000 x0 : 00000000ffffffea
Call trace:
__unmap_stage2_range (arch/arm64/kvm/mmu.c:335 (discriminator 3)) (P)
kvm_stage2_unmap_range (arch/arm64/kvm/mmu.c:346)
kvm_nested_s2_unmap (arch/arm64/kvm/nested.c:1168 (discriminator 14))
kvm_unmap_gfn_range (arch/arm64/kvm/mmu.c:2416)
kvm_mmu_notifier_invalidate_range_start (arch/arm64/kvm/../../../virt/kvm/kvm_main.c:718 (discriminator 32768) arch/arm64/kvm/../../../virt/kvm/kvm_main.c:633 (discriminator 32768) arch/arm64/kvm/../../../virt/kvm/kvm_main.c:765 (discriminator 32768))
mn_hlist_invalidate_range_start (mm/mmu_notifier.c:525)
__mmu_notifier_invalidate_range_start (mm/mmu_notifier.c:580)
try_to_migrate_one (./include/linux/mmu_notifier.h:478 ./include/linux/mmu_notifier.h:471 mm/rmap.c:2459)
rmap_walk_anon (mm/rmap.c:3001)
rmap_walk (mm/rmap.c:3106 mm/rmap.c:3101)
try_to_migrate (mm/rmap.c:2774)
migrate_folio_unmap (mm/migrate.c:1330 (discriminator 3))
migrate_pages_batch (mm/migrate.c:1909)
migrate_pages_sync (mm/migrate.c:2026)
migrate_pages (mm/migrate.c:2135)
compact_zone (mm/compaction.c:2663)
compact_node (mm/compaction.c:2932)
kcompactd (mm/compaction.c:3230)
kthread (kernel/kthread.c:436)
ret_from_fork (arch/arm64/kernel/entry.S:858)
---[ end trace 0000000000000000 ]---

------------[ cut here ]------------
WARNING: arch/arm64/kvm/mmu.c:336 at __unmap_stage2_range+0x64/0x80, CPU#5: kcompactd0/66
...
Tainted: [S]=CPU_OUT_OF_SPEC, [W]=WARN
Hardware name: Apple Inc. Mac14,7/J493, BIOS 2026.04 04/01/2026
pstate: 61401009 (nZCv daif +PAN -UAO -TCO +DIT +SSBS BTYPE=--)
pc : __unmap_stage2_range (arch/arm64/kvm/mmu.c:335 (discriminator 3))
lr : __unmap_stage2_range (arch/arm64/kvm/mmu.c:336 (discriminator 5))
sp : ffff8000856e7450
x29: ffff8000856e7450 x28: ffff0000404b2000 x27: 0000000000000030
x26: ffffffffffffffe0 x25: 0000000000000001 x24: ffff00000eaa5920
x23: ffff0000404b2000 x22: 0000000000000001 x21: 0000000000000040
x20: 0000000000000001 x19: 0000000000000001 x18: 0000000000000006
x17: 0000000000000000 x16: 0000000000000002 x15: ffff800083adbed0
x14: ffff800083adbed0 x13: 0000000000000008 x12: 0000b00000000000
x11: 0000000000000001 x10: 0000006000000000 x9 : ffff8000817334fc
x8 : 0000000000200000 x7 : ffff0000c59ea078 x6 : 0000000000000800
x5 : 0000000002000000 x4 : 0000000000000000 x3 : ffff0000404b2000
x2 : 0000000000000001 x1 : 0000000002000000 x0 : 00000000ffffffea
Call trace:
__unmap_stage2_range (arch/arm64/kvm/mmu.c:335 (discriminator 3)) (P)
kvm_stage2_unmap_range (arch/arm64/kvm/mmu.c:346)
kvm_nested_s2_unmap (arch/arm64/kvm/nested.c:1168 (discriminator 14))
kvm_unmap_gfn_range (arch/arm64/kvm/mmu.c:2416)
kvm_mmu_notifier_invalidate_range_start (arch/arm64/kvm/../../../virt/kvm/kvm_main.c:718 (discriminator 32768) arch/arm64/kvm/../../../virt/kvm/kvm_main.c:633 (discriminator 32768) arch/arm64/kvm/../../../virt/kvm/kvm_main.c:765 (discriminator 32768))
mn_hlist_invalidate_range_start (mm/mmu_notifier.c:525)
__mmu_notifier_invalidate_range_start (mm/mmu_notifier.c:580)
try_to_migrate_one (./include/linux/mmu_notifier.h:478 ./include/linux/mmu_notifier.h:471 mm/rmap.c:2459)
rmap_walk_anon (mm/rmap.c:3001)
rmap_walk (mm/rmap.c:3106 mm/rmap.c:3101)
try_to_migrate (mm/rmap.c:2774)
migrate_folio_unmap (mm/migrate.c:1330 (discriminator 3))
migrate_pages_batch (mm/migrate.c:1909)
migrate_pages_sync (mm/migrate.c:2026)
migrate_pages (mm/migrate.c:2135)
compact_zone (mm/compaction.c:2663)
compact_node (mm/compaction.c:2932)
kcompactd (mm/compaction.c:3230)
kthread (kernel/kthread.c:436)
ret_from_fork (arch/arm64/kernel/entry.S:858)
---[ end trace 0000000000000000 ]---

------------[ cut here ]------------
WARNING: arch/arm64/kvm/mmu.c:336 at __unmap_stage2_range+0x64/0x80, CPU#5: kcompactd0/66
...
Tainted: [S]=CPU_OUT_OF_SPEC, [W]=WARN
Hardware name: Apple Inc. Mac14,7/J493, BIOS 2026.04 04/01/2026
pstate: 61401009 (nZCv daif +PAN -UAO -TCO +DIT +SSBS BTYPE=--)
pc : __unmap_stage2_range (arch/arm64/kvm/mmu.c:335 (discriminator 3))
lr : __unmap_stage2_range (arch/arm64/kvm/mmu.c:336 (discriminator 5))
sp : ffff8000856e7450
x29: ffff8000856e7450 x28: ffff0000404b2000 x27: 0000000000000030
x26: ffffffffffffffe0 x25: 0000000000000001 x24: ffff00000eaa5920
x23: ffff0000404b2000 x22: 0000000000000002 x21: 0000000000000040
x20: 0000000000000001 x19: 0000000000000001 x18: 0000000000000006
x17: 0000000000000000 x16: 0000000000000002 x15: ffff800083adbed0
x14: ffff800083adbed0 x13: 0000000000000008 x12: 0000b00000000000
x11: 0000000000000001 x10: 0000006000000000 x9 : ffff8000817334fc
x8 : 0000000000200000 x7 : ffff0000c59ea0f0 x6 : 0000000000000800
x5 : 0000000002000000 x4 : 0000000000000000 x3 : ffff0000404b2000
x2 : 0000000000000001 x1 : 0000000002000000 x0 : 00000000ffffffea
Call trace:
__unmap_stage2_range (arch/arm64/kvm/mmu.c:335 (discriminator 3)) (P)
kvm_stage2_unmap_range (arch/arm64/kvm/mmu.c:346)
kvm_nested_s2_unmap (arch/arm64/kvm/nested.c:1168 (discriminator 14))
kvm_unmap_gfn_range (arch/arm64/kvm/mmu.c:2416)
kvm_mmu_notifier_invalidate_range_start (arch/arm64/kvm/../../../virt/kvm/kvm_main.c:718 (discriminator 32768) arch/arm64/kvm/../../../virt/kvm/kvm_main.c:633 (discriminator 32768) arch/arm64/kvm/../../../virt/kvm/kvm_main.c:765 (discriminator 32768))
mn_hlist_invalidate_range_start (mm/mmu_notifier.c:525)
__mmu_notifier_invalidate_range_start (mm/mmu_notifier.c:580)
try_to_migrate_one (./include/linux/mmu_notifier.h:478 ./include/linux/mmu_notifier.h:471 mm/rmap.c:2459)
rmap_walk_anon (mm/rmap.c:3001)
rmap_walk (mm/rmap.c:3106 mm/rmap.c:3101)
try_to_migrate (mm/rmap.c:2774)
migrate_folio_unmap (mm/migrate.c:1330 (discriminator 3))
migrate_pages_batch (mm/migrate.c:1909)
migrate_pages_sync (mm/migrate.c:2026)
migrate_pages (mm/migrate.c:2135)
compact_zone (mm/compaction.c:2663)
compact_node (mm/compaction.c:2932)
kcompactd (mm/compaction.c:3230)
kthread (kernel/kthread.c:436)
ret_from_fork (arch/arm64/kernel/entry.S:858)
---[ end trace 0000000000000000 ]---

--

Also I wrote something extra but dropped from the already-far-too-long commit
msg but the registers match the diagnosis, basically:

--

The observed assembly from the splats is consistent with this analysis:

Examining the assembly for stage2_apply_range() loop and pgt check,
conveniently x1 contains the current IPA being examined (addr) and is not
clobbered on return:

	mov  x1, x19			addr = next
	ldr  x0, [x26, #16]		struct kvm_pgtable *pgt = mmu->pgt;
	cbnz x0, ...			if (!pgt)...
	ldp  x19, x20, [sp, #16]	}
	mov  w0, #0xffffffea		} return -EINVAL
	ret				}

Splat 1 has x1 of 0x38000000 which tells us the splat occurred 27 * 32 MiB
or 864 MiB into the nested guest physical address space (27 iterations).

This is consistent with an an interleaving stage 2 tear down occurring
part-way through this operation.

There being 2 other splats is consistent with there being a total of 3
nested mmus, as expected, and that the mmu is invalidated before the
warning can occur.

The remaining 2 have x1 equal to 0x2000000 or exactly 32 MiB, which is what
you would expect with the stage2_apply_range() and
kvm_arch_flush_shadow_all() running in lockstep across mmus:

	     kvm_unmap_gfn_range()       | kvm_arch_flush_shadow_all()
	---------------------------------|----------------------------
	< kvm->mmu_lock held >           |
	stage2_apply_range()             |
	  .. 26 successful iterations .. | < contend kvm->mmu_lock >
	  < releases kvm->mmu lock >     | < acquires kvm->mmu_lock >
	                                 | mmu->pgt = NULL
	                                 | < releases kvm->mmu_lock >

Then 3 occurrences of:

	  < reacquires kvm->mmu_lock >   |
	return to __unmap_stage2_range() |
	  WARN_ON(-EINVAL);              |
        mmu = &kvm->arch.nested_mmus[1]; |
	stage2_apply_range()             |
	  .. 1 successful iteration ..   |
	  < releases kvm->mmu lock >     | < contend kvm->mmu_lock >
					 | < acquires kvm->mmu_lock >
					 | mmu->pgt = NULL
					 | < releases kvm->mmu_lock >

One single iteration sets addr = next = 0x2000000 which is reflected in the
splats.

Finally, in kvm_nested_s2_unmap() the disassembly indicates that w22
maintains the i counter, which happens not to be clobbered over the
operation either:

	mov  w22, #0x0		for (i = 0;
	add  w22, w22, #0x1     	                 ; i++)
	sxtw x2, w22		}
	cmp  x2, x1		}    i < nested_mmus_size

The splats have w22 equal to 0, 1 and 2 respectively which further
corroborates this analysis.

--
Cheers, Lorenzo

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 teardown race
  2026-08-12 13:31 ` [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 " Lorenzo Stoakes (ARM)
  2026-08-12 13:46   ` Lorenzo Stoakes (ARM)
@ 2026-08-12 13:54   ` sashiko-bot
  1 sibling, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-08-12 13:54 UTC (permalink / raw)
  To: Lorenzo Stoakes (ARM); +Cc: kvmarm, Oliver Upton, Marc Zyngier

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Pre-existing issues:
- [Critical] Use-After-Free of the dynamically allocated `kvm->arch.nested_mmus` array when the MMU lock is dropped during stage-2 unmap operations.
- [High] NULL Pointer Dereference in `kvm_vcpu_init_nested()` during concurrent VM teardown.
--

--- Patch [1]: [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 teardown race ---
commit ffda2ec0115c7506d269acccba8c1f4511410e87
Author: Lorenzo Stoakes (ARM) <ljs@kernel.org>

KVM: arm64: Fix spurious warning for benign stage 2 teardown race

This patch suppresses a warning in stage2_apply_range() when the page table
pointer is found to be NULL after reacquiring the MMU lock. It assumes that
the NULL page table is a benign result of a concurrent teardown.

> diff --git a/arch/arm64/kvm/mmu.c b/arch/arm64/kvm/mmu.c
> index 74e7e7f7564c8..31e049ded0932 100644
> --- a/arch/arm64/kvm/mmu.c
> +++ b/arch/arm64/kvm/mmu.c
> @@ -59,19 +59,25 @@ static phys_addr_t stage2_range_addr_end(phys_addr_t addr, phys_addr_t end)
>   * long will also starve other vCPUs. We have to also make sure that the page
>   * tables are not freed while we released the lock.
>   */
> -static int stage2_apply_range(struct kvm_s2_mmu *mmu, phys_addr_t addr,
> +static int stage2_apply_range(struct kvm_s2_mmu *mmu, phys_addr_t start,
>  			      phys_addr_t end,
>  			      int (*fn)(struct kvm_pgtable *, u64, u64),
>  			      bool resched)
>  {
>  	struct kvm *kvm = kvm_s2_mmu_to_kvm(mmu);
> +	phys_addr_t addr = start;
>  	int ret;
>  	u64 next;
>  
>  	do {
>  		struct kvm_pgtable *pgt = mmu->pgt;
> +		/*
> +		 * We may be raced on PGT teardown when we release the
> +		 * kvm->mmu_lock. That's fine as the PGT is legitimately no
> +		 * longer present.
> +		 */
>  		if (!pgt)
> -			return -EINVAL;
> +			return resched && addr > start ? 0 : -EINVAL;

[Severity: Critical]
This is a pre-existing issue, but does suppressing this warning mask a
regression where the mmu pointer itself is used after being freed?

When kvm_nested_s2_unmap() passes an mmu pointer to stage2_apply_range(),
that pointer references a dynamically allocated array:

arch/arm64/kvm/nested.c:kvm_nested_s2_unmap() {
    ...
    struct kvm_s2_mmu *mmu = &kvm->arch.nested_mmus[i];
    if (kvm_s2_mmu_valid(mmu))
        kvm_stage2_unmap_range(mmu, ...);
}

If cond_resched_rwlock_write() drops the lock inside stage2_apply_range(),
a concurrent VM teardown or VCPU initialization can completely free this
array:

arch/arm64/kvm/nested.c:kvm_arch_flush_shadow_all() {
    ...
    kvfree(kvm->arch.nested_mmus);
    kvm->arch.nested_mmus = NULL;
}

When the lock is reacquired, the loop continues and accesses mmu->pgt
from the now-freed memory. Is it possible that the warning was actually
firing because we were reading a freed mmu structure?


[Severity: High]
This is also a pre-existing issue, but looking at the concurrent VM teardown
path, does kvm_vcpu_init_nested() have a NULL pointer dereference?

When kvm_vcpu_init_nested() reallocates the array, it unconditionally
updates the mmu pointer for each entry:

arch/arm64/kvm/nested.c:kvm_vcpu_init_nested() {
    ...
    for (int i = 0; i < kvm->arch.nested_mmus_size; i++)
        tmp[i].pgt->mmu = &tmp[i];
    ...
}

If kvm_arch_flush_shadow_all() concurrently calls kvm_free_stage2_pgd()
and sets pgt to NULL before the lock is acquired here, wouldn't this crash
when dereferencing tmp[i].pgt?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260812-kvm-arm-nested-virt-fix-v1-0-4ad883f1b6a5@kernel.org?part=1

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 2/2] KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown
  2026-08-12 13:31 ` [PATCH 2/2] KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown Lorenzo Stoakes (ARM)
@ 2026-08-12 14:01   ` sashiko-bot
  0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-08-12 14:01 UTC (permalink / raw)
  To: Lorenzo Stoakes (ARM); +Cc: kvmarm, Oliver Upton, Marc Zyngier

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:

Pre-existing issues:
- [Critical] Use-After-Free on `kvm->arch.nested_mmus` array due to concurrent teardown.
- [Critical] Incomplete Fix: `kvm_nested_s2_wp()` is vulnerable to the same NULL pointer dereference and UAF.
- [High] Data Race: `kvm->arch.nested_mmus_size` updated locklessly causing uninitialized memory reads.
--

--- Patch [2]: [PATCH 2/2] KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown ---
commit 04c9d62ec51b0000a28c8099341f6a2d31ce7848
Author: Lorenzo Stoakes (ARM) <ljs@kernel.org>

KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown

This commit adds a NULL check for kvm->arch.mmu.pgt in kvm_nested_s2_unmap()
to prevent a NULL pointer dereference. This happens when the MMU lock is
dropped during stage 2 teardown, allowing concurrent progress that clears the
page table.

> diff --git a/arch/arm64/kvm/nested.c b/arch/arm64/kvm/nested.c
> index 17123f0b6daba..9fc0b1696dcb6 100644
> --- a/arch/arm64/kvm/nested.c
> +++ b/arch/arm64/kvm/nested.c
> @@ -1295,7 +1295,11 @@ void kvm_nested_s2_unmap(struct kvm *kvm, bool may_block)
>  			kvm_stage2_unmap_range(mmu, 0, kvm_phys_size(mmu), may_block);

[Severity: Critical]
This isn't a bug introduced by this patch, but does this loop contain a
Use-After-Free vulnerability?

While iterating over the nested_mmus array in kvm_nested_s2_unmap(),
kvm_stage2_unmap_range() can drop kvm->mmu_lock inside stage2_apply_range().

Concurrently, if kvm_mmu_notifier_release() calls kvm_arch_flush_shadow_all(),
it frees the kvm->arch.nested_mmus array unconditionally and without taking
kvm->mmu_lock:

kvm_arch_flush_shadow_all() {
    ...
    kvfree(kvm->arch.nested_mmus);
    ...
}

If the array is freed during the lock-drop window, the local mmu pointer
becomes dangling, and accessing it when the lock is reacquired could result
in a Use-After-Free.

>  	}
>  
> -	kvm_invalidate_vncr_ipa(kvm, 0, BIT(kvm->arch.mmu.pgt->ia_bits));
> +	/* NULL pgt should only be possible if raced when mmu_lock dropped. */
> +	if (kvm->arch.mmu.pgt)
> +		kvm_invalidate_vncr_ipa(kvm, 0, BIT(kvm->arch.mmu.pgt->ia_bits));
> +	else
> +		WARN_ON(!may_block);
>  }

[Severity: Critical]
This is a pre-existing issue, but does kvm_nested_s2_wp() suffer from the exact
same NULL pointer dereference and Use-After-Free vulnerability?

Like kvm_nested_s2_unmap(), kvm_nested_s2_wp() iterates over the nested_mmus
array and drops mmu_lock via stage2_apply_range_resched(). When it resumes
and exits the loop, it also unconditionally executes
BIT(kvm->arch.mmu.pgt->ia_bits).

Should the same fix be applied to kvm_nested_s2_wp() to prevent a guaranteed
NULL pointer dereference when kvm_arch_flush_shadow_all() clears the page
table in the window where the lock is dropped?

[Severity: High]
This isn't a bug introduced by this patch, but is there a data race on
kvm->arch.nested_mmus_size?

In kvm_vcpu_init_nested(), nested_mmus_size is updated outside of
kvm->mmu_lock and without memory barriers:

kvm_vcpu_init_nested() {
    ...
    kvm->arch.nested_mmus_size = num_mmus;
    ...
}

A concurrent reader like kvm_nested_s2_unmap() can see the updated size
before the stores initializing the new array elements become visible. Because
the new array is allocated with kvcalloc(), an uninitialized/zeroed
mmu->tlb_vttbr causes kvm_s2_mmu_valid() to incorrectly evaluate to TRUE.

Will this cause the reader to process the zeroed mmu structure, passing it
to kvm_stage2_unmap_range(), which would dereference a NULL mmu->pgt or
access a partially initialized pgt?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260812-kvm-arm-nested-virt-fix-v1-0-4ad883f1b6a5@kernel.org?part=2

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-08-12 14:01 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-12 13:31 [PATCH 0/2] KVM: arm64: Fix spurious warn, null ptr deref on S2 teardown race Lorenzo Stoakes (ARM)
2026-08-12 13:31 ` [PATCH 1/2] KVM: arm64: Fix spurious warning for benign stage 2 " Lorenzo Stoakes (ARM)
2026-08-12 13:46   ` Lorenzo Stoakes (ARM)
2026-08-12 13:54   ` sashiko-bot
2026-08-12 13:31 ` [PATCH 2/2] KVM: arm64: nv: Fix null ptr deref in kvm_nested_s2_unmap() on S2 teardown Lorenzo Stoakes (ARM)
2026-08-12 14:01   ` sashiko-bot

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.