KVM: arm64: VNCR TLB invalidation fixes
Subsystems (4)
Marc Zyngier
maintainerv2 cover KVM: arm64: VNCR TLB invalidation fixes
Prompted by a patch [0] from Hyunwoo Kim which was addressing a pretty annoying corner case, I spent some time looking at what was wrong in our VNCR TLBI code. And there was a few things to say about it...
- The TLB tracking to get into the slow path is wrong. It tracks what is mapped, but not the established TLBs. Kind of annoying.
- Handling IPA invalidation when the TLB was established with the S1 MMU disabled is failing for a number of reasons. This is the bug that Hyunwoo Kim found, but I decided to address it at its root rather than just fixing the symptoms.
- Deciding to run with a VNCR TLB doesn't take the state of SCTLR_EL2.M into account. Yes, it is a special sort of sport to run a hypervisor with its MMU disabled...
- TLB invalidation by VA targeting the last page/block of TTBR1_EL2 is dropped on the floor, because I cannot count. This was amusing. Not.
Additionally, Sashiko kindly provided some extra horrors to look at:
- A missing sign extension for range invalidation results in the wrong VA range being considered.
- TLBI doesn't participate in the general MMU invalidation retry machinery, meaning that VNCR faults and TLBIs race in an uncontrolled way.
- TLBI and vcpu_put() can race badly, leading to a TOCTOU pattern which results in either a BUG_ON() or a call to vncr_fixmap(-1).
I have fixes for each of these issues, all stable candidates.
On top of that, I have a patch reintroducing the tracking that the first patch removes, this time in a way that is actually functional. Or at least I think it is...
I'd like to thank Hyunwoo Kim for their initial patch and for providing a reproducer that helped me finding these issues by running it at multiple levels of nesting.
It is now too late for 7.2, so let's try to make this 7.3 material (hence the early repost).
* From v1 [1]
- Added sign extension to range invalidation (Sashiko)
- Use mmu_invalidate_seq to resolve fault vs TLBI races (Sashiko)
- Speculatively bump the TLB refcount on fault to make sure the TLBI takes the slow path when we are going for a S1 walk
- Clarify the check for the TLB matching for the S1 MMU state
- Extra sanitisation added on the S2 invalidation path
- Applied RBs from Yao Yuan to the patches that didn't change, with thanks.
[0] https://lore.kernel.org/r/ameGoxbn2wzBq2kL@v4bel
[1] https://lore.kernel.org/r/[email protected]
Marc Zyngier (8):
KVM: arm64: Remove VM-wide VNCR mapping counter
KVM: arm64: Handle negative S1 walk levels in VNCR TLB size evaluation
KVM: arm64: Consider SCTLR_EL2.M when mapping the L1 VNCR page
KVM: arm64: Correctly handle end of VA space TLBI invalidation
KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR
unmapping
KVM: arm64: Sign-extend VA for range-based TLBI invalidation
KVM: arm64: Make VNCR invalidation participate in MMU invalidation
retry
KVM: arm64: Add VNCR TLB tracking again
arch/arm64/include/asm/kvm_host.h | 4 +- arch/arm64/include/asm/kvm_nested.h | 14 +++ arch/arm64/kvm/at.c | 2 - arch/arm64/kvm/hyp/vhe/switch.c | 10 +- arch/arm64/kvm/nested.c | 173 +++++++++++++++++++--------- arch/arm64/kvm/sys_regs.c | 11 ++ 6 files changed, 155 insertions(+), 59 deletions(-)
Signature · 1 line
2.47.3
Marc Zyngier
maintainerPATCH v2 1/8 KVM: arm64: Remove VM-wide VNCR mapping counter
The global VNCR mapping counter is used to decide whether an L1 provided VNCR page is mapped in L0 on any CPU at the point of dealing with a TLB invalidation. It is incremented when a mapping is made in the fixmap, and decremented when unmapped.
As it turns out, this tracking has several flaws:
- we are trying to invalidate TLBs, and the mapping is only an opportunistic consequence of the TLB. Checking this counter to decide whether a TLB needs to be invalidated may result in missed invalidations.
- an L1 vcpu invalidating its own TLB (a very likely case) will not succeed in invalidating the VNCR pseudo TLB because that page is not mapped in L0 at this stage.
Given that this tracking fails at delivering the minimum guarantees that are required and is only a performance optimisation, remove it completely.
--- arch/arm64/include/asm/kvm_host.h | 3 --- arch/arm64/kvm/hyp/vhe/switch.c | 3 +-- arch/arm64/kvm/nested.c | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-)
arch/arm64/include/asm/kvm_host.h +0 -3
@@ -411,9 +411,6 @@ struct kvm_arch {/* Masks for VNCR-backed and general EL2 sysregs */ struct kvm_sysreg_masks *sysreg_masks; -/* Count the number of VNCR_EL2 currently mapped */ -atomic_t vncr_map_count; -/* * For an untrusted host VM, 'pkvm.handle' is used to lookup * the associated pKVM instance in the hypervisor.
arch/arm64/kvm/hyp/vhe/switch.c +1 -2
@@ -427,8 +427,7 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)* If we have to check for any VNCR mapping being invalidated, * go back to the slow path for further processing. */ -if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && -atomic_read (&vcpu->kvm->arch.vncr_map_count)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu))return false;__kvm_skip_instr (vcpu);
arch/arm64/kvm/nested.c +0 -3
@@ -48,7 +48,6 @@ void kvm_init_nested(struct kvm *kvm) { kvm->arch.nested_mmus =NULL ; kvm->arch.nested_mmus_size =0 ; -atomic_set (&kvm->arch.vncr_map_count,0 ); }static int init_nested_s2_mmu (struct kvm *kvm,struct kvm_s2_mmu *mmu)
@@ -890,7 +889,6 @@ static void this_cpu_reset_vncr_fixmap(struct kvm_vcpu *vcpu)clear_fixmap (vncr_fixmap (vcpu->arch.vncr_tlb->cpu)); vcpu->arch.vncr_tlb->cpu =-1 ;host_data_clear_flag (L1_VNCR_MAPPED ); -atomic_dec (&vcpu->kvm->arch.vncr_map_count); }void kvm_vcpu_put_hw_mmu (struct kvm_vcpu *vcpu)
@@ -1592,7 +1590,6 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)if (pgprot_val (prot) !=pgprot_val (PAGE_NONE )) {__set_fixmap (vncr_fixmap (vt->cpu), vt->hpa, prot);host_data_set_flag (L1_VNCR_MAPPED ); -atomic_inc (&vcpu->kvm->arch.vncr_map_count); } }
Signature · 1 line
2.47.3
[email protected]
AI reviewRe: [PATCH v2 1/8] KVM: arm64: Remove VM-wide VNCR mapping counter
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] S1E2 TLBI instructions fail to invalidate the VNCR pseudo-TLB when TGE=0 or E2H=0.
- [High] Hardcoded TR_EL20 in kvm_translate_vncr misinterprets TCR_EL2 for non-VHE guests. --
--- Patch [1]: [PATCH v2 1/8] KVM: arm64: Remove VM-wide VNCR mapping counter --- commit 9a09630623c9341a08c8ecea540015307bde098c
Author: Marc Zyngier <[email protected]>
Subject: KVM: arm64: Remove VM-wide VNCR mapping counter
The global VNCR mapping counter is used to decide whether an L1 provided VNCR page is mapped in L0 on any CPU at the point of dealing with a TLB invalidation. It is incremented when a mapping is made in the fixmap, and decremented when unmapped.
arch/arm64/kvm/hyp/vhe/switch.c static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code) · line 430
Quoted 11 lines
arch/arm64/kvm/hyp/vhe/switch.c +1
-2@@ -427,8 +427,7 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)* If we have to check for any VNCR mapping being invalidated, * go back to the slow path for further processing. */ -if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && -atomic_read (&vcpu->kvm->arch.vncr_map_count)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu))
[Severity: High]
This is a pre-existing issue, but does this condition incorrectly limit the slow-path fallback to only E2H=1 and TGE=1 contexts?
If a non-VHE guest (E2H=0) or a VHE guest running a VM (TGE=0) executes an S1E2 TLBI instruction (e.g. TLBI VAE2), the fast-path handler kvm_hyp_handle_tlbi_el2() performs the hardware TLBI but will evaluate the above condition as false.
It will then execute __kvm_skip_instr() and return true, bypassing the EL1 slow-path handler entirely.
Since the EL1 handler is exclusively responsible for invalidating the software VNCR pseudo-TLB, won't this leave stale mappings in the pseudo-TLB when a guest hypervisor invalidates its EL2 mappings?
arch/arm64/kvm/nested.c
Quoted 8 lines
arch/arm64/kvm/hyp/vhe/switch.c +0
-0return false;__kvm_skip_instr (vcpu);arch/arm64/kvm/nested.c +0
-0
[ ... ]
[Severity: High]
This is also a pre-existing issue in this file within kvm_translate_vncr(), where the TR_EL20 translation regime is hardcoded:
kvm_translate_vncr() {
...
vt->wi = (struct s1_walk_info) {
.regime = TR_EL20,
.as_el0 = false,
.pan = false,
};
...
}Could this misinterpret TCR_EL2 for non-VHE guests?
When setup_s1_walk() parses TCR_EL2 for the TR_EL20 regime, it assumes the E2H=1 register layout (reading TBI from bit 37 and PS from bits 34:32).
However, an untrusted nested guest hypervisor with E2H=0 programs TCR_EL2 using the standard E2H=0 layout, where TBI is at bit 20 and PS is at bits 18:16.
Won't this cause KVM to use garbage bits to configure the walk, leading to spurious translation faults or using the wrong physical address size?
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
Marc Zyngier
maintainerRe: [PATCH v2 1/8] KVM: arm64: Remove VM-wide VNCR mapping counter
Quoted 34 lines · On Thu, 06 Aug 2026 10:35:41 +0100, [email protected] wrote:
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] S1E2 TLBI instructions fail to invalidate the VNCR pseudo-TLB when TGE=0 or E2H=0.
- [High] HardcodedTR_EL20inkvm_translate_vncrmisinterpretsTCR_EL2for non-VHE guests. ----- Patch [1]: [PATCH v2 1/8] KVM: arm64: Remove VM-wide VNCR mapping counter --- commit
9a09630623c9341a08c8ecea540015307bde098c
Author: Marc Zyngier <[email protected]>
Subject: KVM: arm64: Remove VM-wide VNCR mapping counterThe global VNCR mapping counter is used to decide whether an L1 provided VNCR page is mapped in L0 on any CPU at the point of dealing with a TLB invalidation. It is incremented when a mapping is made in the fixmap, and decremented when unmapped.
arch/arm64/kvm/hyp/vhe/switch.c static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code) · line 430
Quoted 11 lines
arch/arm64/kvm/hyp/vhe/switch.c +1
-2@@ -427,8 +427,7 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)* If we have to check for any VNCR mapping being invalidated, * go back to the slow path for further processing. */ -if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && -atomic_read (&vcpu->kvm->arch.vncr_map_count)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu))[Severity: High]
This is a pre-existing issue, but does this condition incorrectly limit the slow-path fallback to only E2H=1 and TGE=1 contexts?
Quoted 4 lines
If a non-VHE guest (E2H=0) or a VHE guest running a VM (TGE=0) executes an S1E2 TLBI instruction (e.g. TLBI VAE2), the fast-path handler
kvm_hyp_handle_tlbi_el2()performs the hardware TLBI but will evaluate the above condition as false.
"Stuck record".
There is no such thing as NV together with a non-VHE guest, so no VNCR, and no need for taking the invalidation on the slow path.
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH v2 1/8] KVM: arm64: Remove VM-wide VNCR mapping counter
In reply to Marc ZyngierBear with me being verbose here, as this is both nascent review + learning :)
Quoted 11 lines · Marc Zyngier
The global VNCR mapping counter is used to decide whether an L1 provided VNCR page is mapped in L0 on any CPU at the point of dealing with a TLB invalidation. It is incremented when a mapping is made in the fixmap, and decremented when unmapped.
As it turns out, this tracking has several flaws:
- we are trying to invalidate TLBs, and the mapping is only an opportunistic consequence of the TLB. Checking this counter to decide whether a TLB needs to be invalidated may result in missed invalidations.
Is it largely the self-invalidation mentioned below or are there other cases?
Quoted 4 lines
- an L1 vcpu invalidating its own TLB (a very likely case) will not succeed in invalidating the VNCR pseudo TLB because that page is not mapped in L0 at this stage.
Ahh yes this is pretty compelling then!
Quoted 8 lines
Given that this tracking fails at delivering the minimum guarantees that are required and is only a performance optimisation, remove it completely.
Fixes 4ffa72ad8f37e ("KVM: arm64: nv: Add S1 TLB invalidation primitive for VNCR_EL2")Reviewed-by Yuan Yao <[email protected]>Signed-off-by Marc Zyngier <[email protected]>
The change LGTM, it neatly removes the described mechanism which is well evidenced.
Comments below that are largely me talking out loud as I learn things :)
Quoted 26 lines
--- arch/arm64/include/asm/kvm_host.h | 3 --- arch/arm64/kvm/hyp/vhe/switch.c | 3 +-- arch/arm64/kvm/nested.c | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-)arch/arm64/include/asm/kvm_host.h +0
-3@@ -411,9 +411,6 @@ struct kvm_arch {/* Masks for VNCR-backed and general EL2 sysregs */ struct kvm_sysreg_masks *sysreg_masks; -/* Count the number of VNCR_EL2 currently mapped */ -atomic_t vncr_map_count; -/* * For an untrusted host VM, 'pkvm.handle' is used to lookup * the associated pKVM instance in the hypervisor. arch/arm64/kvm/hyp/vhe/switch.c +0
-0
arch/arm64/kvm/hyp/vhe/switch.c static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code) · line 431
Quoted 7 lines
arch/arm64/kvm/hyp/vhe/switch.c +1
-2@@ -427,8 +427,7 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)* If we have to check for any VNCR mapping being invalidated, * go back to the slow path for further processing. */ -if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && -atomic_read (&vcpu->kvm->arch.vncr_map_count)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu))return false;
So this seems to be the crux of it - seems to be 'is there any possibility that we will need to check for VNCR mappings being invalidated?'
Checks:
* vcpu_el2_e2h_is_set() - is the guest host kernel (?)'s hcr_el2.e2h enabled? From what I gather hcr_el2.e2h is what allows sysreg_EL1 -> sysreg_EL2 for the host kernel to allow unmodified kernels to run in EL2.
IOW - is the guest host kernel VHE?
* vcpu_el2_tge_is_set() - Similarly tests for the hcr_el2.tge bit - and this seems to be is 'EL1 -> EL2 redirection on?' - IOW - is this a kernel running in EL2?
Actually I see in is_hyp_ctxt():
* We are in a hypervisor context if the vcpu mode is EL2 or
* E2H and TGE bits are set. The latter means we are in the user space
* of the VHE kernel. ARMv8.1 ARM describes this as 'InHost'
So I _think_ the combination of the two is checking to see if you're the L0 kernel that _could_ send TLBi's that need to be handled?
Previously it seemed the logic was 'if there are no VNCR mappings present then we can optimise by short-circuiting the rest of the processing in kvm_hyp_handle_sysreg_vhe()'.
It seems that the hardware TLBi has been processed by now so it's actually more like - there's still work to be done maintaining the software TLB and that's done elsewhere.
As:
static bool kvm_hyp_handle_sysreg_vhe(struct kvm_vcpu *vcpu, u64 *exit_code)
{
if (kvm_hyp_handle_tlbi_el2(vcpu, exit_code)) <- return ->true
return true;
if (kvm_hyp_handle_timer(vcpu, exit_code)) <- this was a TLBi ->false
return true;
if (kvm_hyp_handle_cpacr_el1(vcpu, exit_code)) <- this was a TLBi ->false
return true;
if (kvm_hyp_handle_zcr_el2(vcpu, exit_code)) <- this was a TLBi ->false
return true;
return kvm_hyp_handle_sysreg(vcpu, exit_code); <- this was a TLBi ->false
}And
static const exit_handler_fn hyp_exit_handlers[] = {
...
[ESR_ELx_EC_SYS64] = kvm_hyp_handle_sysreg_vhe,
...
};And:
static inline bool kvm_hyp_handle_exit(struct kvm_vcpu *vcpu, u64 *exit_code,
const exit_handler_fn *handlers)
{
exit_handler_fn fn = handlers[kvm_vcpu_trap_get_class(vcpu)]; <- kvm_hyp_handle_sysreg_vhe()
if (fn)
return fn(vcpu, exit_code);
return false;
}Annnd:
/*
* Return true when we were able to fixup the guest exit and should return to
* the guest, false when we should restore the host state and return to the
* main run loop.
*/
static inline bool __fixup_guest_exit(struct kvm_vcpu *vcpu, u64 *exit_code,
const exit_handler_fn *handlers)
{
...
/* Check if there's an exit handler and allow it to handle the exit. */
if (kvm_hyp_handle_exit(vcpu, exit_code, handlers))
goto guest;
exit:
/* Return to the host kernel and handle the exit */
return false;
...
}Finally in __kvm_vcpu_run_vhe():
do {
/* Jump in the fire! */(Good track ;)
exit_code = __guest_enter(vcpu); /* And we're baaack! */ } while (fixup_guest_exit(vcpu, &exit_code));
(With fixup_guest_exit() ultimately calling __fixup_guest_exit().)
And fixup_guest_exit() will return false, meaning the guest isn't re-entered and instead you go back to the full fat slow path:
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
{
...
ret = kvm_arm_vcpu_enter_exit(vcpu); <-- does all the above just returned.
...
ret = handle_exit(vcpu, ret);
}Then there's some more stuff in this fuller fat handle_exit() path:
int handle_exit(struct kvm_vcpu *vcpu, int exception_index)
{
...
switch (exception_index) {
...
case ARM_EXCEPTION_TRAP:
return handle_trap_exceptions(vcpu);
...
}
}Which then calls kvm_get_exit_handler() which ultimately gets handle_tlbi_el2() and calls kvm_handle_s1e2_tlbi() in turn and then invalidate_vncr_va():
static void invalidate_vncr_va(struct kvm *kvm,
struct s1e2_tlbi_scope *scope)
{
...
kvm_for_each_vncr_tlb(i, vcpu, vt, kvm) {
...
invalidate_vncr(vt);
}
}And:
static void invalidate_vncr(struct vncr_tlb *vt)
{
vt->valid = false;
if (vt->cpu != -1)
clear_fixmap(vncr_fixmap(vt->cpu));
}Where you are ultimately clearing the fixmap and setting the vncr_tlb->valid to false.
I think this is all vaguely sane :)
Quoted 23 lines
arch/arm64/kvm/hyp/vhe/switch.c +0
-0__kvm_skip_instr (vcpu);arch/arm64/kvm/nested.c +0
-2@@ -48,7 +48,6 @@ void kvm_init_nested(struct kvm *kvm) { kvm->arch.nested_mmus =NULL ; kvm->arch.nested_mmus_size =0 ; -atomic_set (&kvm->arch.vncr_map_count,0 ); }static int init_nested_s2_mmu (struct kvm *kvm,struct kvm_s2_mmu *mmu)@@ -890,7 +889,6 @@ static void this_cpu_reset_vncr_fixmap(struct kvm_vcpu *vcpu)clear_fixmap (vncr_fixmap (vcpu->arch.vncr_tlb->cpu)); vcpu->arch.vncr_tlb->cpu =-1 ;host_data_clear_flag (L1_VNCR_MAPPED ); -atomic_dec (&vcpu->kvm->arch.vncr_map_count); }void kvm_vcpu_put_hw_mmu (struct kvm_vcpu *vcpu)
arch/arm64/kvm/nested.c static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu) · line 1594
Quoted 6 lines
arch/arm64/kvm/nested.c +0
-1@@ -1592,7 +1590,6 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)if (pgprot_val (prot) !=pgprot_val (PAGE_NONE )) {__set_fixmap (vncr_fixmap (vt->cpu), vt->hpa, prot);host_data_set_flag (L1_VNCR_MAPPED ); -atomic_inc (&vcpu->kvm->arch.vncr_map_count); } }
And it looks like you got all the places that manipulated this :)
Quoted 4 lines
arch/arm64/kvm/nested.c +0
-1--2.47.3
--
Cheers, Lorenzo
Marc Zyngier
maintainerRe: [PATCH v2 1/8] KVM: arm64: Remove VM-wide VNCR mapping counter
Quoted 3 lines · Lorenzo Stoakes (ARM)
Bear with me being verbose here, as this is both nascent review + learning :)
No worries.
Quoted 7 lines · Marc Zyngier
Quoted 5 lines
The global VNCR mapping counter is used to decide whether an L1 provided VNCR page is mapped in L0 on any CPU at the point of dealing with a TLB invalidation. It is incremented when a mapping is made in the fixmap, and decremented when unmapped.
Quoted 8 lines
Quoted 6 lines
As it turns out, this tracking has several flaws:
- we are trying to invalidate TLBs, and the mapping is only an opportunistic consequence of the TLB. Checking this counter to decide whether a TLB needs to be invalidated may result in missed invalidations.
Is it largely the self-invalidation mentioned below or are there other cases?
No. The self-invalidation is only an additional consequence outlining that even the most basic requirements cannot be honoured.
The thing to realise is that is that we are dealing with two separate "objects" when it comes to VNCR:
- a SW TLB, which represent the guest VA to guest IPA to host PA translations. This is the vncr_tlb structure, populated as we walk the S1/S2 page tables. This structure's lifetime is controlled by TLB invalidations from the guest, MMU notifiers from the host, and natural eviction (there is only one such structure per vcpu).
- a shadow page table that implements the translation at *runtime*, as described by vncr_tlb. This is the per-CPU fixmap mapping. It's lifetime is at most a vcpu_load/vcpu_put cycle, but it can also be torn down by TLBI and notifiers.
vncr_map_count only tracks the latter, not the former. Which means that if the mapping is not live on a CPU at the point of TLBI on *any* CPU, nothing will happen. That's a blatant violation of the architecture, and it could lead to memory corruption in the guest.
The obvious fix is in patch 8, tracking the TLBs rather than the mappings.
Quoted 52 lines
Quoted 5 lines
- an L1 vcpu invalidating its own TLB (a very likely case) will not succeed in invalidating the VNCR pseudo TLB because that page is not mapped in L0 at this stage.
Ahh yes this is pretty compelling then!
Quoted 8 lines
Given that this tracking fails at delivering the minimum guarantees that are required and is only a performance optimisation, remove it completely.
Fixes 4ffa72ad8f37e ("KVM: arm64: nv: Add S1 TLB invalidation primitive for VNCR_EL2")Reviewed-by Yuan Yao <[email protected]>Signed-off-by Marc Zyngier <[email protected]>The change LGTM, it neatly removes the described mechanism which is well evidenced.
Comments below that are largely me talking out loud as I learn things :)
Quoted 28 lines
--- arch/arm64/include/asm/kvm_host.h | 3 --- arch/arm64/kvm/hyp/vhe/switch.c | 3 +-- arch/arm64/kvm/nested.c | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-)arch/arm64/include/asm/kvm_host.h +0
-3@@ -411,9 +411,6 @@ struct kvm_arch {/* Masks for VNCR-backed and general EL2 sysregs */ struct kvm_sysreg_masks *sysreg_masks; -/* Count the number of VNCR_EL2 currently mapped */ -atomic_t vncr_map_count; -/* * For an untrusted host VM, 'pkvm.handle' is used to lookup * the associated pKVM instance in the hypervisor. arch/arm64/kvm/hyp/vhe/switch.c +0
-0@@ -427,8 +427,7 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)* If we have to check for any VNCR mapping being invalidated, * go back to the slow path for further processing.
Quoted 8 lines
arch/arm64/kvm/hyp/vhe/switch.c static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code) · line 431
Quoted 5 lines
arch/arm64/kvm/hyp/vhe/switch.c +1
-2*/ -if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && -atomic_read (&vcpu->kvm->arch.vncr_map_count)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu))return false;So this seems to be the crux of it - seems to be 'is there any possibility that we will need to check for VNCR mappings being invalidated?'
Yup. If the guest's state is HCR_EL2.{E2H,TGE}={1,1}, then this is the guest hypervisor invalidating TLBs for itself (and not its guest), and returning 'false' takes us on the slow path where we'll have to look at the individual vcnt_tlb structures to find out if there is any overlap.
Quoted 6 lines
Checks:
*
vcpu_el2_e2h_is_set()- is the guest host kernel (?)'s hcr_el2.e2h enabled? From what I gather hcr_el2.e2h is what allowssysreg_EL1->sysreg_EL2for the host kernel to allow unmodified kernels to run in EL2.
Amongst other things, yes. Please see FEAT_VHE in the ARM ARM for a description of what HCR_EL2.E2H==1 implies.
Quoted 2 lines
IOW - is the guest host kernel VHE?
Note that for a KVM guest, if FEAT_VHE is present, the FEAT_E2H0 is not implemented, which means that E2H is RES1. As an additional restriction, we only expose NV to VHE guests. This greatly simplifies the scope of what we need to support, and conveniently hides a bunch of architecture defects that cannot be otherwise mitigated.
Quoted 4 lines
*
vcpu_el2_tge_is_set()- Similarly tests for the hcr_el2.tge bit - and this seems to be is 'EL1 -> EL2 redirection on?' - IOW - is this a kernel running in EL2?
Not quite. We know for sure this is running at virtual EL2 (we trapped with HCR_EL2.NV set, for a start). In the TLBI case, HCR_EL2.TGE controls whether the behaviour of a TLBI S1E1* instruction targets the host (TGE==1) or the guest (TGE==0).
Quoted 9 lines
Actually I see in
is_hyp_ctxt():* We are in a hypervisor context if the vcpu mode is EL2 or
* E2H and TGE bits are set. The latter means we are in the user space
* of the VHE kernel. ARMv8.1 ARM describes this as 'InHost'So I _think_ the combination of the two is checking to see if you're the L0 kernel that _could_ send TLBi's that need to be handled?
The combination of the two bits indicates: is this a VHE hypervisor invalidating TLBs for itself. If yes, then we need to check the VNCR SW TLBs to complete the job.
Quoted 3 lines
Previously it seemed the logic was 'if there are no VNCR mappings present then we can optimise by short-circuiting the rest of the processing in
kvm_hyp_handle_sysreg_vhe()'.
Exactly.
Quoted 4 lines
It seems that the hardware TLBi has been processed by now so it's actually more like - there's still work to be done maintaining the software TLB and that's done elsewhere.
Yup.
[...]
Quoted 1 line
I think this is all vaguely sane :)
You have been assimilated.
Thanks for reading thus far!
M.
Signature · 1 line
Jazz isn't dead. It just smells funny.
Marc Zyngier
maintainerPATCH v2 2/8 KVM: arm64: Handle negative S1 walk levels in VNCR TLB size evaluation
In reply to Marc ZyngierComputing the effects of a TLB invalidation involves looking at the size of the mapping cached by the TLB. For S1 mappings such as VNCR, this is deducted from the combination of the base granule size and the mapping level.
However, this implies that the S1 MMU is *on*. When the MMU is off, we indicate this with the level being set to a "creative" value of -127 (S1_MMU_DISABLED).
This ends-up being misinterpreted by pgshift_level_to_ttl() as it doesn't handle negative levels at all (the level is immediately cast to a u8 and only the bottom two bits considered), leading to an invalidation size of 0. Not helpful.
Tidy-up pgshift_level_to_ttl() to handle these negative levels, and ttl_to_size() to always return SZ_1G when no valid TTL is present. This allows the removal of open-coded checks for similar situations.
Note that the check for a negative value not explicitely checking for S1_MMU_DISABLED is deliberate, so that actual negative levels introduced with LVA2 and D128 can take the same path if we ever support them.
--- arch/arm64/kvm/nested.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-)
arch/arm64/kvm/nested.c +19 -7
@@ -505,7 +505,7 @@ int kvm_walk_nested_s2(struct kvm_vcpu *vcpu, phys_addr_t gipa,return ret; } -static unsigned int ttl_to_size (u8 ttl) +static unsigned int __ttl_to_size (u8 ttl) {int level = ttl &3 ;int gran = (ttl >>2 ) &3 ;
@@ -561,10 +561,22 @@ static unsigned int ttl_to_size(u8 ttl)return max_size; } -static u8 pgshift_level_to_ttl (u16 shift,u8 level) +static unsigned int ttl_to_size (u8 ttl) +{ +return __ttl_to_size (ttl) ?:SZ_1G ; +} + +static u8 pgshift_level_to_ttl (u16 shift,s8 level) {u8 ttl; +/* +* If we don't have a proper level, fallback to the maximum +* size. +*/ +if (level <0 ) +return 0 ; +switch (shift) {case 12 : ttl =TLBI_TTL_TG_4K ;
@@ -675,7 +687,11 @@ unsigned long compute_tlb_inval_range(struct kvm_s2_mmu *mmu, u64 val) ttl =get_guest_mapping_ttl (mmu, addr); } - max_size =ttl_to_size (ttl); +/* +* Don't use the default 1GB fallback, as we can adapt to the +* max mapping size we allow at S2. +*/ + max_size =__ttl_to_size (ttl);if (!max_size) {/* Compute the maximum extent of the invalidation */
@@ -1124,8 +1140,6 @@ static void compute_s1_tlbi_range(struct kvm_vcpu *vcpu, u32 inst, u64 val,case OP_TLBI_VALE1OSNXS : scope->type =TLBI_VA ; scope->size =ttl_to_size (FIELD_GET (TLBI_TTL_MASK , val)); -if (!scope->size) - scope->size =SZ_1G ; scope->va =tlbi_va_s1_to_va (val) & ~(scope->size -1 ); scope->asid =FIELD_GET (TLBIR_ASID_MASK , val);break ;
@@ -1152,8 +1166,6 @@ static void compute_s1_tlbi_range(struct kvm_vcpu *vcpu, u32 inst, u64 val,case OP_TLBI_VAALE1OSNXS : scope->type =TLBI_VAA ; scope->size =ttl_to_size (FIELD_GET (TLBI_TTL_MASK , val)); -if (!scope->size) - scope->size =SZ_1G ; scope->va =tlbi_va_s1_to_va (val) & ~(scope->size -1 );break ;case OP_TLBI_RVAE2 :
Signature · 1 line
2.47.3
Re: [PATCH v2 2/8] KVM: arm64: Handle negative S1 walk levels in VNCR TLB size evaluation
Quoted 8 lines · Marc Zyngier
Computing the effects of a TLB invalidation involves looking at the size of the mapping cached by the TLB. For S1 mappings such as VNCR, this is deducted from the combination of the base granule size and the mapping level.
However, this implies that the S1 MMU is *on*. When the MMU is off, we indicate this with the level being set to a "creative" value of -127 (
S1_MMU_DISABLED).
:)
Quoted 5 lines
This ends-up being misinterpreted by
pgshift_level_to_ttl()as it doesn't handle negative levels at all (the level is immediately cast to a u8 and only the bottom two bits considered), leading to an invalidation size of 0. Not helpful.
So by two's complement -127 is ~0b01111111 + 1 = 0b10000001 = 129
And:
static u8 pgshift_level_to_ttl(u16 shift, u8 level)
{
u8 ttl;
... shift stuff ...
ttl <<= 2;
ttl |= level & 3;
return tll;
}So ttl |= 1 because of the mask and in ttl_to_size():
static unsigned int ttl_to_size(u8 ttl)
{
int level = ttl & 3;
int gran = (ttl >> 2) & 3;
unsigned int max_size = 0;
switch (gran) {
case TLBI_TTL_TG_4K:
switch (level) {
...
case 1:
max_size = SZ_1G;
break;
...
case TLBI_TTL_TG_16K:
switch (level) {
...
case 1:
break;
...
case TLBI_TTL_TG_64K:
switch (level) {
...
case 1:
/* No 52bit IPA support */
break;
...
}
return max_size;
}So actually if granularity is TLBI_TTL_TG_4K this will return SZ_1G and 0 in the other cases unless I'm getting something wrong here?
This is really more a 'maybe worth mentioning in the commit log to be pedantic' kind of thing :)
IOW you could luck out before with SZ_1G for TLBI_TTL_TG_4K.
Quoted 4 lines
Tidy-up
pgshift_level_to_ttl()to handle these negative levels, andttl_to_size()to always returnSZ_1Gwhen no valid TTL is present. This allows the removal of open-coded checks for similar situations.
I guess SZ_1G is a reasonable default here?
Quoted 2 lines
Note that the check for a negative value not explicitely checking for
NIT: explicitely -> explicitly
Quoted 26 lines
S1_MMU_DISABLEDis deliberate, so that actual negative levels introduced with LVA2 and D128 can take the same path if we ever support them.Fixes 7270cc9157f47 ("KVM: arm64: nv: Handle VNCR_EL2 invalidation from MMU notifiers")Reported-by Hyunwoo Kim <[email protected]>Signed-off-by Marc Zyngier <[email protected]>--- arch/arm64/kvm/nested.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-)arch/arm64/kvm/nested.c +1
-1@@ -505,7 +505,7 @@ int kvm_walk_nested_s2(struct kvm_vcpu *vcpu, phys_addr_t gipa,return ret; } -static unsigned int ttl_to_size (u8 ttl) +static unsigned int __ttl_to_size (u8 ttl) {int level = ttl &3 ;int gran = (ttl >>2 ) &3 ;
arch/arm64/kvm/nested.c static unsigned int ttl_to_size(u8 ttl) · line 567
Quoted 8 lines
arch/arm64/kvm/nested.c +4
-1@@ -561,10 +561,22 @@ static unsigned int ttl_to_size(u8 ttl)return max_size; } -static u8 pgshift_level_to_ttl (u16 shift,u8 level) +static unsigned int ttl_to_size (u8 ttl) +{ +return __ttl_to_size (ttl) ?:SZ_1G ; +}
Might be worth a comment about the default?
Quoted 16 lines
arch/arm64/kvm/nested.c +9
-0+ +static u8 pgshift_level_to_ttl (u16 shift,s8 level) {u8 ttl; +/* +* If we don't have a proper level, fallback to the maximum +* size. +*/ +if (level <0 ) +return 0 ; +switch (shift) {case 12 : ttl =TLBI_TTL_TG_4K ;
arch/arm64/kvm/nested.c unsigned long compute_tlb_inval_range(struct kvm_s2_mmu *mmu, u64 val) · line 693
Quoted 8 lines
arch/arm64/kvm/nested.c +4
-1@@ -675,7 +687,11 @@ unsigned long compute_tlb_inval_range(struct kvm_s2_mmu *mmu, u64 val) ttl =get_guest_mapping_ttl (mmu, addr); } - max_size =ttl_to_size (ttl); +/* +* Don't use the default 1GB fallback, as we can adapt to the +* max mapping size we allow at S2. +*/
Being a bit pedantic here but I wonder if simply just to say 'Adapt to the max mapping size allowed at S2' as the fallback is inferred?
Quoted 14 lines
arch/arm64/kvm/nested.c +1
-2+ max_size =__ttl_to_size (ttl);if (!max_size) {/* Compute the maximum extent of the invalidation */ @@ -1124,8 +1140,6 @@ static void compute_s1_tlbi_range(struct kvm_vcpu *vcpu, u32 inst, u64 val,case OP_TLBI_VALE1OSNXS : scope->type =TLBI_VA ; scope->size =ttl_to_size (FIELD_GET (TLBI_TTL_MASK , val)); -if (!scope->size) - scope->size =SZ_1G ; scope->va =tlbi_va_s1_to_va (val) & ~(scope->size -1 ); scope->asid =FIELD_GET (TLBIR_ASID_MASK , val);break ;
arch/arm64/kvm/nested.c static void compute_s1_tlbi_range(struct kvm_vcpu *vcpu, u32 inst, u64 val, · line 1156
Quoted 5 lines
arch/arm64/kvm/nested.c +0
-2@@ -1152,8 +1166,6 @@ static void compute_s1_tlbi_range(struct kvm_vcpu *vcpu, u32 inst, u64 val,case OP_TLBI_VAALE1OSNXS : scope->type =TLBI_VAA ; scope->size =ttl_to_size (FIELD_GET (TLBI_TTL_MASK , val)); -if (!scope->size) - scope->size =SZ_1G ;
Nice that you can eliminate this and the one above!
Quoted 6 lines
arch/arm64/kvm/nested.c +0
-1scope->va =tlbi_va_s1_to_va (val) & ~(scope->size -1 );break ;case OP_TLBI_RVAE2 : --2.47.3
--
Cheers, Lorenzo
Marc Zyngier
maintainerRe: [PATCH v2 2/8] KVM: arm64: Handle negative S1 walk levels in VNCR TLB size evaluation
On Fri, 07 Aug 2026 18:12:18 +0100,
"Lorenzo Stoakes (ARM)" <[email protected]> wrote:
[...]
Quoted 7 lines
So actually if granularity is
TLBI_TTL_TG_4Kthis will returnSZ_1Gand 0 in the other cases unless I'm getting something wrong here?This is really more a 'maybe worth mentioning in the commit log to be pedantic' kind of thing :)
IOW you could luck out before with
SZ_1GforTLBI_TTL_TG_4K.
Yes, and that's what happens in most cases. But as Huynwoo Kim's reports, 16k and 64k lead to something really bad. And even 4k works by pure luck, so this is all fsck'd in my book.
Quoted 7 lines
Quoted 5 lines
Tidy-up
pgshift_level_to_ttl()to handle these negative levels, andttl_to_size()to always returnSZ_1Gwhen no valid TTL is present. This allows the removal of open-coded checks for similar situations.I guess
SZ_1Gis a reasonable default here?
It is more than a reasonable default. It is the maximum block size that can architecturally be mapped in the absence of FEAT_LPA*. In this situation, 16k pages imply a 32M max block mapping, and 64k implies 512M. 4K pages, by virtue of allowing a level-1 block mapping result in a 1G size.
Quoted 35 lines
Quoted 3 lines
Note that the check for a negative value not explicitely checking for
NIT: explicitely -> explicitly
Quoted 29 lines
S1_MMU_DISABLEDis deliberate, so that actual negative levels introduced with LVA2 and D128 can take the same path if we ever support them.Fixes 7270cc9157f47 ("KVM: arm64: nv: Handle VNCR_EL2 invalidation from MMU notifiers")Reported-by Hyunwoo Kim <[email protected]>Signed-off-by Marc Zyngier <[email protected]>--- arch/arm64/kvm/nested.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-)arch/arm64/kvm/nested.c +1
-1@@ -505,7 +505,7 @@ int kvm_walk_nested_s2(struct kvm_vcpu *vcpu, phys_addr_t gipa,return ret; } -static unsigned int ttl_to_size (u8 ttl) +static unsigned int __ttl_to_size (u8 ttl) {int level = ttl &3 ;int gran = (ttl >>2 ) &3 ;@@ -561,10 +561,22 @@ static unsigned int ttl_to_size(u8 ttl)return max_size; }
Quoted 7 lines
arch/arm64/kvm/nested.c static unsigned int ttl_to_size(u8 ttl) · line 567
Quoted 5 lines
arch/arm64/kvm/nested.c +4
-1-static u8 pgshift_level_to_ttl (u16 shift,u8 level) +static unsigned int ttl_to_size (u8 ttl) +{ +return __ttl_to_size (ttl) ?:SZ_1G ; +}Might be worth a comment about the default?
It's the architecture. I'm trying hard not to make KVM a running commentary of the ARM ARM ;-).
Quoted 20 lines
Quoted 20 lines
arch/arm64/kvm/nested.c +9
-0+ +static u8 pgshift_level_to_ttl (u16 shift,s8 level) {u8 ttl; +/* +* If we don't have a proper level, fallback to the maximum +* size. +*/ +if (level <0 ) +return 0 ; +switch (shift) {case 12 : ttl =TLBI_TTL_TG_4K ;@@ -675,7 +687,11 @@ unsigned long compute_tlb_inval_range(struct kvm_s2_mmu *mmu, u64 val) ttl =get_guest_mapping_ttl (mmu, addr); }
Quoted 8 lines
arch/arm64/kvm/nested.c unsigned long compute_tlb_inval_range(struct kvm_s2_mmu *mmu, u64 val) · line 693
Quoted 5 lines
arch/arm64/kvm/nested.c +4
-1- max_size =ttl_to_size (ttl); +/* +* Don't use the default 1GB fallback, as we can adapt to the +* max mapping size we allow at S2. +*/ Being a bit pedantic here but I wonder if simply just to say 'Adapt to the max mapping size allowed at S2' as the fallback is inferred?
Sure.
Thanks,
M.
Signature · 1 line
Jazz isn't dead. It just smells funny.
Marc Zyngier
maintainerPATCH v2 3/8 KVM: arm64: Consider SCTLR_EL2.M when mapping the L1 VNCR page
In reply to Marc ZyngierWe record a VNCR TLB even when SCTLR_EL2.M is 0 in order to make our life easier. But this is not something that the architecture anticipate.
As a consequence, a hypervisor is free to set VNCR_EL2 to some PA when SCTLR_EL2.M==0, use it to run a guest which indirectly accesses the VNCR page, then eventually set SCTLR_EL2.M==1 with the same VA. Yes, this is odd, but apparently legal.
A common trick in HW is to invalidate the TLBs on SCTLR_ELx.M being flipped. But doing this is a not a good idea for us (we'd need to trap SCTLR accesses), and wouldn't scale as we nest deeper.
Instead, use the fact that the S1 MMU being off at the point of translation is cached in our TLB, and if it doesn't match the current MMU state, leave the VNCR unmapped.
--- arch/arm64/include/asm/kvm_nested.h | 7 +++++++ arch/arm64/kvm/at.c | 2 -- arch/arm64/kvm/nested.c | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-)
arch/arm64/include/asm/kvm_nested.h +7 -0
@@ -388,6 +388,8 @@ struct s1_walk_result {bool failed; }; +#define S1_MMU_DISABLED (-127) +static inline void fail_s1_walk (struct s1_walk_result *wr,u8 fst,bool s1ptw) { wr->fst = fst;
@@ -396,6 +398,11 @@ static inline void fail_s1_walk(struct s1_walk_result *wr, u8 fst, bool s1ptw) wr->failed = true; } +static inline bool s1_walk_translated (struct s1_walk_result *wr) +{ +return wr->level !=S1_MMU_DISABLED ; +} +int __kvm_translate_va (struct kvm_vcpu *vcpu,struct s1_walk_info *wi,struct s1_walk_result *wr,u64 va); int __kvm_find_s1_desc_level(struct kvm_vcpu *vcpu, u64 va, u64 ipa,
arch/arm64/kvm/at.c +0 -2
@@ -11,8 +11,6 @@#include <asm/kvm_mmu.h> #include <asm/lsui.h> -#define S1_MMU_DISABLED (-127) -static int get_ia_size (struct s1_walk_info *wi) {return 64 - wi->txsz;
arch/arm64/kvm/nested.c +4 -0
@@ -1578,6 +1578,10 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)if (!vt->valid)return ; +/* We cache the MMU state in the TLB. Check that it matches. */ +if (!!(vcpu_read_sys_reg (vcpu,SCTLR_EL2 ) & SCTLR_ELx_M) !=s1_walk_translated (&vt->wr)) +return ; +if (read_vncr_el2 (vcpu) != vt->gva)return ;
Signature · 1 line
2.47.3
[email protected]
AI reviewRe: [PATCH v2 3/8] KVM: arm64: Consider SCTLR_EL2.M when mapping the L1 VNCR page
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Acquiring mmu_lock while preemption is disabled causes a 'sleeping in atomic context' BUG on PREEMPT_RT kernels. --
--- Patch [3]: [PATCH v2 3/8] KVM: arm64: Consider SCTLR_EL2.M when mapping the L1 VNCR page --- commit 20af1a04bc308d515b8afd3fe9909a21ff7706cb
Author: Marc Zyngier <[email protected]>
KVM: arm64: Consider SCTLR_EL2.M when mapping the L1 VNCR page
We record a VNCR TLB even when SCTLR_EL2.M is 0 in order to make our life easier. But this is not something that the architecture anticipate.
[ ... ]
arch/arm64/kvm/nested.c static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
Quoted 5 lines
arch/arm64/kvm/nested.c +0
-0@@ -1579,6 +1579,10 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
[Severity: High]
This is a pre-existing issue, but looking at the beginning of kvm_map_l1_vncr():
static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
{
...
guard(preempt)();
guard(read_lock)(&vcpu->kvm->mmu_lock);
...Can acquiring mmu_lock while preemption is disabled cause a sleeping in atomic context warning on PREEMPT_RT kernels?
Since rwlock_t is sleepable on RT, does this sequence need adjustment to avoid panicking the host during nested virtualization execution?
Quoted 10 lines
arch/arm64/kvm/nested.c +4
-0if (!vt->valid)return ; +/* We cache the MMU state in the TLB. Check that it matches. */ +if (!!(vcpu_read_sys_reg (vcpu,SCTLR_EL2 ) & SCTLR_ELx_M) !=s1_walk_translated (&vt->wr)) +return ; +if (read_vncr_el2 (vcpu) != vt->gva)return ;
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
Marc Zyngier
maintainerPATCH v2 4/8 KVM: arm64: Correctly handle end of VA space TLBI invalidation
In reply to Marc ZyngierOur TLB invalidation by VA code is based on comparing two ranges, one defined by the TLB, and one defined by the TLBI instruction.
Each range is defined by a start and a size. However, the way the comparison is done doesn't account for address rollover, as it compares an address with (base + size). This works nicely until this expression represent the last page/block in the TTBR1 VA space, as the result is a big fat 0. And a failed TLB invalidation.
Rewrite the comparison in a way that is immune to the address rollover (making the end address inclusive instead of exclusive), and move this into a common helper that is used by both VA and IPA invalidations, as suggested by Hyunwoo Kim (although the IPA version didn't suffer from this particular problem, obviously).
--- arch/arm64/kvm/nested.c | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-)
arch/arm64/kvm/nested.c +19 -24
@@ -999,6 +999,20 @@ static void invalidate_vncr(struct vncr_tlb *vt)clear_fixmap (vncr_fixmap (vt->cpu)); } +static bool vncr_tlb_intersects (struct vncr_tlb *vt,u64 addr, +u64 scope_start,u64 scope_size) +{ +u64 tlb_size, tlb_start, tlb_end, scope_end; + + tlb_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, vt->wr.level)); + + tlb_start = addr & ~(tlb_size -1 ); + tlb_end = tlb_start + tlb_size -1 ; + scope_end = scope_start + scope_size -1 ; + +return !(tlb_end < scope_start || tlb_start > scope_end); +} +/* * VNCR TLB invalidation occurs from MMU notifiers or TLBI instructions, and * either can race against a vcpu not being onlined yet (no pseudo-TLB
@@ -1021,19 +1035,9 @@ static void kvm_invalidate_vncr_ipa(struct kvm *kvm, u64 start, u64 end)if (!kvm_has_feat (kvm,ID_AA64MMFR4_EL1 , NV_frac,NV2_ONLY ))return ; -kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) { -u64 ipa_start, ipa_end, ipa_size; - - ipa_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, - vt->wr.level)); - ipa_start = vt->wr.pa & ~(ipa_size -1 ); - ipa_end = ipa_start + ipa_size; - -if (ipa_end <= start || ipa_start >= end) -continue ; - -invalidate_vncr (vt); - } +kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) +if (vncr_tlb_intersects (vt, vt->wr.pa, start, end - start)) +invalidate_vncr (vt); }struct s1e2_tlbi_scope {
@@ -1059,28 +1063,19 @@ static void invalidate_vncr_va(struct kvm *kvm,lockdep_assert_held_write (&kvm->mmu_lock);kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) { -u64 va_start, va_end, va_size; - - va_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, - vt->wr.level)); - va_start = vt->gva & ~(va_size -1 ); - va_end = va_start + va_size; -switch (scope->type) {case TLBI_ALL :break ;case TLBI_VA : -if (va_end <= scope->va || - va_start >= (scope->va + scope->size)) +if (!vncr_tlb_intersects (vt, vt->gva, scope->va, scope->size))continue ;if (vt->wr.nG && vt->wr.asid != scope->asid)continue ;break ;case TLBI_VAA : -if (va_end <= scope->va || - va_start >= (scope->va + scope->size)) +if (!vncr_tlb_intersects (vt, vt->gva, scope->va, scope->size))continue ;break ;
Signature · 1 line
2.47.3
[email protected]
AI reviewRe: [PATCH v2 4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] Range TLBI instructions for Stage-1 TTBR1 addresses silently fail to invalidate VNCR entries because the base address is not sign-extended.
- [High] The tlbi_va_s1_to_va macro and read_vncr_el2 sign-extend from bit 48, which corrupts 52-bit Virtual Addresses (LVA). --
--- Patch [4]: [PATCH v2 4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation --- commit 0f00ac726029153c77159941056d984a8dc753cf
Author: Marc Zyngier <[email protected]>
Subject: KVM: arm64: Correctly handle end of VA space TLBI invalidation
This commit fixes an issue with TLB invalidation by VA where address rollover at the end of the TTBR1 VA space caused invalidations to fail. It introduces a common helper, vncr_tlb_intersects(), which uses inclusive end addresses to prevent this issue for both VA and IPA invalidations.
Quoted 5 lines
arch/arm64/kvm/nested.c +0
-0@@ -1000,6 +1000,20 @@ static void invalidate_vncr(struct vncr_tlb *vt)
[ ... ]
arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1013
Quoted 12 lines
arch/arm64/kvm/nested.c +12
-0+static bool vncr_tlb_intersects (struct vncr_tlb *vt,u64 addr, +u64 scope_start,u64 scope_size) +{ +u64 tlb_size, tlb_start, tlb_end, scope_end; + + tlb_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, vt->wr.level)); + + tlb_start = addr & ~(tlb_size -1 ); + tlb_end = tlb_start + tlb_size -1 ; + scope_end = scope_start + scope_size -1 ; + +return !(tlb_end < scope_start || tlb_start > scope_end);
[Severity: High]
This is a pre-existing issue, but could this intersection check fail for Stage-1 TTBR1 Range TLBIs due to a missing sign extension?
When a guest issues a Range TLBI for a TTBR1 virtual address, decode_range_tlbi() zero-extends the base address:
arch/arm64/include/asm/kvm_nested.h:decode_range_tlbi() {
...
base = (val & GENMASK(36, 0)) << shift;
...
}In compute_s1_tlbi_range(), this is directly assigned to scope->va.
Since scope->va is passed as scope_start here and is zero-extended (a large positive unsigned number), won't it fail to intersect with vt->gva, which is sign-extended (a negative number for TTBR1)?
Quoted 5 lines
arch/arm64/kvm/nested.c +2
-0+} +/* * VNCR TLB invalidation occurs from MMU notifiers or TLBI instructions, and * either can race against a vcpu not being onlined yet (no pseudo-TLB
[ ... ]
arch/arm64/kvm/nested.c static void invalidate_vncr_va(struct kvm *kvm, · line 1072
Quoted 12 lines
arch/arm64/kvm/nested.c +1
-2@@ -1060,28 +1064,19 @@ static void invalidate_vncr_va(struct kvm *kvm,lockdep_assert_held_write (&kvm->mmu_lock);kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) {switch (scope->type) {case TLBI_ALL :break ;case TLBI_VA : -if (va_end <= scope->va || - va_start >= (scope->va + scope->size)) +if (!vncr_tlb_intersects (vt, vt->gva, scope->va, scope->size))
[Severity: High]
This isn't a bug introduced by this patch, but does KVM incorrectly truncate 52-bit Virtual Addresses (LVA) to 48 bits when constructing these VAs?
The tlbi_va_s1_to_va() macro and read_vncr_el2() both hardcode a 48-bit sign extension:
arch/arm64/kvm/nested.c:read_vncr_el2() {
...
return (u64)sign_extend64(__vcpu_sys_reg(vcpu, VNCR_EL2), 48);
}If a guest uses 52-bit VAs (FEAT_LVA) and places the VNCR page at a TTBR1 address where bit 48 is 0, wouldn't the upper bits (49-63) be zero-extended instead of sign-extended?
This seems like it would corrupt the VA into a TTBR0 address, and Range TLBIs (which preserve bits up to 53 in decode_range_tlbi) would fail to intersect with the corrupted 48-bit vt->gva.
Quoted 4 lines
arch/arm64/kvm/nested.c +0
-0continue ;if (vt->wr.nG && vt->wr.asid != scope->asid)continue ;break ;
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
Marc Zyngier
maintainerRe: [PATCH v2 4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation
Quoted 50 lines · On Thu, 06 Aug 2026 10:30:29 +0100, [email protected] wrote:
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] Range TLBI instructions for Stage-1 TTBR1 addresses silently fail to invalidate VNCR entries because the base address is not sign-extended.
- [High] Thetlbi_va_s1_to_vamacro andread_vncr_el2sign-extend from bit 48, which corrupts 52-bit Virtual Addresses (LVA). ----- Patch [4]: [PATCH v2 4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation --- commit
0f00ac726029153c77159941056d984a8dc753cf
Author: Marc Zyngier <[email protected]>
Subject: KVM: arm64: Correctly handle end of VA space TLBI invalidationThis commit fixes an issue with TLB invalidation by VA where address rollover at the end of the TTBR1 VA space caused invalidations to fail. It introduces a common helper,
vncr_tlb_intersects(), which uses inclusive end addresses to prevent this issue for both VA and IPA invalidations.Quoted 5 lines
arch/arm64/kvm/nested.c +0
-0@@ -1000,6 +1000,20 @@ static void invalidate_vncr(struct vncr_tlb *vt)[ ... ]
arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1013
Quoted 12 lines
arch/arm64/kvm/nested.c +12
-0+static bool vncr_tlb_intersects (struct vncr_tlb *vt,u64 addr, +u64 scope_start,u64 scope_size) +{ +u64 tlb_size, tlb_start, tlb_end, scope_end; + + tlb_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, vt->wr.level)); + + tlb_start = addr & ~(tlb_size -1 ); + tlb_end = tlb_start + tlb_size -1 ; + scope_end = scope_start + scope_size -1 ; + +return !(tlb_end < scope_start || tlb_start > scope_end);[Severity: High]
This is a pre-existing issue, but could this intersection check fail for Stage-1 TTBR1 Range TLBIs due to a missing sign extension?When a guest issues a Range TLBI for a TTBR1 virtual address,
decode_range_tlbi()zero-extends the base address:arch/arm64/include/asm/kvm_nested.h:decode_range_tlbi() { ... base = (val & GENMASK(36, 0)) << shift; ... }
Quoted 5 lines
In
compute_s1_tlbi_range(), this is directly assigned toscope->va.Since
scope->vais passed asscope_starthere and is zero-extended (a large positive unsigned number), won't it fail to intersect withvt->gva, which is sign-extended (a negative number for TTBR1)?
This is patch #6.
Quoted 32 lines
Quoted 6 lines
arch/arm64/kvm/nested.c +2
-0+} +/* * VNCR TLB invalidation occurs from MMU notifiers or TLBI instructions, and * either can race against a vcpu not being onlined yet (no pseudo-TLB [ ... ]
arch/arm64/kvm/nested.c static void invalidate_vncr_va(struct kvm *kvm, · line 1072
Quoted 12 lines
arch/arm64/kvm/nested.c +1
-2@@ -1060,28 +1064,19 @@ static void invalidate_vncr_va(struct kvm *kvm,lockdep_assert_held_write (&kvm->mmu_lock);kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) {switch (scope->type) {case TLBI_ALL :break ;case TLBI_VA : -if (va_end <= scope->va || - va_start >= (scope->va + scope->size)) +if (!vncr_tlb_intersects (vt, vt->gva, scope->va, scope->size))[Severity: High]
This isn't a bug introduced by this patch, but does KVM incorrectly truncate 52-bit Virtual Addresses (LVA) to 48 bits when constructing these VAs?The
tlbi_va_s1_to_va()macro andread_vncr_el2()both hardcode a 48-bit sign extension:arch/arm64/kvm/nested.c:read_vncr_el2() { ... return (u64)sign_extend64(__vcpu_sys_reg(vcpu, VNCR_EL2), 48); }
Quoted 7 lines
If a guest uses 52-bit VAs (
FEAT_LVA) and places the VNCR page at a TTBR1 address where bit 48 is 0, wouldn't the upper bits (49-63) be zero-extended instead of sign-extended?This seems like it would corrupt the VA into a TTBR0 address, and Range TLBIs (which preserve bits up to 53 in
decode_range_tlbi) would fail to intersect with the corrupted 48-bitvt->gva.
That'd be a bug if we had support for LVA. We don't.
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH v2 4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation
In reply to Marc ZyngierHi Marc,
Quoted 31 lines · Marc Zyngier
Our TLB invalidation by VA code is based on comparing two ranges, one defined by the TLB, and one defined by the TLBI instruction.
Each range is defined by a start and a size. However, the way the comparison is done doesn't account for address rollover, as it compares an address with (base + size). This works nicely until this expression represent the last page/block in the TTBR1 VA space, as the result is a big fat 0. And a failed TLB invalidation.
Rewrite the comparison in a way that is immune to the address rollover (making the end address inclusive instead of exclusive), and move this into a common helper that is used by both VA and IPA invalidations, as suggested by Hyunwoo Kim (although the IPA version didn't suffer from this particular problem, obviously).
Fixes 4ffa72ad8f37e ("KVM: arm64: nv: Add S1 TLB invalidation primitive for VNCR_EL2")Reviewed-by Yuan Yao <[email protected]>Signed-off-by Marc Zyngier <[email protected]>--- arch/arm64/kvm/nested.c | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-)arch/arm64/kvm/nested.c +0
-0@@ -999,6 +999,20 @@ static void invalidate_vncr(struct vncr_tlb *vt)clear_fixmap (vncr_fixmap (vt->cpu)); }
arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1011
Quoted 10 lines
arch/arm64/kvm/nested.c +10
-0+static bool vncr_tlb_intersects (struct vncr_tlb *vt,u64 addr, +u64 scope_start,u64 scope_size) +{ +u64 tlb_size, tlb_start, tlb_end, scope_end; + + tlb_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, vt->wr.level)); + + tlb_start = addr & ~(tlb_size -1 ); + tlb_end = tlb_start + tlb_size -1 ; + scope_end = scope_start + scope_size -1 ;
I think if scope_end overflows,
arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1013
Quoted 2 lines
arch/arm64/kvm/nested.c +2
-0+ +return !(tlb_end < scope_start || tlb_start > scope_end);
tlb_start > scope_end can evaluate to true and we return false even when there is overlap near the end of the TTBR1 address space.
Therefore I think we need to saturate scope_end to ULONG_MAX when overflow happens. This is also what the architecture does when looking at the pseudocode J1.2.3.442 TLBIRange (M.c).
Thanks,
Wei-Lin Chang
Quoted 62 lines
arch/arm64/kvm/nested.c +7
-24+} +/* * VNCR TLB invalidation occurs from MMU notifiers or TLBI instructions, and * either can race against a vcpu not being onlined yet (no pseudo-TLB @@ -1021,19 +1035,9 @@ static void kvm_invalidate_vncr_ipa(struct kvm *kvm, u64 start, u64 end)if (!kvm_has_feat (kvm,ID_AA64MMFR4_EL1 , NV_frac,NV2_ONLY ))return ; -kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) { -u64 ipa_start, ipa_end, ipa_size; - - ipa_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, - vt->wr.level)); - ipa_start = vt->wr.pa & ~(ipa_size -1 ); - ipa_end = ipa_start + ipa_size; - -if (ipa_end <= start || ipa_start >= end) -continue ; - -invalidate_vncr (vt); - } +kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) +if (vncr_tlb_intersects (vt, vt->wr.pa, start, end - start)) +invalidate_vncr (vt); }struct s1e2_tlbi_scope {@@ -1059,28 +1063,19 @@ static void invalidate_vncr_va(struct kvm *kvm,lockdep_assert_held_write (&kvm->mmu_lock);kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) { -u64 va_start, va_end, va_size; - - va_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, - vt->wr.level)); - va_start = vt->gva & ~(va_size -1 ); - va_end = va_start + va_size; -switch (scope->type) {case TLBI_ALL :break ;case TLBI_VA : -if (va_end <= scope->va || - va_start >= (scope->va + scope->size)) +if (!vncr_tlb_intersects (vt, vt->gva, scope->va, scope->size))continue ;if (vt->wr.nG && vt->wr.asid != scope->asid)continue ;break ;case TLBI_VAA : -if (va_end <= scope->va || - va_start >= (scope->va + scope->size)) +if (!vncr_tlb_intersects (vt, vt->gva, scope->va, scope->size))continue ;break ;Signature · 1 line
2.47.3
Marc Zyngier
maintainerRe: [PATCH v2 4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation
Quoted 48 lines · Wei-Lin Chang
Hi Marc,
Quoted 31 lines · Marc Zyngier
Our TLB invalidation by VA code is based on comparing two ranges, one defined by the TLB, and one defined by the TLBI instruction.
Each range is defined by a start and a size. However, the way the comparison is done doesn't account for address rollover, as it compares an address with (base + size). This works nicely until this expression represent the last page/block in the TTBR1 VA space, as the result is a big fat 0. And a failed TLB invalidation.
Rewrite the comparison in a way that is immune to the address rollover (making the end address inclusive instead of exclusive), and move this into a common helper that is used by both VA and IPA invalidations, as suggested by Hyunwoo Kim (although the IPA version didn't suffer from this particular problem, obviously).
Fixes 4ffa72ad8f37e ("KVM: arm64: nv: Add S1 TLB invalidation primitive for VNCR_EL2")Reviewed-by Yuan Yao <[email protected]>Signed-off-by Marc Zyngier <[email protected]>--- arch/arm64/kvm/nested.c | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-)arch/arm64/kvm/nested.c +0
-0@@ -999,6 +999,20 @@ static void invalidate_vncr(struct vncr_tlb *vt)clear_fixmap (vncr_fixmap (vt->cpu)); }arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1011
Quoted 10 lines
arch/arm64/kvm/nested.c +10
-0+static bool vncr_tlb_intersects (struct vncr_tlb *vt,u64 addr, +u64 scope_start,u64 scope_size) +{ +u64 tlb_size, tlb_start, tlb_end, scope_end; + + tlb_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, vt->wr.level)); + + tlb_start = addr & ~(tlb_size -1 ); + tlb_end = tlb_start + tlb_size -1 ; + scope_end = scope_start + scope_size -1 ;I think if
scope_endoverflows,
Quoted 5 lines
arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1013
Quoted 2 lines
arch/arm64/kvm/nested.c +2
-0+ +return !(tlb_end < scope_start || tlb_start > scope_end);
tlb_start>scope_endcan evaluate to true and we return false even when there is overlap near the end of the TTBR1 address space.
I assume that you are implicitly talking about TLB Range Invalidation, right? Because we otherwise align base on size, making overflows impossible with this patch.
Quoted 3 lines
Therefore I think we need to saturate
scope_endtoULONG_MAXwhen overflow happens. This is also what the architecture does when looking at the pseudocode J1.2.3.442 TLBIRange (M.c).
That's one possible implementation, because the pseudocode is dealing with start/end directly, but that's not how our scope works (it deals with start and size).
For that we need to cap the *size*, similarly to what is done in handle_ripas2e1is() (see patch #6). Something like the hack below.
M.
arch/arm64/include/asm/kvm_nested.h +6 -0
@@ -305,6 +305,12 @@ static inline u64 decode_range_tlbi(u64 val, u64 *range, u16 *asid) num =FIELD_GET (GENMASK (43 ,39 ), val); *range =__TLBI_RANGE_PAGES (num, scale) << shift; +/* Cap the range to the correct half of the address space */ +if (!(base &BIT (48 ))) + *range =min (*range, (BIT (48 ) - base)); +else + *range =min (*range, ~base +1 ); +return base; }
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH v2 4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation
Quoted 56 lines · Marc Zyngier
Quoted 48 lines · Wei-Lin Chang
Hi Marc,
Quoted 31 lines · Marc Zyngier
Our TLB invalidation by VA code is based on comparing two ranges, one defined by the TLB, and one defined by the TLBI instruction.
Each range is defined by a start and a size. However, the way the comparison is done doesn't account for address rollover, as it compares an address with (base + size). This works nicely until this expression represent the last page/block in the TTBR1 VA space, as the result is a big fat 0. And a failed TLB invalidation.
Rewrite the comparison in a way that is immune to the address rollover (making the end address inclusive instead of exclusive), and move this into a common helper that is used by both VA and IPA invalidations, as suggested by Hyunwoo Kim (although the IPA version didn't suffer from this particular problem, obviously).
Fixes 4ffa72ad8f37e ("KVM: arm64: nv: Add S1 TLB invalidation primitive for VNCR_EL2")Reviewed-by Yuan Yao <[email protected]>Signed-off-by Marc Zyngier <[email protected]>--- arch/arm64/kvm/nested.c | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-)arch/arm64/kvm/nested.c +0
-0@@ -999,6 +999,20 @@ static void invalidate_vncr(struct vncr_tlb *vt)clear_fixmap (vncr_fixmap (vt->cpu)); }arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1011
Quoted 10 lines
arch/arm64/kvm/nested.c +10
-0+static bool vncr_tlb_intersects (struct vncr_tlb *vt,u64 addr, +u64 scope_start,u64 scope_size) +{ +u64 tlb_size, tlb_start, tlb_end, scope_end; + + tlb_size =ttl_to_size (pgshift_level_to_ttl (vt->wi.pgshift, vt->wr.level)); + + tlb_start = addr & ~(tlb_size -1 ); + tlb_end = tlb_start + tlb_size -1 ; + scope_end = scope_start + scope_size -1 ;I think if
scope_endoverflows,Quoted 5 lines
arch/arm64/kvm/nested.c static void invalidate_vncr(struct vncr_tlb *vt) · line 1013
Quoted 2 lines
arch/arm64/kvm/nested.c +2
-0+ +return !(tlb_end < scope_start || tlb_start > scope_end);
tlb_start>scope_endcan evaluate to true and we return false even when there is overlap near the end of the TTBR1 address space.
Quoted 3 lines
I assume that you are implicitly talking about TLB Range Invalidation, right? Because we otherwise align base on size, making overflows impossible with this patch.
Yes, I am talking about TLB range invalidations.
Quoted 11 lines
Quoted 4 lines
Therefore I think we need to saturate
scope_endtoULONG_MAXwhen overflow happens. This is also what the architecture does when looking at the pseudocode J1.2.3.442 TLBIRange (M.c).That's one possible implementation, because the pseudocode is dealing with start/end directly, but that's not how our scope works (it deals with start and size).
For that we need to cap the *size*, similarly to what is done in
handle_ripas2e1is()(see patch #6). Something like the hack below.
Makes sense.
Quoted 9 lines
M.arch/arm64/include/asm/kvm_nested.h +0
-0@@ -305,6 +305,12 @@ static inline u64 decode_range_tlbi(u64 val, u64 *range, u16 *asid) num =FIELD_GET (GENMASK (43 ,39 ), val);
arch/arm64/include/asm/kvm_nested.h static inline u64 decode_range_tlbi(u64 val, u64 *range, u16 *asid) · line 315
Quoted 10 lines
arch/arm64/include/asm/kvm_nested.h +6
-0*range =__TLBI_RANGE_PAGES (num, scale) << shift; +/* Cap the range to the correct half of the address space */ +if (!(base &BIT (48 ))) + *range =min (*range, (BIT (48 ) - base)); +else + *range =min (*range, ~base +1 ); +return base; }
This looks correct to me!
Thanks,
Wei-Lin Chang
Quoted 4 lines
arch/arm64/include/asm/kvm_nested.h +0
-1--Without deviation from the norm, progress is not possible.
Marc Zyngier
maintainerPATCH v2 5/8 KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR unmapping
In reply to Marc ZyngierWhile VNCR TLB invalidation always occurs under the MMU lock, vcpu_put() doesn't, while it unmaps the VNCR page.
The problem is that the invalidation evaluates vncr_tlb::cpu to decide whether an unmapping needs to take place (cpu != -1) before performing it. On the other hand, this_cpu_reset_vncr_fixmap() unconditionally unmaps if L1_VNCR_MAPPED is set.
These two obviously can race, with a TOCTOU pattern on the TLBI path, and a BUG_ON() on the vcpu_put() path. And the two can end-up calling vncr_fixmap(-1), with extra lethal effects.
Move the reset of vncr_tlb::cpu to -1 to a common function, and make this update atomic so that only a single thread can reset the field and perform the corresponding unmap. The vcpu_put() still need to unconditionally unmap the current VNCR to close another ugly race.
Finally, the assignment of vncr_tlb::cpu is moved to be kept in sync with the actual mapping, similar to L1_VNCR_MAPPED being set.
--- arch/arm64/kvm/nested.c | 42 +++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-)
arch/arm64/kvm/nested.c +32 -10
@@ -27,7 +27,7 @@ struct vncr_tlb {bool hpa_writable;/* -1 when not mapped on a CPU */ -int cpu; +atomic_t cpu;/* * true if the TLB is valid. Can only be changed with the
@@ -894,16 +894,40 @@ void kvm_vcpu_load_hw_mmu(struct kvm_vcpu *vcpu) } } +/* +* Unmapping an L1 VNCR can happen concurrently without the mmu lock being +* effective (vcpu_put() vs TLBI handling). The atomic_xchg below ensures +* that only one CPU sets it to -1 while getting a valid CPU number back. +*/ +static int unmap_l1_vncr (struct vncr_tlb *vt) +{ +int cpu =atomic_xchg_relaxed (&vt->cpu,-1 ); + +if (cpu !=-1 ) +clear_fixmap (vncr_fixmap (cpu)); + +return cpu; +} +static void this_cpu_reset_vncr_fixmap (struct kvm_vcpu *vcpu) {if (!host_data_test_flag (L1_VNCR_MAPPED ))return ; -BUG_ON (vcpu->arch.vncr_tlb->cpu !=smp_processor_id ());BUG_ON (is_hyp_ctxt (vcpu)); -clear_fixmap (vncr_fixmap (vcpu->arch.vncr_tlb->cpu)); - vcpu->arch.vncr_tlb->cpu =-1 ; +/* +* Unconditionally unmap the local VNCR if we have lost the race +* against a concurrent TLBI. Otherwise we could end-up running +* another vcpu with VNCR still mapped if the TLBI thread is +* preempted between the exchange and the clear_fixmap(). +* +* Note that we do not care about the TLBI nuking the fixmap behind +* the back of an running vcpu. This will only generate a fault and +* possibly a retranslation. +*/ +if (unmap_l1_vncr (vcpu->arch.vncr_tlb) ==-1 ) +clear_fixmap (vncr_fixmap (smp_processor_id ()));host_data_clear_flag (L1_VNCR_MAPPED ); }
@@ -995,8 +1019,7 @@ u16 get_asid_by_regime(struct kvm_vcpu *vcpu, enum trans_regime regime)static void invalidate_vncr (struct vncr_tlb *vt) { vt->valid = false; -if (vt->cpu !=-1 ) -clear_fixmap (vncr_fixmap (vt->cpu)); +unmap_l1_vncr (vt); } static bool vncr_tlb_intersects(struct vncr_tlb *vt, u64 addr,
@@ -1452,7 +1475,7 @@ static int kvm_translate_vncr(struct kvm_vcpu *vcpu, bool *is_gmem) vt->hpa = pfn <<PAGE_SHIFT ; vt->hpa_writable = writable; vt->valid = true; - vt->cpu =-1 ; +atomic_set (&vt->cpu,-1 );kvm_make_request (KVM_REQ_MAP_L1_VNCR_EL2 , vcpu);kvm_release_faultin_page (vcpu->kvm, page, false, vt->wr.pw && vt->hpa_writable);
@@ -1583,8 +1606,6 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)if (vt->wr.nG &&get_asid_by_regime (vcpu,TR_EL20 ) != vt->wr.asid)return ; - vt->cpu =smp_processor_id (); -if (vt->hpa_writable && vt->wr.pw && vt->wr.pr) prot =PAGE_KERNEL ;else if (vt->wr.pr)
@@ -1599,7 +1620,8 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)* FIXME: WO doesn't work at all, need POE support in the kernel. */ if (pgprot_val (prot) !=pgprot_val (PAGE_NONE )) { -__set_fixmap (vncr_fixmap (vt->cpu), vt->hpa, prot); +atomic_set (&vt->cpu,smp_processor_id ()); +__set_fixmap (vncr_fixmap (atomic_read (&vt->cpu)), vt->hpa, prot);host_data_set_flag (L1_VNCR_MAPPED ); } }
Signature · 1 line
2.47.3
[email protected]
AI reviewRe: [PATCH v2 5/8] KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR unmapping
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Acquiring mmu_lock with preemption disabled causes a sleep-in-atomic bug on PREEMPT_RT kernels. --
--- Patch [5]: [PATCH v2 5/8] KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR unmapping --- commit 7a7c469ac4158e77c3548a526ab62a924ed75161
Author: Marc Zyngier <[email protected]>
KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR unmapping
This patch addresses a race condition between VNCR TLB invalidation and VNCR unmapping during vcpu_put(). It introduces an atomic exchange for resetting the cpu tracking field and a common function for unmapping, ensuring these operations happen atomically. This closes the TOCTOU race and prevents concurrent mapping faults.
Quoted 4 lines
arch/arm64/kvm/nested.c +0
-0
[ ... ]
arch/arm64/kvm/nested.c static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
Quoted 1 line
arch/arm64/kvm/nested.c +0
-0@@ -1584,8 +1607,6 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
[Severity: High]
This is a pre-existing issue, but does acquiring mmu_lock inside a preemption-disabled region here cause a sleep-in-atomic bug on RT kernels?
Looking at kvm_map_l1_vncr() from the context, it starts with:
static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
{
struct vncr_tlb *vt = vcpu->arch.vncr_tlb;
pgprot_t prot;
guard(preempt)();
guard(read_lock)(&vcpu->kvm->mmu_lock);Since read_lock() is implemented as an RT-mutex on PREEMPT_RT and can block, could this sequence trigger a scheduling-while-atomic panic if the lock is contended?
Quoted 8 lines
arch/arm64/kvm/nested.c +0
-2if (vt->wr.nG &&get_asid_by_regime (vcpu,TR_EL20 ) != vt->wr.asid)return ; - vt->cpu =smp_processor_id (); -if (vt->hpa_writable && vt->wr.pw && vt->wr.pr) prot =PAGE_KERNEL ;else if (vt->wr.pr)
[ ... ]
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=5
Marc Zyngier
maintainerRe: [PATCH v2 5/8] KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR unmapping
Quoted 44 lines · On Thu, 06 Aug 2026 10:25:39 +0100, [email protected] wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Acquiringmmu_lockwith preemption disabled causes a sleep-in-atomic bug onPREEMPT_RTkernels. ----- Patch [5]: [PATCH v2 5/8] KVM: arm64: Handle VNCR TLB invalidation race with
vcpu_put()VNCR unmapping --- commit7a7c469ac4158e77c3548a526ab62a924ed75161
Author: Marc Zyngier <[email protected]>KVM: arm64: Handle VNCR TLB invalidation race with
vcpu_put()VNCR unmappingThis patch addresses a race condition between VNCR TLB invalidation and VNCR unmapping during
vcpu_put(). It introduces an atomic exchange for resetting the cpu tracking field and a common function for unmapping, ensuring these operations happen atomically. This closes the TOCTOU race and prevents concurrent mapping faults.Quoted 4 lines
arch/arm64/kvm/nested.c +0
-0[ ... ]
arch/arm64/kvm/nested.c static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
Quoted 1 line
arch/arm64/kvm/nested.c +0
-0@@ -1584,8 +1607,6 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)[Severity: High]
This is a pre-existing issue, but does acquiringmmu_lockinside a preemption-disabled region here cause a sleep-in-atomic bug on RT kernels?Looking at
kvm_map_l1_vncr()from the context, it starts with:static void
kvm_map_l1_vncr(struct kvm_vcpu *vcpu){ struct vncr_tlb *vt = vcpu->arch.vncr_tlb; pgprot_t prot; guard(preempt)(); guard(read_lock)(&vcpu->kvm->mmu_lock);
Quoted 3 lines
Since
read_lock()is implemented as an RT-mutex onPREEMPT_RTand can block, could this sequence trigger a scheduling-while-atomic panic if the lock is contended?
I think we need to understand what that means for the state of the fixmap if we get preempted at this point.
I think it is OK as long as the read_lock itself disables preemption but this is sketchy. In any case, RT+NV is not something I wish to think about right now.
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Re: [PATCH v2 5/8] KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR unmapping
In reply to Marc ZyngierQuoted 11 lines · Marc Zyngier
While VNCR TLB invalidation always occurs under the MMU lock,
vcpu_put()doesn't, while it unmaps the VNCR page.The problem is that the invalidation evaluates
vncr_tlb::cputo decide whether an unmapping needs to take place (cpu != -1) before performing it. On the other hand,this_cpu_reset_vncr_fixmap()unconditionally unmaps ifL1_VNCR_MAPPEDis set.These two obviously can race, with a TOCTOU pattern on the TLBI path, and a
BUG_ON()on thevcpu_put()path. And the two can end-up callingvncr_fixmap(-1), with extra lethal effects.
...
Quoted 5 lines
static void
this_cpu_reset_vncr_fixmap(struct kvm_vcpu *vcpu){arch/arm64/kvm/nested.c +0
-0if (!host_data_test_flag (L1_VNCR_MAPPED ))return ;
arch/arm64/kvm/nested.c void kvm_vcpu_load_hw_mmu(struct kvm_vcpu *vcpu) · line 923
Quoted 10 lines
arch/arm64/kvm/nested.c +5
-3-BUG_ON (vcpu->arch.vncr_tlb->cpu !=smp_processor_id ());BUG_ON (is_hyp_ctxt (vcpu)); -clear_fixmap (vncr_fixmap (vcpu->arch.vncr_tlb->cpu)); - vcpu->arch.vncr_tlb->cpu =-1 ; +/* +* Unconditionally unmap the local VNCR if we have lost the race +* against a concurrent TLBI. Otherwise we could end-up running +* another vcpu with VNCR still mapped if the TLBI thread is +* preempted between the exchange and the clear_fixmap().
NICE CATCH.
I thought about the remain changed code path in this patch and they have MMU lock w/ preemption disabled. This is one trick racing.
Quoted 50 lines
arch/arm64/kvm/nested.c +11
-7+* +* Note that we do not care about the TLBI nuking the fixmap behind +* the back of an running vcpu. This will only generate a fault and +* possibly a retranslation. +*/ +if (unmap_l1_vncr (vcpu->arch.vncr_tlb) ==-1 ) +clear_fixmap (vncr_fixmap (smp_processor_id ()));host_data_clear_flag (L1_VNCR_MAPPED ); }@@ -995,8 +1019,7 @@ u16 get_asid_by_regime(struct kvm_vcpu *vcpu, enum trans_regime regime)static void invalidate_vncr (struct vncr_tlb *vt) { vt->valid = false; -if (vt->cpu !=-1 ) -clear_fixmap (vncr_fixmap (vt->cpu)); +unmap_l1_vncr (vt); } static bool vncr_tlb_intersects(struct vncr_tlb *vt, u64 addr,@@ -1452,7 +1475,7 @@ static int kvm_translate_vncr(struct kvm_vcpu *vcpu, bool *is_gmem) vt->hpa = pfn <<PAGE_SHIFT ; vt->hpa_writable = writable; vt->valid = true; - vt->cpu =-1 ; +atomic_set (&vt->cpu,-1 );kvm_make_request (KVM_REQ_MAP_L1_VNCR_EL2 , vcpu);kvm_release_faultin_page (vcpu->kvm, page, false, vt->wr.pw && vt->hpa_writable);@@ -1583,8 +1606,6 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)if (vt->wr.nG &&get_asid_by_regime (vcpu,TR_EL20 ) != vt->wr.asid)return ; - vt->cpu =smp_processor_id (); -if (vt->hpa_writable && vt->wr.pw && vt->wr.pr) prot =PAGE_KERNEL ;else if (vt->wr.pr)@@ -1599,7 +1620,8 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)* FIXME: WO doesn't work at all, need POE support in the kernel. */ if (pgprot_val (prot) !=pgprot_val (PAGE_NONE )) { -__set_fixmap (vncr_fixmap (vt->cpu), vt->hpa, prot); +atomic_set (&vt->cpu,smp_processor_id ()); +__set_fixmap (vncr_fixmap (atomic_read (&vt->cpu)), vt->hpa, prot);host_data_set_flag (L1_VNCR_MAPPED ); } } --2.47.3
Marc Zyngier
maintainerPATCH v2 6/8 KVM: arm64: Sign-extend VA for range-based TLBI invalidation
In reply to Marc ZyngierWhen the decode_range_tlbi() helper was moved to be used for S1 TLBIs, the required sign extension was omitted. Add it.
As a result, special care must be taken to not overflow PA bits when this is used for S2 invalidation.
--- arch/arm64/include/asm/kvm_nested.h | 7 +++++++ arch/arm64/kvm/sys_regs.c | 11 +++++++++++ 2 files changed, 18 insertions(+)
arch/arm64/include/asm/kvm_nested.h +7 -0
@@ -291,6 +291,13 @@ static inline u64 decode_range_tlbi(u64 val, u64 *range, u16 *asid) base = (val &GENMASK (36 ,0 )) << shift; +/* +* We only deal with at most 48bit VA/IPA, so 48 is where we +* sign-extend from. Should we support FEAT_L{VP}A* at some point, +* this will need to be revisited. +*/ + base = (u64 )sign_extend64 (base,48 ); +if (asid) *asid =FIELD_GET (TLBIR_ASID_MASK , val);
arch/arm64/kvm/sys_regs.c +11 -0
@@ -4057,6 +4057,7 @@ static bool handle_ripas2e1is(struct kvm_vcpu *vcpu, struct sys_reg_params *p,u32 sys_encoding =sys_insn (p->Op0, p->Op1, p->CRn, p->CRm, p->Op2);u64 vttbr =vcpu_read_sys_reg (vcpu,VTTBR_EL2 );u64 base, range; +int pa_bits;if (!kvm_supported_tlbi_ipas2_op (vcpu, sys_encoding))return undef_access (vcpu, p, r);
@@ -4068,6 +4069,16 @@ static bool handle_ripas2e1is(struct kvm_vcpu *vcpu, struct sys_reg_params *p,*/ base =decode_range_tlbi (p->regval, &range,NULL ); +/* +* Ignore TLBIs that start out of PA_bits range, and cap the +* invalidation to the [base:bit(PA_bits)] interval. +*/ + pa_bits =kvm_get_pa_bits (vcpu->kvm); +if (fls64 (base) > pa_bits) +return true; + + range =min (range,BIT_ULL (pa_bits) - base); + kvm_s2_mmu_iterate_by_vmid(vcpu->kvm, get_vmid(vttbr), &(union tlbi_info) { .range = {
Signature · 1 line
2.47.3
Marc Zyngier
maintainerPATCH v2 7/8 KVM: arm64: Make VNCR invalidation participate in MMU invalidation retry
In reply to Marc ZyngierA VNCR TLB invalidation can occur on one vcpu while another vcpu is faulting in this same page. Without correctly handling this, we can end up with the following scenario:
- vcpu A walks the PTs to translate VNCR
- before vcpu A is able to grab the MMU lock to insert the TLB, vcpu B updates the S1 PTs with an invalid entry, and issues a TLBI S1E2 for this VA
- vcpu A inserts the TLB for something that is now invalid
This isn't a new problem, and we manage S2 by having the MMU notifier to bump up mmu_invalidate_seq on invalidation so that the fault can be replayed.
We can perform something similar here, and extend invalidate_vncr_va() to update the same counter, clearly indicating that the context has changed under our feet. This is safe as the invalidation always happen while holding the MMU lock for write, and that we sample the sequence number before walking S1.
--- arch/arm64/kvm/nested.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-)
arch/arm64/kvm/nested.c +18 -3
@@ -1058,6 +1058,12 @@ static void kvm_invalidate_vncr_ipa(struct kvm *kvm, u64 start, u64 end)if (!kvm_has_feat (kvm,ID_AA64MMFR4_EL1 , NV_frac,NV2_ONLY ))return ; +/* +* Note that invalidating the VNCR on the back of an MMU notifier +* doesn't require messing with the invalidation counter for a +* parallel walk. The notifier itself will have bumped the counter, +* making sure we rewalk. +*/ kvm_for_each_vncr_tlb (i, vcpu, vt, kvm)if (vncr_tlb_intersects (vt, vt->wr.pa, start, end - start))invalidate_vncr (vt);
@@ -1085,6 +1091,15 @@ static void invalidate_vncr_va(struct kvm *kvm,lockdep_assert_held_write (&kvm->mmu_lock); +/* +* We might be performing a parallel S1 walk, so bump up the +* invalidation counter even in the absence of an actual VNCR TLB +* invalidation, as this could indicate that the guest has gone +* through a BBM sequence. +*/ + kvm->mmu_invalidate_seq++; +smp_wmb (); +kvm_for_each_vncr_tlb (i, vcpu, vt, kvm) {switch (scope->type) {case TLBI_ALL :
@@ -1419,15 +1434,15 @@ static int kvm_translate_vncr(struct kvm_vcpu *vcpu, bool *is_gmem) va =read_vncr_el2 (vcpu); + mmu_seq = vcpu->kvm->mmu_invalidate_seq; +smp_rmb (); + ret =__kvm_translate_va (vcpu, &vt->wi, &vt->wr, va);if (ret)return ret; write_fault =kvm_is_write_fault (vcpu); - mmu_seq = vcpu->kvm->mmu_invalidate_seq; -smp_rmb (); - gfn = vt->wr.pa >>PAGE_SHIFT ; memslot =gfn_to_memslot (vcpu->kvm, gfn);if (!memslot) {
Signature · 1 line
2.47.3
Marc Zyngier
maintainerPATCH v2 8/8 KVM: arm64: Add VNCR TLB tracking again
In reply to Marc ZyngierHaving established that our VNCR TLB tracking was flawed and having dropped it from KVM, it is time to replace it with something that actually works in order to get some performance back, as the TLBI overhead is otherwise pretty high.
The goal of such tracking is to hit the TLBI slow path if there are any VNCR TLBs in the guest, irrespective of their mapping state.
For this purpose, we introduce an VM wide counter (vncr_tlb_count) that tracks how many valid VNCR TLB are present. This means that creating such TLB must increment the counter, and invalidation decrement it, and both these operations must be done with the MMU lock held for write.
On TLBI handling affecting EL2 S1, a non-zero counter forces the handling to take the slow path to consider the VNCR TLBs. Note that the bumping up is done "speculatively" in order to make sure that a concurrent invalidation will be taken on the slow path.
Not exactly rocket science. Hopefully I got it right this time.
--- arch/arm64/include/asm/kvm_host.h | 3 +++ arch/arm64/kvm/hyp/vhe/switch.c | 11 +++++++--- arch/arm64/kvm/nested.c | 36 +++++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 9 deletions(-)
arch/arm64/include/asm/kvm_host.h +3 -0
@@ -411,6 +411,9 @@ struct kvm_arch {/* Masks for VNCR-backed and general EL2 sysregs */ struct kvm_sysreg_masks *sysreg_masks; +/* Count the number of VNCR_EL2 TLBs */ +atomic_t vncr_tlb_count; +/* * For an untrusted host VM, 'pkvm.handle' is used to lookup * the associated pKVM instance in the hypervisor.
arch/arm64/kvm/hyp/vhe/switch.c +8 -3
@@ -424,10 +424,15 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)return false;/* -* If we have to check for any VNCR mapping being invalidated, -* go back to the slow path for further processing. +* If we have to check for any VNCR TLB being invalidated, go back +* to the slow path for further processing. +* +* The synchronisation betweem TLBI and walk is provided by the +* speculative increment of the TLB counter on walk, and the +* invalidation counter. Yes, this is fiddly. */ -if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && +atomic_read (&vcpu->kvm->arch.vncr_tlb_count))return false;__kvm_skip_instr (vcpu);
arch/arm64/kvm/nested.c +30 -6
@@ -48,6 +48,7 @@ void kvm_init_nested(struct kvm *kvm) { kvm->arch.nested_mmus =NULL ; kvm->arch.nested_mmus_size =0 ; +atomic_set (&kvm->arch.vncr_tlb_count,0 ); }static int init_nested_s2_mmu (struct kvm *kvm,struct kvm_s2_mmu *mmu)
@@ -1016,10 +1017,12 @@ u16 get_asid_by_regime(struct kvm_vcpu *vcpu, enum trans_regime regime)return asid; } -static void invalidate_vncr (struct vncr_tlb *vt) +static void invalidate_vncr (struct kvm *kvm,struct vncr_tlb *vt) { +BUG_ON (!vt->valid); vt->valid = false;unmap_l1_vncr (vt); +atomic_dec (&kvm->arch.vncr_tlb_count); } static bool vncr_tlb_intersects(struct vncr_tlb *vt, u64 addr,
@@ -1066,7 +1069,7 @@ static void kvm_invalidate_vncr_ipa(struct kvm *kvm, u64 start, u64 end)*/ kvm_for_each_vncr_tlb (i, vcpu, vt, kvm)if (vncr_tlb_intersects (vt, vt->wr.pa, start, end - start)) -invalidate_vncr (vt); +invalidate_vncr (kvm, vt); }struct s1e2_tlbi_scope {
@@ -1123,7 +1126,7 @@ static void invalidate_vncr_va(struct kvm *kvm,break ; } -invalidate_vncr (vt); +invalidate_vncr (kvm, vt); } }
@@ -1359,13 +1362,20 @@ void kvm_arch_flush_shadow_all(struct kvm *kvm)* intersects with the TLBI request, invalidate it, and unmap the page * from the fixmap. Because we need to look at all the vcpu-private TLBs, * this requires some wide-ranging locking to ensure that nothing races -* against it. This may require some refcounting to avoid the search when -* no such TLB is present. +* against it. This requires some refcounting to avoid the search when +* no such TLB is present (see below). * * - On MMU notifiers, we must invalidate our TLB in a similar way, but * looking at the IPA instead. The funny part is that there may not be a * stage-2 mapping for this page if L1 hasn't accessed it using LD/ST * instructions. +* +* - vncr_tlb_count tracks the number of valid VNCR TLBs VM-wide. This isn't +* the number of *mapped* L1 VNCR pages, which is likely be a subset (and +* by definition, a TLBI handled from L1 runs with the canonical VNCR +* page, not the L1's). The innermost trap handling code checks this to +* find out whether to return to the guest ASAP (no L1 TLBs) or to visit +* this part of the world for some extra invalidation work. */ int kvm_vcpu_allocate_vncr_tlb (struct kvm_vcpu *vcpu)
@@ -1420,7 +1430,8 @@ static int kvm_translate_vncr(struct kvm_vcpu *vcpu, bool *is_gmem)*/ scoped_guard(write_lock, &vcpu->kvm->mmu_lock) {this_cpu_reset_vncr_fixmap (vcpu); - vt->valid = false; +if (vt->valid) +invalidate_vncr (vcpu->kvm, vt); vt->wi = (struct s1_walk_info ) { .regime =TR_EL20 ,
@@ -1545,7 +1556,20 @@ int kvm_handle_vncr_abort(struct kvm_vcpu *vcpu)return -EIO ; } +/* +* Speculatively increment the TLB count to make sure concurrent +* TLBIs will take the slow path, and will interact with the retry +* mechanism. Drop it again on error. +*/ +atomic_inc (&vcpu->kvm->arch.vncr_tlb_count); +smp_mb__after_atomic (); + ret =kvm_translate_vncr (vcpu, &is_gmem); +if (ret) { +smp_mb__before_atomic (); +atomic_dec (&vcpu->kvm->arch.vncr_tlb_count); + } +switch (ret) {case -EAGAIN :/* Let's try again... */
Signature · 1 line
2.47.3
[email protected]
AI reviewRe: [PATCH v2 8/8] KVM: arm64: Add VNCR TLB tracking again
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] The fast-path TLBI handler incorrectly skips the slow path for native EL2 TLBI instructions when E2H or TGE is clear, bypassing software VNCR TLB invalidation and leaving stale mappings. --
--- Patch [8]: [PATCH v2 8/8] KVM: arm64: Add VNCR TLB tracking again --- commit 65741607ee5db3e8d57cf0e1773bb1031e83a3ee
Author: Marc Zyngier <[email protected]>
KVM: arm64: Add VNCR TLB tracking again
This commit introduces a VM-wide counter to track valid VNCR TLBs. It aims to improve performance by taking the TLBI slow path only when there are active VNCR TLBs that need invalidation.
Quoted 3 lines
arch/arm64/kvm/hyp/vhe/switch.c +0
-0
[ ... ]
Quoted 7 lines
arch/arm64/kvm/hyp/vhe/switch.c +1
-2@@ -424,10 +424,15 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)return false;/* -* If we have to check for any VNCR mapping being invalidated, -* go back to the slow path for further processing. +* If we have to check for any VNCR TLB being invalidated, go back
arch/arm64/kvm/hyp/vhe/switch.c static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code) · line 436
Quoted 10 lines
arch/arm64/kvm/hyp/vhe/switch.c +7
-1+* to the slow path for further processing. +* +* The synchronisation betweem TLBI and walk is provided by the +* speculative increment of the TLB counter on walk, and the +* invalidation counter. Yes, this is fiddly. */ -if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && +atomic_read (&vcpu->kvm->arch.vncr_tlb_count))return false;
[Severity: High]
This is a pre-existing issue, but does this condition incorrectly skip the slow path for native EL2 TLBI instructions when E2H or TGE is clear?
If a nested hypervisor running in nVHE mode (E2H=0) or temporarily clearing TGE executes an S1E2 TLBI instruction, kvm_hyp_handle_tlbi_el2() will execute the hardware TLBI. However, since the fallback check requires both E2H and TGE to be set, it evaluates to false. This returns true and bypasses the software VNCR TLB invalidation.
Should the check instead read atomic_read(&vcpu->kvm->arch.vncr_tlb_count) unconditionally for any successful EL2 S1 TLBI so that stale software VNCR mappings aren't left behind?
Quoted 2 lines
arch/arm64/kvm/hyp/vhe/switch.c +0
-0__kvm_skip_instr (vcpu);
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
Marc Zyngier
maintainerRe: [PATCH v2 8/8] KVM: arm64: Add VNCR TLB tracking again
Quoted 34 lines · On Thu, 06 Aug 2026 10:35:52 +0100, [email protected] wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] The fast-path TLBI handler incorrectly skips the slow path for native EL2 TLBI instructions when E2H or TGE is clear, bypassing software VNCR TLB invalidation and leaving stale mappings. ----- Patch [8]: [PATCH v2 8/8] KVM: arm64: Add VNCR TLB tracking again --- commit
65741607ee5db3e8d57cf0e1773bb1031e83a3ee
Author: Marc Zyngier <[email protected]>KVM: arm64: Add VNCR TLB tracking again
This commit introduces a VM-wide counter to track valid VNCR TLBs. It aims to improve performance by taking the TLBI slow path only when there are active VNCR TLBs that need invalidation.
Quoted 3 lines
arch/arm64/kvm/hyp/vhe/switch.c +0
-0[ ... ]
Quoted 13 lines
arch/arm64/kvm/hyp/vhe/switch.c +6
-2@@ -424,10 +424,15 @@ static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code)return false;/* -* If we have to check for any VNCR mapping being invalidated, -* go back to the slow path for further processing. +* If we have to check for any VNCR TLB being invalidated, go back +* to the slow path for further processing. +* +* The synchronisation betweem TLBI and walk is provided by the +* speculative increment of the TLB counter on walk, and the +* invalidation counter. Yes, this is fiddly. */
Quoted 8 lines
arch/arm64/kvm/hyp/vhe/switch.c static bool kvm_hyp_handle_tlbi_el2(struct kvm_vcpu *vcpu, u64 *exit_code) · line 436
Quoted 4 lines
arch/arm64/kvm/hyp/vhe/switch.c +2
-1-if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu)) +if (vcpu_el2_e2h_is_set (vcpu) &&vcpu_el2_tge_is_set (vcpu) && +atomic_read (&vcpu->kvm->arch.vncr_tlb_count))return false;[Severity: High]
This is a pre-existing issue, but does this condition incorrectly skip the slow path for native EL2 TLBI instructions when E2H or TGE is clear?
See answer to the patch #1 review.
M.
Signature · 1 line
Without deviation from the norm, progress is not possible.
Quoted 8 lines · Marc Zyngier
Prompted by a patch [0] from Hyunwoo Kim which was addressing a pretty annoying corner case, I spent some time looking at what was wrong in our VNCR TLBI code. And there was a few things to say about it...
- The TLB tracking to get into the slow path is wrong. It tracks what is mapped, but not the established TLBs. Kind of annoying.
[...]
Applied to next, thanks!
[1/8] KVM: arm64: Remove VM-wide VNCR mapping counter
https://git.kernel.org/kvmarm/kvmarm/c/c55bc773b6e8 [2/8] KVM: arm64: Handle negative S1 walk levels in VNCR TLB size evaluation https://git.kernel.org/kvmarm/kvmarm/c/8053393680d4 [3/8] KVM: arm64: Consider SCTLR_EL2.M when mapping the L1 VNCR page https://git.kernel.org/kvmarm/kvmarm/c/8c774604b6ec [4/8] KVM: arm64: Correctly handle end of VA space TLBI invalidation https://git.kernel.org/kvmarm/kvmarm/c/34af2c3e31f9 [5/8] KVM: arm64: Handle VNCR TLB invalidation race with vcpu_put() VNCR unmapping https://git.kernel.org/kvmarm/kvmarm/c/38640bc32be3 [6/8] KVM: arm64: Sign-extend VA for range-based TLBI invalidation https://git.kernel.org/kvmarm/kvmarm/c/239347008564 [7/8] KVM: arm64: Make VNCR invalidation participate in MMU invalidation retry https://git.kernel.org/kvmarm/kvmarm/c/2b7324f3a0c1 [8/8] KVM: arm64: Add VNCR TLB tracking again https://git.kernel.org/kvmarm/kvmarm/c/62e3bb894a6e
--
Best,
Oliver