[PATCH 0/4] KVM: arm64: Reduce overhead of full S2 teardown
Shuai Xue
xueshuai at linux.alibaba.com
Mon Sep 14 02:06:33 PDT 2026
On 9/14/26 4:14 PM, Marc Zyngier wrote:
> On Mon, 14 Sep 2026 07:44:15 +0100,
> Shuai Xue <xueshuai at linux.alibaba.com> wrote:
>>
>>
>>
>> On 9/12/26 6:48 PM, Marc Zyngier wrote:
>>> Tearing down a full S2 is a pretty involved process, resulting in a
>>> lot of TLB invalidation. These TLBIs are either on a per leaf basis if
>>> the HW doesn't support range invalidation, or by top-level range if it
>>> does. Amusingly, the latter occurs even when nothing has been
>>> unmapped.
>>>
>>> Things are made worse with NV, as we have a bucket-load of shadow S2s,
>>> and the need to invalidate them all on the back of an MMU notifier.
>>> The latter will eventually be solved by the reverse-map tracking that
>>> Wei-Lin is working on, but we need to be better at full-S2 teardown.
>>>
>>> This small series adds a "no TLBI" unmapping primitive, which allows
>>> the caller to then whack the TLBs using a VMID-wide invalidation. This
>>> results in far fewer TLBIs, and a better recursive virtualisation as
>>> we get far fewer traps as a consequence.
>>>
>>> This applies on top of my shadow-s2 lifetime fixes, and is expected to
>>> be a prefix to Wei-Lin's series.
>>>
>>> Marc Zyngier (4):
>>> KVM: arm64: pgtable: Add Stage-2 unmap without TLBI primitive
>>> KVM: arm64: MMU: Add kvm_stage2_unmap_all() helper
>>> KVM: arm64: nv: Move full s2_mmu unmap over to kvm_stage2_unmap_all()
>>> KVM: arm64: nv: Move TLBI VMALLS12E1* emulation over to
>>> kvm_stage2_unmap_all()
>>>
>>> arch/arm64/include/asm/kvm_mmu.h | 1 +
>>> arch/arm64/include/asm/kvm_pgtable.h | 17 +++++++++++++
>>> arch/arm64/include/asm/kvm_pkvm.h | 1 +
>>> arch/arm64/kvm/hyp/pgtable.c | 38 +++++++++++++++++++++-------
>>> arch/arm64/kvm/mmu.c | 15 +++++++++++
>>> arch/arm64/kvm/nested.c | 4 +--
>>> arch/arm64/kvm/pkvm.c | 2 ++
>>> arch/arm64/kvm/sys_regs.c | 18 ++++++-------
>>> 8 files changed, 75 insertions(+), 21 deletions(-)
>>>
>>
>>
>> Hi Marc,
>>
>> Thanks for putting this series together. I reviewed the four patches
>> and revisited the traces from my earlier Marc-only tests.
>>
>> I have a correctness concern about child page-table reclamation.
>>
>
> You? Or your AI model? I'd really expect you to explain *your*
> perception of the problem rather than dumping the result of your AI in
> an email.
Hi Marc,
You're right. I used AI assistance (GPT-6 Astra) for the analysis.
Let me clarify the technical points.
>
>> 1. Child page-table reclamation before the final TLBI
>>
>> In patch 1, SKIP_S2_TLBI suppresses invalidation for both leaf and table
>> descriptors, while stage2_unmap_walker() still immediately releases
>> empty child tables.
>
> What is a "child" table?
I meant the next-level shadow S2 table obtained here in
stage2_unmap_walker():
if (kvm_pte_table(ctx->old, ctx->level)) {
childp = kvm_pte_follow(ctx->old, mm_ops);
For example, if ctx->old is an L2 table descriptor, childp points
to the L3 table that it describes. This is a host-allocated shadow
page-table page, not a guest data page.
>
>>
>> For a child table with page_count(childp) == 1, the sequence is:
>>
>> stage2_unmap_walker()
>> stage2_unmap_put_pte()
>> clear the parent table descriptor
>> skip its TLBI
>> mm_ops->put_page(childp)
>> kvm_s2_put_page()
>> put_page() /* drop the child's last reference */
>>
>> ... process the remaining address ranges ...
>>
>> __kvm_tlb_flush_vmid() /* final invalidation in patch 2 */
>>
>> This path does not use the free_unlinked_table()/call_rcu() deferred
>> reclamation mechanism. The existing deferred-range-TLBI path still
>> invalidates table descriptors immediately; the new flag skips that
>> invalidation too.
>
> And? What is the actual problem here?
The concern is that this page becomes available for reuse before
the corresponding invalidation completes.
With page_count(childp) == 1, the walker clears the parent descriptor
through stage2_unmap_put_pte(), then releases the next-level table
with mm_ops->put_page(childp). SKIP_S2_TLBI suppresses the table-
descriptor TLBI that previously happened before this release.
The final VMID-wide TLBI happens after the full walk.
If hardware can still walk through that page using old intermediate
translation state, it could interpret contents written by a new
owner as S2 descriptors. That is the failure scenario I intended
to describe.
>
>>
>> Patch 4 provides a caller operating on active shadow MMUs:
>> kvm_s2_mmu_iterate_by_vmid() holds mmu_lock for write and visits valid
>> matching shadow MMUs, but does not require refcnt == 0 or wait for
>> other vCPUs using the MMU to exit.
>
> Of course it doesn't, since this is simply emulating an instruction
> local to that vcpu. How would the actual HW "wait" for another CPU to
> stop using a set of translation?
Agreed. I should not have presented the absence of a vCPU-stop
mechanism as a problem. Stopping another vCPU and waiting for
invalidation to complete are different things.
My concern is the lifetime of the host shadow page-table memory:
whether it can be reclaimed before the final TLBI completes.
>
>>
>> Another vCPU can therefore still use that shadow S2. The write lock
>> excludes software page-table updates, not hardware table walks.
>> The following interleaving is allowed:
>>
>> vCPU B / hardware walker vCPU A
>> ------------------------ ----------------------------
>> Holds an old reference to T
>> Clears parent, skips TLBI
>> Drops T's last reference
>> T is reused by the allocator
>> Accesses T via the old reference
>> Performs final VMID-wide TLBI
>>
>> The final flush barriers cannot retroactively protect a table that
>> has already been freed and reused.
>
> T is a shadow page-table page on the host. How can vcpu B hold a
> reference on that page? It isn't even in the same address space.
Sorry for the misleading. I meant a hardware table walk
on the PE running B going through T, not a software reference held
by B or a guest mapping of T.
>
> Now, I can see that B could have a VA that *translate through* T, and
> that's rather annoying.
>
> I'll have a think.
>
> M.
Yes, translating through T is what I meant.
Would retaining the table-descriptor TLBI be a reasonable minimal
fix? That would preserve invalidation before releasing the next-level
table, while still avoiding the per-range flushes for empty chunks.
Thanks for looking into it.
Shuai
More information about the linux-arm-kernel
mailing list