* [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU @ 2026-08-17 10:24 Yaxing Guo 2026-10-09 18:51 ` Anup Patel 0 siblings, 1 reply; 12+ messages in thread From: Yaxing Guo @ 2026-08-17 10:24 UTC (permalink / raw) To: Anup Patel Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso, guoyaxing Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs that previously backed a vCPU. This can miss stale VS-stage TLB entries when a vCPU migrates across host CPUs. For example: - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. - vcpu0 migrates to host CPU0. - the guest updates the mapping on vcpu0 and issues a local sfence.vma. - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. Host CPU1 did not observe the guest-local sfence.vma, and KVM can enter vcpu1 with a stale VS-stage translation left behind by vcpu0. The reported failure was seen on a XiangShan RISC-V system. A bash task ran on vcpu0 while vcpu0 was backed by host CPU1, and a read-only page was prefetched into CPU1's VS-stage TLB. vcpu0 later migrated to host CPU0, where the guest triggered COW, updated the mapping, and issued only a local sfence.vma. The stale VS-stage entry on CPU1 survived. When the same guest task later ran on another vCPU backed by host CPU1, it hit the stale translation. The page contained a GOT pointer, and using the stale data led to a NULL dereference and a userspace segmentation fault. The existing local TLB sanitize path flushes G-stage entries when a vCPU migrates, and flushes VS-stage entries only for implementations selected by the Andes-specific kvm_riscv_vsstage_tlb_no_gpa static key. That handles a split two-stage TLB implementation where HFENCE.GVMA does not invalidate VS-stage entries, but it does not cover the generic guest-local sfence.vma case above. Track, per VM and per host CPU, the last vCPU that entered the guest on that CPU. Before guest entry, flush the current CPU's VS-stage context for the VM if either the current vCPU migrated since its last exit, or this host CPU is switching from another vCPU of the same VM. Keep the existing HFENCE.GVMA behavior tied to vCPU migration. This also subsumes the Andes split-TLB workaround. That workaround was needed because the migration sanitize path previously relied on HFENCE.GVMA alone for most implementations, and only issued HFENCE.VVMA when kvm_riscv_vsstage_tlb_no_gpa was set. The new sanitize path always issues HFENCE.VVMA when reusing a host CPU for a VM context that may have missed a guest-local sfence.vma, so it no longer depends on HFENCE.GVMA invalidating VS-stage entries. The implementation-specific static key and setup hook are therefore no longer needed. Reported-by: Zhizun Wang <anzoso@outlook.com> Reviewed-by: Jiuyue Ma <majiuyue@bosc.ac.cn> Signed-off-by: Yaxing Guo <guoyaxing@bosc.ac.cn> --- arch/riscv/include/asm/kvm_host.h | 6 +++--- arch/riscv/kvm/main.c | 15 --------------- arch/riscv/kvm/tlb.c | 31 +++++++++++++++++++++++-------- arch/riscv/kvm/vm.c | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/include/asm/kvm_host.h index 24585304c..396ac6209 100644 --- a/arch/riscv/include/asm/kvm_host.h +++ b/arch/riscv/include/asm/kvm_host.h @@ -91,6 +91,9 @@ struct kvm_arch { /* G-stage vmid */ struct kvm_vmid vmid; + /* Last VCPU that ran on each physical CPU */ + int __percpu *last_vcpu_ran; + /* G-stage page table */ pgd_t *pgd; phys_addr_t pgd_phys; @@ -330,7 +333,4 @@ bool kvm_riscv_vcpu_stopped(struct kvm_vcpu *vcpu); void kvm_riscv_vcpu_record_steal_time(struct kvm_vcpu *vcpu); -/* Flags representing implementation specific details */ -DECLARE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); - #endif /* __RISCV_KVM_HOST_H__ */ diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c index 0f3fe3986..b1bbc0835 100644 --- a/arch/riscv/kvm/main.c +++ b/arch/riscv/kvm/main.c @@ -10,23 +10,10 @@ #include <linux/err.h> #include <linux/module.h> #include <linux/kvm_host.h> -#include <asm/cpufeature.h> #include <asm/kvm_mmu.h> #include <asm/kvm_nacl.h> #include <asm/sbi.h> -DEFINE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); - -static void kvm_riscv_setup_vendor_features(void) -{ - /* Andes AX66: split two-stage TLBs */ - if (riscv_cached_mvendorid(0) == ANDES_VENDOR_ID && - (riscv_cached_marchid(0) & 0xFFFF) == 0x8A66) { - static_branch_enable(&kvm_riscv_vsstage_tlb_no_gpa); - kvm_info("VS-stage TLB does not cache guest physical address and VMID\n"); - } -} - long kvm_arch_dev_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { @@ -172,8 +159,6 @@ static int __init riscv_kvm_init(void) kvm_info("AIA available with %d guest external interrupts\n", kvm_riscv_aia_nr_hgei); - kvm_riscv_setup_vendor_features(); - kvm_register_perf_callbacks(); rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE); diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c index ff1aeac4e..1541d059a 100644 --- a/arch/riscv/kvm/tlb.c +++ b/arch/riscv/kvm/tlb.c @@ -8,6 +8,7 @@ #include <linux/errno.h> #include <linux/err.h> #include <linux/module.h> +#include <linux/percpu.h> #include <linux/smp.h> #include <linux/kvm_host.h> #include <asm/cacheflush.h> @@ -160,12 +161,19 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) { + bool vcpu_migrated; + bool vcpu_switched; unsigned long vmid; + int *last_ran; - if (!kvm_riscv_gstage_vmid_bits() || - vcpu->arch.last_exit_cpu == vcpu->cpu) + last_ran = this_cpu_ptr(vcpu->kvm->arch.last_vcpu_ran); + vcpu_migrated = (vcpu->arch.last_exit_cpu != vcpu->cpu); + vcpu_switched = (*last_ran != vcpu->vcpu_idx); + if (!vcpu_migrated && !vcpu_switched) return; + vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); + /* * On RISC-V platforms with hardware VMID support, we share same * VMID for all VCPUs of a particular Guest/VM. This means we might @@ -176,16 +184,23 @@ void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) * To cleanup stale TLB entries, we simply flush all G-stage TLB * entries by VMID whenever underlying Host CPU changes for a VCPU. */ - - vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); - kvm_riscv_local_hfence_gvma_vmid_all(vmid); + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) + kvm_riscv_local_hfence_gvma_vmid_all(vmid); /* - * Flush VS-stage TLB entries for implementation where VS-stage - * TLB does not cahce guest physical address and VMID. + * Guest-local sfence.vma only invalidates VS-stage translations on + * the Host CPU currently backing the VCPU. If a VCPU migrates, or + * if this Host CPU switches between VCPUs of the same VM, stale + * VS-stage entries can be left behind on a Host CPU that missed a + * guest-local flush. Flush the current CPU's VS-stage context before + * entering the guest. */ - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) + if (kvm_riscv_nacl_available()) + nacl_hfence_vvma_all(nacl_shmem(), vmid); + else kvm_riscv_local_hfence_vvma_all(vmid); + + *last_ran = vcpu->vcpu_idx; } void kvm_riscv_fence_i_process(struct kvm_vcpu *vcpu) diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c index 13c63ae1a..2907218a1 100644 --- a/arch/riscv/kvm/vm.c +++ b/arch/riscv/kvm/vm.c @@ -9,6 +9,7 @@ #include <linux/errno.h> #include <linux/err.h> #include <linux/module.h> +#include <linux/percpu.h> #include <linux/uaccess.h> #include <linux/kvm_host.h> #include <asm/kvm_mmu.h> @@ -30,7 +31,9 @@ const struct kvm_stats_header kvm_vm_stats_header = { int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) { + int *last_ran; int r; + int cpu; r = kvm_riscv_mmu_alloc_pgd(kvm); if (r) @@ -42,6 +45,17 @@ int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) return r; } + kvm->arch.last_vcpu_ran = alloc_percpu(int); + if (!kvm->arch.last_vcpu_ran) { + kvm_riscv_mmu_free_pgd(kvm); + return -ENOMEM; + } + + for_each_possible_cpu(cpu) { + last_ran = per_cpu_ptr(kvm->arch.last_vcpu_ran, cpu); + *last_ran = -1; + } + kvm_riscv_aia_init_vm(kvm); kvm_riscv_guest_timer_init(kvm); @@ -54,6 +68,8 @@ void kvm_arch_destroy_vm(struct kvm *kvm) kvm_destroy_vcpus(kvm); kvm_riscv_aia_destroy_vm(kvm); + + free_percpu(kvm->arch.last_vcpu_ran); } int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level *irql, -- 2.43.0 ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-08-17 10:24 [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU Yaxing Guo @ 2026-10-09 18:51 ` Anup Patel 2026-10-10 2:11 ` guoyaxing 0 siblings, 1 reply; 12+ messages in thread From: Anup Patel @ 2026-10-09 18:51 UTC (permalink / raw) To: Yaxing Guo Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: > > Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be > local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs > that previously backed a vCPU. > > This can miss stale VS-stage TLB entries when a vCPU migrates across host > CPUs. For example: > > - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. > - vcpu0 migrates to host CPU0. > - the guest updates the mapping on vcpu0 and issues a local sfence.vma. > - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() will ensure TLB entries associated with Guest/VM are nuked. By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA for the associated VMID because most RISC-V H-extension implemenation will tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both G-stage and VS-stage TLB entries for given VMID. If your HW does not tag VS-stage TLB enteries with VMID then it buggy / inefficient so you have to set kvm_riscv_vsstage_tlb_no_gpa in kvm_riscv_setup_vendor_features() as work-around. NACK to this patch from myside. Regards, Anup > > Host CPU1 did not observe the guest-local sfence.vma, and KVM can enter > vcpu1 with a stale VS-stage translation left behind by vcpu0. > > The reported failure was seen on a XiangShan RISC-V system. A bash task > ran on vcpu0 while vcpu0 was backed by host CPU1, and a read-only page was > prefetched into CPU1's VS-stage TLB. vcpu0 later migrated to host CPU0, > where the guest triggered COW, updated the mapping, and issued only a local > sfence.vma. The stale VS-stage entry on CPU1 survived. When the same guest > task later ran on another vCPU backed by host CPU1, it hit the stale > translation. The page contained a GOT pointer, and using the stale data led > to a NULL dereference and a userspace segmentation fault. > > The existing local TLB sanitize path flushes G-stage entries when a vCPU > migrates, and flushes VS-stage entries only for implementations selected by > the Andes-specific kvm_riscv_vsstage_tlb_no_gpa static key. That handles a > split two-stage TLB implementation where HFENCE.GVMA does not invalidate > VS-stage entries, but it does not cover the generic guest-local sfence.vma > case above. > > Track, per VM and per host CPU, the last vCPU that entered the guest on > that CPU. Before guest entry, flush the current CPU's VS-stage context for > the VM if either the current vCPU migrated since its last exit, or this > host CPU is switching from another vCPU of the same VM. Keep the existing > HFENCE.GVMA behavior tied to vCPU migration. > > This also subsumes the Andes split-TLB workaround. That workaround was > needed because the migration sanitize path previously relied on > HFENCE.GVMA alone for most implementations, and only issued HFENCE.VVMA > when kvm_riscv_vsstage_tlb_no_gpa was set. The new sanitize path always > issues HFENCE.VVMA when reusing a host CPU for a VM context that may have > missed a guest-local sfence.vma, so it no longer depends on HFENCE.GVMA > invalidating VS-stage entries. The implementation-specific static key and > setup hook are therefore no longer needed. > > Reported-by: Zhizun Wang <anzoso@outlook.com> > Reviewed-by: Jiuyue Ma <majiuyue@bosc.ac.cn> > Signed-off-by: Yaxing Guo <guoyaxing@bosc.ac.cn> > --- > arch/riscv/include/asm/kvm_host.h | 6 +++--- > arch/riscv/kvm/main.c | 15 --------------- > arch/riscv/kvm/tlb.c | 31 +++++++++++++++++++++++-------- > arch/riscv/kvm/vm.c | 16 ++++++++++++++++ > 4 files changed, 42 insertions(+), 26 deletions(-) > > diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/include/asm/kvm_host.h > index 24585304c..396ac6209 100644 > --- a/arch/riscv/include/asm/kvm_host.h > +++ b/arch/riscv/include/asm/kvm_host.h > @@ -91,6 +91,9 @@ struct kvm_arch { > /* G-stage vmid */ > struct kvm_vmid vmid; > > + /* Last VCPU that ran on each physical CPU */ > + int __percpu *last_vcpu_ran; > + > /* G-stage page table */ > pgd_t *pgd; > phys_addr_t pgd_phys; > @@ -330,7 +333,4 @@ bool kvm_riscv_vcpu_stopped(struct kvm_vcpu *vcpu); > > void kvm_riscv_vcpu_record_steal_time(struct kvm_vcpu *vcpu); > > -/* Flags representing implementation specific details */ > -DECLARE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); > - > #endif /* __RISCV_KVM_HOST_H__ */ > diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c > index 0f3fe3986..b1bbc0835 100644 > --- a/arch/riscv/kvm/main.c > +++ b/arch/riscv/kvm/main.c > @@ -10,23 +10,10 @@ > #include <linux/err.h> > #include <linux/module.h> > #include <linux/kvm_host.h> > -#include <asm/cpufeature.h> > #include <asm/kvm_mmu.h> > #include <asm/kvm_nacl.h> > #include <asm/sbi.h> > > -DEFINE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); > - > -static void kvm_riscv_setup_vendor_features(void) > -{ > - /* Andes AX66: split two-stage TLBs */ > - if (riscv_cached_mvendorid(0) == ANDES_VENDOR_ID && > - (riscv_cached_marchid(0) & 0xFFFF) == 0x8A66) { > - static_branch_enable(&kvm_riscv_vsstage_tlb_no_gpa); > - kvm_info("VS-stage TLB does not cache guest physical address and VMID\n"); > - } > -} > - > long kvm_arch_dev_ioctl(struct file *filp, > unsigned int ioctl, unsigned long arg) > { > @@ -172,8 +159,6 @@ static int __init riscv_kvm_init(void) > kvm_info("AIA available with %d guest external interrupts\n", > kvm_riscv_aia_nr_hgei); > > - kvm_riscv_setup_vendor_features(); > - > kvm_register_perf_callbacks(); > > rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE); > diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c > index ff1aeac4e..1541d059a 100644 > --- a/arch/riscv/kvm/tlb.c > +++ b/arch/riscv/kvm/tlb.c > @@ -8,6 +8,7 @@ > #include <linux/errno.h> > #include <linux/err.h> > #include <linux/module.h> > +#include <linux/percpu.h> > #include <linux/smp.h> > #include <linux/kvm_host.h> > #include <asm/cacheflush.h> > @@ -160,12 +161,19 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) > > void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > { > + bool vcpu_migrated; > + bool vcpu_switched; > unsigned long vmid; > + int *last_ran; > > - if (!kvm_riscv_gstage_vmid_bits() || > - vcpu->arch.last_exit_cpu == vcpu->cpu) > + last_ran = this_cpu_ptr(vcpu->kvm->arch.last_vcpu_ran); > + vcpu_migrated = (vcpu->arch.last_exit_cpu != vcpu->cpu); > + vcpu_switched = (*last_ran != vcpu->vcpu_idx); > + if (!vcpu_migrated && !vcpu_switched) > return; > > + vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); > + > /* > * On RISC-V platforms with hardware VMID support, we share same > * VMID for all VCPUs of a particular Guest/VM. This means we might > @@ -176,16 +184,23 @@ void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > * To cleanup stale TLB entries, we simply flush all G-stage TLB > * entries by VMID whenever underlying Host CPU changes for a VCPU. > */ > - > - vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); > - kvm_riscv_local_hfence_gvma_vmid_all(vmid); > + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) > + kvm_riscv_local_hfence_gvma_vmid_all(vmid); > > /* > - * Flush VS-stage TLB entries for implementation where VS-stage > - * TLB does not cahce guest physical address and VMID. > + * Guest-local sfence.vma only invalidates VS-stage translations on > + * the Host CPU currently backing the VCPU. If a VCPU migrates, or > + * if this Host CPU switches between VCPUs of the same VM, stale > + * VS-stage entries can be left behind on a Host CPU that missed a > + * guest-local flush. Flush the current CPU's VS-stage context before > + * entering the guest. > */ > - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) > + if (kvm_riscv_nacl_available()) > + nacl_hfence_vvma_all(nacl_shmem(), vmid); > + else > kvm_riscv_local_hfence_vvma_all(vmid); > + > + *last_ran = vcpu->vcpu_idx; > } > > void kvm_riscv_fence_i_process(struct kvm_vcpu *vcpu) > diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c > index 13c63ae1a..2907218a1 100644 > --- a/arch/riscv/kvm/vm.c > +++ b/arch/riscv/kvm/vm.c > @@ -9,6 +9,7 @@ > #include <linux/errno.h> > #include <linux/err.h> > #include <linux/module.h> > +#include <linux/percpu.h> > #include <linux/uaccess.h> > #include <linux/kvm_host.h> > #include <asm/kvm_mmu.h> > @@ -30,7 +31,9 @@ const struct kvm_stats_header kvm_vm_stats_header = { > > int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) > { > + int *last_ran; > int r; > + int cpu; > > r = kvm_riscv_mmu_alloc_pgd(kvm); > if (r) > @@ -42,6 +45,17 @@ int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) > return r; > } > > + kvm->arch.last_vcpu_ran = alloc_percpu(int); > + if (!kvm->arch.last_vcpu_ran) { > + kvm_riscv_mmu_free_pgd(kvm); > + return -ENOMEM; > + } > + > + for_each_possible_cpu(cpu) { > + last_ran = per_cpu_ptr(kvm->arch.last_vcpu_ran, cpu); > + *last_ran = -1; > + } > + > kvm_riscv_aia_init_vm(kvm); > > kvm_riscv_guest_timer_init(kvm); > @@ -54,6 +68,8 @@ void kvm_arch_destroy_vm(struct kvm *kvm) > kvm_destroy_vcpus(kvm); > > kvm_riscv_aia_destroy_vm(kvm); > + > + free_percpu(kvm->arch.last_vcpu_ran); > } > > int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level *irql, > -- > 2.43.0 > ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-09 18:51 ` Anup Patel @ 2026-10-10 2:11 ` guoyaxing 2026-10-10 7:50 ` Anup Patel 0 siblings, 1 reply; 12+ messages in thread From: guoyaxing @ 2026-10-10 2:11 UTC (permalink / raw) To: Anup Patel Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso 在 2026/10/10 02:51, Anup Patel 写道: > On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: >> >> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be >> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs >> that previously backed a vCPU. >> >> This can miss stale VS-stage TLB entries when a vCPU migrates across host >> CPUs. For example: >> >> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. >> - vcpu0 migrates to host CPU0. >> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. >> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. > > When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() > will ensure TLB entries associated with Guest/VM are nuked. > > By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA > for the associated VMID because most RISC-V H-extension implemenation will > tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both > G-stage and VS-stage TLB entries for given VMID. > > If your HW does not tag VS-stage TLB enteries with VMID then it buggy > / inefficient > so you have to set kvm_riscv_vsstage_tlb_no_gpa in > kvm_riscv_setup_vendor_features() > as work-around. > > NACK to this patch from myside. > > Regards, > Anup > Hi Anup, Thanks for the review and clarification. Regarding “When the vcpu0 is brought back to host CPU1”: the failure we hit is not caused by vcpu0 being brought back onto host CPU1. In our case, after vcpu0 has already migrated away, neither vCPU migrates again. The guest process is later scheduled inside the multi-vCPU guest onto vcpu1, which is still running on host CPU1. So the stale TLB is re-hit without any further KVM vCPU migration, and kvm_riscv_local_tlb_sanitize() is not involved on that path. My commit message was a bit too terse on this point. The concrete sequence we observed was described earlier in the RFC discussion: https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ In short, the issue is: because vcpu0 migrated away, cpu1 still has stale TLB entries while cpu0 has the new mapping. After that, neither vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 (which is on cpu1), causing it to hit the stale TLB. Consider the following scenario: 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is on cpu1, and a process on it reads a page, which gets cached in the TLB (let's call this the "stale TLB entry"). 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on cpu0) does a CoW (copy-on-write) and establishes a new mapping. 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the same page, and hits the stale TLB entry. So this is an intra-guest scheduling case onto a vCPU that stayed on the original host CPU, rather than a “vcpu0 returns to host CPU1” case. Thanks, Yaxing >> >> Host CPU1 did not observe the guest-local sfence.vma, and KVM can enter >> vcpu1 with a stale VS-stage translation left behind by vcpu0. >> >> The reported failure was seen on a XiangShan RISC-V system. A bash task >> ran on vcpu0 while vcpu0 was backed by host CPU1, and a read-only page was >> prefetched into CPU1's VS-stage TLB. vcpu0 later migrated to host CPU0, >> where the guest triggered COW, updated the mapping, and issued only a local >> sfence.vma. The stale VS-stage entry on CPU1 survived. When the same guest >> task later ran on another vCPU backed by host CPU1, it hit the stale >> translation. The page contained a GOT pointer, and using the stale data led >> to a NULL dereference and a userspace segmentation fault. >> >> The existing local TLB sanitize path flushes G-stage entries when a vCPU >> migrates, and flushes VS-stage entries only for implementations selected by >> the Andes-specific kvm_riscv_vsstage_tlb_no_gpa static key. That handles a >> split two-stage TLB implementation where HFENCE.GVMA does not invalidate >> VS-stage entries, but it does not cover the generic guest-local sfence.vma >> case above. >> >> Track, per VM and per host CPU, the last vCPU that entered the guest on >> that CPU. Before guest entry, flush the current CPU's VS-stage context for >> the VM if either the current vCPU migrated since its last exit, or this >> host CPU is switching from another vCPU of the same VM. Keep the existing >> HFENCE.GVMA behavior tied to vCPU migration. >> >> This also subsumes the Andes split-TLB workaround. That workaround was >> needed because the migration sanitize path previously relied on >> HFENCE.GVMA alone for most implementations, and only issued HFENCE.VVMA >> when kvm_riscv_vsstage_tlb_no_gpa was set. The new sanitize path always >> issues HFENCE.VVMA when reusing a host CPU for a VM context that may have >> missed a guest-local sfence.vma, so it no longer depends on HFENCE.GVMA >> invalidating VS-stage entries. The implementation-specific static key and >> setup hook are therefore no longer needed. >> >> Reported-by: Zhizun Wang <anzoso@outlook.com> >> Reviewed-by: Jiuyue Ma <majiuyue@bosc.ac.cn> >> Signed-off-by: Yaxing Guo <guoyaxing@bosc.ac.cn> >> --- >> arch/riscv/include/asm/kvm_host.h | 6 +++--- >> arch/riscv/kvm/main.c | 15 --------------- >> arch/riscv/kvm/tlb.c | 31 +++++++++++++++++++++++-------- >> arch/riscv/kvm/vm.c | 16 ++++++++++++++++ >> 4 files changed, 42 insertions(+), 26 deletions(-) >> >> diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/include/asm/kvm_host.h >> index 24585304c..396ac6209 100644 >> --- a/arch/riscv/include/asm/kvm_host.h >> +++ b/arch/riscv/include/asm/kvm_host.h >> @@ -91,6 +91,9 @@ struct kvm_arch { >> /* G-stage vmid */ >> struct kvm_vmid vmid; >> >> + /* Last VCPU that ran on each physical CPU */ >> + int __percpu *last_vcpu_ran; >> + >> /* G-stage page table */ >> pgd_t *pgd; >> phys_addr_t pgd_phys; >> @@ -330,7 +333,4 @@ bool kvm_riscv_vcpu_stopped(struct kvm_vcpu *vcpu); >> >> void kvm_riscv_vcpu_record_steal_time(struct kvm_vcpu *vcpu); >> >> -/* Flags representing implementation specific details */ >> -DECLARE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); >> - >> #endif /* __RISCV_KVM_HOST_H__ */ >> diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c >> index 0f3fe3986..b1bbc0835 100644 >> --- a/arch/riscv/kvm/main.c >> +++ b/arch/riscv/kvm/main.c >> @@ -10,23 +10,10 @@ >> #include <linux/err.h> >> #include <linux/module.h> >> #include <linux/kvm_host.h> >> -#include <asm/cpufeature.h> >> #include <asm/kvm_mmu.h> >> #include <asm/kvm_nacl.h> >> #include <asm/sbi.h> >> >> -DEFINE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); >> - >> -static void kvm_riscv_setup_vendor_features(void) >> -{ >> - /* Andes AX66: split two-stage TLBs */ >> - if (riscv_cached_mvendorid(0) == ANDES_VENDOR_ID && >> - (riscv_cached_marchid(0) & 0xFFFF) == 0x8A66) { >> - static_branch_enable(&kvm_riscv_vsstage_tlb_no_gpa); >> - kvm_info("VS-stage TLB does not cache guest physical address and VMID\n"); >> - } >> -} >> - >> long kvm_arch_dev_ioctl(struct file *filp, >> unsigned int ioctl, unsigned long arg) >> { >> @@ -172,8 +159,6 @@ static int __init riscv_kvm_init(void) >> kvm_info("AIA available with %d guest external interrupts\n", >> kvm_riscv_aia_nr_hgei); >> >> - kvm_riscv_setup_vendor_features(); >> - >> kvm_register_perf_callbacks(); >> >> rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE); >> diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c >> index ff1aeac4e..1541d059a 100644 >> --- a/arch/riscv/kvm/tlb.c >> +++ b/arch/riscv/kvm/tlb.c >> @@ -8,6 +8,7 @@ >> #include <linux/errno.h> >> #include <linux/err.h> >> #include <linux/module.h> >> +#include <linux/percpu.h> >> #include <linux/smp.h> >> #include <linux/kvm_host.h> >> #include <asm/cacheflush.h> >> @@ -160,12 +161,19 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) >> >> void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) >> { >> + bool vcpu_migrated; >> + bool vcpu_switched; >> unsigned long vmid; >> + int *last_ran; >> >> - if (!kvm_riscv_gstage_vmid_bits() || >> - vcpu->arch.last_exit_cpu == vcpu->cpu) >> + last_ran = this_cpu_ptr(vcpu->kvm->arch.last_vcpu_ran); >> + vcpu_migrated = (vcpu->arch.last_exit_cpu != vcpu->cpu); >> + vcpu_switched = (*last_ran != vcpu->vcpu_idx); >> + if (!vcpu_migrated && !vcpu_switched) >> return; >> >> + vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); >> + >> /* >> * On RISC-V platforms with hardware VMID support, we share same >> * VMID for all VCPUs of a particular Guest/VM. This means we might >> @@ -176,16 +184,23 @@ void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) >> * To cleanup stale TLB entries, we simply flush all G-stage TLB >> * entries by VMID whenever underlying Host CPU changes for a VCPU. >> */ >> - >> - vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); >> - kvm_riscv_local_hfence_gvma_vmid_all(vmid); >> + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) >> + kvm_riscv_local_hfence_gvma_vmid_all(vmid); >> >> /* >> - * Flush VS-stage TLB entries for implementation where VS-stage >> - * TLB does not cahce guest physical address and VMID. >> + * Guest-local sfence.vma only invalidates VS-stage translations on >> + * the Host CPU currently backing the VCPU. If a VCPU migrates, or >> + * if this Host CPU switches between VCPUs of the same VM, stale >> + * VS-stage entries can be left behind on a Host CPU that missed a >> + * guest-local flush. Flush the current CPU's VS-stage context before >> + * entering the guest. >> */ >> - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) >> + if (kvm_riscv_nacl_available()) >> + nacl_hfence_vvma_all(nacl_shmem(), vmid); >> + else >> kvm_riscv_local_hfence_vvma_all(vmid); >> + >> + *last_ran = vcpu->vcpu_idx; >> } >> >> void kvm_riscv_fence_i_process(struct kvm_vcpu *vcpu) >> diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c >> index 13c63ae1a..2907218a1 100644 >> --- a/arch/riscv/kvm/vm.c >> +++ b/arch/riscv/kvm/vm.c >> @@ -9,6 +9,7 @@ >> #include <linux/errno.h> >> #include <linux/err.h> >> #include <linux/module.h> >> +#include <linux/percpu.h> >> #include <linux/uaccess.h> >> #include <linux/kvm_host.h> >> #include <asm/kvm_mmu.h> >> @@ -30,7 +31,9 @@ const struct kvm_stats_header kvm_vm_stats_header = { >> >> int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) >> { >> + int *last_ran; >> int r; >> + int cpu; >> >> r = kvm_riscv_mmu_alloc_pgd(kvm); >> if (r) >> @@ -42,6 +45,17 @@ int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) >> return r; >> } >> >> + kvm->arch.last_vcpu_ran = alloc_percpu(int); >> + if (!kvm->arch.last_vcpu_ran) { >> + kvm_riscv_mmu_free_pgd(kvm); >> + return -ENOMEM; >> + } >> + >> + for_each_possible_cpu(cpu) { >> + last_ran = per_cpu_ptr(kvm->arch.last_vcpu_ran, cpu); >> + *last_ran = -1; >> + } >> + >> kvm_riscv_aia_init_vm(kvm); >> >> kvm_riscv_guest_timer_init(kvm); >> @@ -54,6 +68,8 @@ void kvm_arch_destroy_vm(struct kvm *kvm) >> kvm_destroy_vcpus(kvm); >> >> kvm_riscv_aia_destroy_vm(kvm); >> + >> + free_percpu(kvm->arch.last_vcpu_ran); >> } >> >> int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level *irql, >> -- >> 2.43.0 >> ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-10 2:11 ` guoyaxing @ 2026-10-10 7:50 ` Anup Patel 2026-10-10 9:06 ` guoyaxing 0 siblings, 1 reply; 12+ messages in thread From: Anup Patel @ 2026-10-10 7:50 UTC (permalink / raw) To: guoyaxing Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > > > > 在 2026/10/10 02:51, Anup Patel 写道: > > On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: > >> > >> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be > >> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs > >> that previously backed a vCPU. > >> > >> This can miss stale VS-stage TLB entries when a vCPU migrates across host > >> CPUs. For example: > >> > >> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. > >> - vcpu0 migrates to host CPU0. > >> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. > >> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. > > > > When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() > > will ensure TLB entries associated with Guest/VM are nuked. > > > > By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA > > for the associated VMID because most RISC-V H-extension implemenation will > > tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both > > G-stage and VS-stage TLB entries for given VMID. > > > > If your HW does not tag VS-stage TLB enteries with VMID then it buggy > > / inefficient > > so you have to set kvm_riscv_vsstage_tlb_no_gpa in > > kvm_riscv_setup_vendor_features() > > as work-around. > > > > NACK to this patch from myside. > > > > Regards, > > Anup > > > Hi Anup, > Thanks for the review and clarification. > > Regarding “When the vcpu0 is brought back to host CPU1”: the failure > we hit is not caused by vcpu0 being brought back onto host CPU1. In our > case, after vcpu0 has already migrated away, neither vCPU migrates > again. The guest process is later scheduled inside the multi-vCPU guest > onto vcpu1, which is still running on host CPU1. So the stale TLB is > re-hit without any further KVM vCPU migration, and > kvm_riscv_local_tlb_sanitize() is not involved on that path. > > My commit message was a bit too terse on this point. The concrete > sequence we observed was described earlier in the RFC discussion: > > > https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ > > In short, the issue is: because vcpu0 migrated away, cpu1 still has > stale TLB entries while cpu0 has the new mapping. After that, neither > vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 > (which is on cpu1), causing it to hit the stale TLB. Consider the > following scenario: > > 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is > on cpu1, and a process on it reads a page, which gets cached in the TLB > (let's call this the "stale TLB entry"). > > 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on > cpu0) does a CoW (copy-on-write) and establishes a new mapping. > > 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process > gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the > same page, and hits the stale TLB entry. This cannot happen because in step#2 when mapping was updated the Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 will also receive TLB flush and the stale TLB will be removed. This means when process is moved back to vcpu1 on cpu0, HW will fetch updated PTE and new TLB entry will be populated. > > So this is an intra-guest scheduling case onto a vCPU that stayed on > the original host CPU, rather than a “vcpu0 returns to host CPU1” case. My NACK still holds. Regards, Anup > > Thanks, > Yaxing > > >> > >> Host CPU1 did not observe the guest-local sfence.vma, and KVM can enter > >> vcpu1 with a stale VS-stage translation left behind by vcpu0. > >> > >> The reported failure was seen on a XiangShan RISC-V system. A bash task > >> ran on vcpu0 while vcpu0 was backed by host CPU1, and a read-only page was > >> prefetched into CPU1's VS-stage TLB. vcpu0 later migrated to host CPU0, > >> where the guest triggered COW, updated the mapping, and issued only a local > >> sfence.vma. The stale VS-stage entry on CPU1 survived. When the same guest > >> task later ran on another vCPU backed by host CPU1, it hit the stale > >> translation. The page contained a GOT pointer, and using the stale data led > >> to a NULL dereference and a userspace segmentation fault. > >> > >> The existing local TLB sanitize path flushes G-stage entries when a vCPU > >> migrates, and flushes VS-stage entries only for implementations selected by > >> the Andes-specific kvm_riscv_vsstage_tlb_no_gpa static key. That handles a > >> split two-stage TLB implementation where HFENCE.GVMA does not invalidate > >> VS-stage entries, but it does not cover the generic guest-local sfence.vma > >> case above. > >> > >> Track, per VM and per host CPU, the last vCPU that entered the guest on > >> that CPU. Before guest entry, flush the current CPU's VS-stage context for > >> the VM if either the current vCPU migrated since its last exit, or this > >> host CPU is switching from another vCPU of the same VM. Keep the existing > >> HFENCE.GVMA behavior tied to vCPU migration. > >> > >> This also subsumes the Andes split-TLB workaround. That workaround was > >> needed because the migration sanitize path previously relied on > >> HFENCE.GVMA alone for most implementations, and only issued HFENCE.VVMA > >> when kvm_riscv_vsstage_tlb_no_gpa was set. The new sanitize path always > >> issues HFENCE.VVMA when reusing a host CPU for a VM context that may have > >> missed a guest-local sfence.vma, so it no longer depends on HFENCE.GVMA > >> invalidating VS-stage entries. The implementation-specific static key and > >> setup hook are therefore no longer needed. > >> > >> Reported-by: Zhizun Wang <anzoso@outlook.com> > >> Reviewed-by: Jiuyue Ma <majiuyue@bosc.ac.cn> > >> Signed-off-by: Yaxing Guo <guoyaxing@bosc.ac.cn> > >> --- > >> arch/riscv/include/asm/kvm_host.h | 6 +++--- > >> arch/riscv/kvm/main.c | 15 --------------- > >> arch/riscv/kvm/tlb.c | 31 +++++++++++++++++++++++-------- > >> arch/riscv/kvm/vm.c | 16 ++++++++++++++++ > >> 4 files changed, 42 insertions(+), 26 deletions(-) > >> > >> diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/include/asm/kvm_host.h > >> index 24585304c..396ac6209 100644 > >> --- a/arch/riscv/include/asm/kvm_host.h > >> +++ b/arch/riscv/include/asm/kvm_host.h > >> @@ -91,6 +91,9 @@ struct kvm_arch { > >> /* G-stage vmid */ > >> struct kvm_vmid vmid; > >> > >> + /* Last VCPU that ran on each physical CPU */ > >> + int __percpu *last_vcpu_ran; > >> + > >> /* G-stage page table */ > >> pgd_t *pgd; > >> phys_addr_t pgd_phys; > >> @@ -330,7 +333,4 @@ bool kvm_riscv_vcpu_stopped(struct kvm_vcpu *vcpu); > >> > >> void kvm_riscv_vcpu_record_steal_time(struct kvm_vcpu *vcpu); > >> > >> -/* Flags representing implementation specific details */ > >> -DECLARE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); > >> - > >> #endif /* __RISCV_KVM_HOST_H__ */ > >> diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c > >> index 0f3fe3986..b1bbc0835 100644 > >> --- a/arch/riscv/kvm/main.c > >> +++ b/arch/riscv/kvm/main.c > >> @@ -10,23 +10,10 @@ > >> #include <linux/err.h> > >> #include <linux/module.h> > >> #include <linux/kvm_host.h> > >> -#include <asm/cpufeature.h> > >> #include <asm/kvm_mmu.h> > >> #include <asm/kvm_nacl.h> > >> #include <asm/sbi.h> > >> > >> -DEFINE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); > >> - > >> -static void kvm_riscv_setup_vendor_features(void) > >> -{ > >> - /* Andes AX66: split two-stage TLBs */ > >> - if (riscv_cached_mvendorid(0) == ANDES_VENDOR_ID && > >> - (riscv_cached_marchid(0) & 0xFFFF) == 0x8A66) { > >> - static_branch_enable(&kvm_riscv_vsstage_tlb_no_gpa); > >> - kvm_info("VS-stage TLB does not cache guest physical address and VMID\n"); > >> - } > >> -} > >> - > >> long kvm_arch_dev_ioctl(struct file *filp, > >> unsigned int ioctl, unsigned long arg) > >> { > >> @@ -172,8 +159,6 @@ static int __init riscv_kvm_init(void) > >> kvm_info("AIA available with %d guest external interrupts\n", > >> kvm_riscv_aia_nr_hgei); > >> > >> - kvm_riscv_setup_vendor_features(); > >> - > >> kvm_register_perf_callbacks(); > >> > >> rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE); > >> diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c > >> index ff1aeac4e..1541d059a 100644 > >> --- a/arch/riscv/kvm/tlb.c > >> +++ b/arch/riscv/kvm/tlb.c > >> @@ -8,6 +8,7 @@ > >> #include <linux/errno.h> > >> #include <linux/err.h> > >> #include <linux/module.h> > >> +#include <linux/percpu.h> > >> #include <linux/smp.h> > >> #include <linux/kvm_host.h> > >> #include <asm/cacheflush.h> > >> @@ -160,12 +161,19 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) > >> > >> void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > >> { > >> + bool vcpu_migrated; > >> + bool vcpu_switched; > >> unsigned long vmid; > >> + int *last_ran; > >> > >> - if (!kvm_riscv_gstage_vmid_bits() || > >> - vcpu->arch.last_exit_cpu == vcpu->cpu) > >> + last_ran = this_cpu_ptr(vcpu->kvm->arch.last_vcpu_ran); > >> + vcpu_migrated = (vcpu->arch.last_exit_cpu != vcpu->cpu); > >> + vcpu_switched = (*last_ran != vcpu->vcpu_idx); > >> + if (!vcpu_migrated && !vcpu_switched) > >> return; > >> > >> + vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); > >> + > >> /* > >> * On RISC-V platforms with hardware VMID support, we share same > >> * VMID for all VCPUs of a particular Guest/VM. This means we might > >> @@ -176,16 +184,23 @@ void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > >> * To cleanup stale TLB entries, we simply flush all G-stage TLB > >> * entries by VMID whenever underlying Host CPU changes for a VCPU. > >> */ > >> - > >> - vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); > >> - kvm_riscv_local_hfence_gvma_vmid_all(vmid); > >> + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) > >> + kvm_riscv_local_hfence_gvma_vmid_all(vmid); > >> > >> /* > >> - * Flush VS-stage TLB entries for implementation where VS-stage > >> - * TLB does not cahce guest physical address and VMID. > >> + * Guest-local sfence.vma only invalidates VS-stage translations on > >> + * the Host CPU currently backing the VCPU. If a VCPU migrates, or > >> + * if this Host CPU switches between VCPUs of the same VM, stale > >> + * VS-stage entries can be left behind on a Host CPU that missed a > >> + * guest-local flush. Flush the current CPU's VS-stage context before > >> + * entering the guest. > >> */ > >> - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) > >> + if (kvm_riscv_nacl_available()) > >> + nacl_hfence_vvma_all(nacl_shmem(), vmid); > >> + else > >> kvm_riscv_local_hfence_vvma_all(vmid); > >> + > >> + *last_ran = vcpu->vcpu_idx; > >> } > >> > >> void kvm_riscv_fence_i_process(struct kvm_vcpu *vcpu) > >> diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c > >> index 13c63ae1a..2907218a1 100644 > >> --- a/arch/riscv/kvm/vm.c > >> +++ b/arch/riscv/kvm/vm.c > >> @@ -9,6 +9,7 @@ > >> #include <linux/errno.h> > >> #include <linux/err.h> > >> #include <linux/module.h> > >> +#include <linux/percpu.h> > >> #include <linux/uaccess.h> > >> #include <linux/kvm_host.h> > >> #include <asm/kvm_mmu.h> > >> @@ -30,7 +31,9 @@ const struct kvm_stats_header kvm_vm_stats_header = { > >> > >> int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) > >> { > >> + int *last_ran; > >> int r; > >> + int cpu; > >> > >> r = kvm_riscv_mmu_alloc_pgd(kvm); > >> if (r) > >> @@ -42,6 +45,17 @@ int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) > >> return r; > >> } > >> > >> + kvm->arch.last_vcpu_ran = alloc_percpu(int); > >> + if (!kvm->arch.last_vcpu_ran) { > >> + kvm_riscv_mmu_free_pgd(kvm); > >> + return -ENOMEM; > >> + } > >> + > >> + for_each_possible_cpu(cpu) { > >> + last_ran = per_cpu_ptr(kvm->arch.last_vcpu_ran, cpu); > >> + *last_ran = -1; > >> + } > >> + > >> kvm_riscv_aia_init_vm(kvm); > >> > >> kvm_riscv_guest_timer_init(kvm); > >> @@ -54,6 +68,8 @@ void kvm_arch_destroy_vm(struct kvm *kvm) > >> kvm_destroy_vcpus(kvm); > >> > >> kvm_riscv_aia_destroy_vm(kvm); > >> + > >> + free_percpu(kvm->arch.last_vcpu_ran); > >> } > >> > >> int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level *irql, > >> -- > >> 2.43.0 > >> > ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-10 7:50 ` Anup Patel @ 2026-10-10 9:06 ` guoyaxing 2026-10-21 9:32 ` BillXiang 2026-10-24 15:49 ` Anup Patel 0 siblings, 2 replies; 12+ messages in thread From: guoyaxing @ 2026-10-10 9:06 UTC (permalink / raw) To: Anup Patel Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso 在 2026/10/10 15:50, Anup Patel 写道: > On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >> >> >> >> 在 2026/10/10 02:51, Anup Patel 写道: >>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: >>>> >>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be >>>> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs >>>> that previously backed a vCPU. >>>> >>>> This can miss stale VS-stage TLB entries when a vCPU migrates across host >>>> CPUs. For example: >>>> >>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. >>>> - vcpu0 migrates to host CPU0. >>>> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. >>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. >>> >>> When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() >>> will ensure TLB entries associated with Guest/VM are nuked. >>> >>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA >>> for the associated VMID because most RISC-V H-extension implemenation will >>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both >>> G-stage and VS-stage TLB entries for given VMID. >>> >>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy >>> / inefficient >>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in >>> kvm_riscv_setup_vendor_features() >>> as work-around. >>> >>> NACK to this patch from myside. >>> >>> Regards, >>> Anup >>> >> Hi Anup, >> Thanks for the review and clarification. >> >> Regarding “When the vcpu0 is brought back to host CPU1”: the failure >> we hit is not caused by vcpu0 being brought back onto host CPU1. In our >> case, after vcpu0 has already migrated away, neither vCPU migrates >> again. The guest process is later scheduled inside the multi-vCPU guest >> onto vcpu1, which is still running on host CPU1. So the stale TLB is >> re-hit without any further KVM vCPU migration, and >> kvm_riscv_local_tlb_sanitize() is not involved on that path. >> >> My commit message was a bit too terse on this point. The concrete >> sequence we observed was described earlier in the RFC discussion: >> >> >> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ >> >> In short, the issue is: because vcpu0 migrated away, cpu1 still has >> stale TLB entries while cpu0 has the new mapping. After that, neither >> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 >> (which is on cpu1), causing it to hit the stale TLB. Consider the >> following scenario: >> >> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is >> on cpu1, and a process on it reads a page, which gets cached in the TLB >> (let's call this the "stale TLB entry"). >> >> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on >> cpu0) does a CoW (copy-on-write) and establishes a new mapping. >> >> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process >> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the >> same page, and hits the stale TLB entry. > > This cannot happen because in step#2 when mapping was updated the > Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 > will also receive TLB flush and the stale TLB will be removed. This means > when process is moved back to vcpu1 on cpu0, HW will fetch updated > PTE and new TLB entry will be populated. > Hi Anup, Thanks for the follow-up. I may be missing something, so I wanted to double-check this against the guest TLB shootdown path. If I follow the Linux guest CoW path correctly, when the mapping is updated in step#2 it goes through: wp_page_copy() -> ptep_clear_flush() -> flush_tlb_page() -> __flush_tlb_range(mm, mm_cpumask(mm), ...) So the remote SFENCE.VMA appears to be targeted only at CPUs in mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is updated in switch_mm()/set_mm() for guest CPUs that have actually run that mm. In our case, before the CoW the process had only ever run on vcpu0. From the guest's point of view it did not migrate: the host migration of vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is currently running. The stale VS-stage TLB entry left on host cpu1 from when vcpu0 previously ran there would then remain. Later, still without any further vCPU migration, the process is scheduled inside the guest onto vcpu1, which remains on host cpu1, and can hit that stale entry. This also seems more likely when the ASID allocator is enabled, because set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So even the first time the process lands on vcpu1, a stale ASID-tagged entry on host cpu1 may still be present. In other words, I am not sure this case is covered by a full guest TLB flush to all VCPUs on the mapping update. Guest shootdown looks mm_cpumask-based, and host pCPU migration of a vCPU does not by itself add other vCPUs that never ran the mm into that mask. The earlier RFC note has a bit more detail on the flow we observed: https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ Please let me know if I misunderstood the intended invalidation path here. Thanks, Yaxing >> >> So this is an intra-guest scheduling case onto a vCPU that stayed on >> the original host CPU, rather than a “vcpu0 returns to host CPU1” case. > > My NACK still holds. > > Regards, > Anup > >> >> Thanks, >> Yaxing >> >>>> >>>> Host CPU1 did not observe the guest-local sfence.vma, and KVM can enter >>>> vcpu1 with a stale VS-stage translation left behind by vcpu0. >>>> >>>> The reported failure was seen on a XiangShan RISC-V system. A bash task >>>> ran on vcpu0 while vcpu0 was backed by host CPU1, and a read-only page was >>>> prefetched into CPU1's VS-stage TLB. vcpu0 later migrated to host CPU0, >>>> where the guest triggered COW, updated the mapping, and issued only a local >>>> sfence.vma. The stale VS-stage entry on CPU1 survived. When the same guest >>>> task later ran on another vCPU backed by host CPU1, it hit the stale >>>> translation. The page contained a GOT pointer, and using the stale data led >>>> to a NULL dereference and a userspace segmentation fault. >>>> >>>> The existing local TLB sanitize path flushes G-stage entries when a vCPU >>>> migrates, and flushes VS-stage entries only for implementations selected by >>>> the Andes-specific kvm_riscv_vsstage_tlb_no_gpa static key. That handles a >>>> split two-stage TLB implementation where HFENCE.GVMA does not invalidate >>>> VS-stage entries, but it does not cover the generic guest-local sfence.vma >>>> case above. >>>> >>>> Track, per VM and per host CPU, the last vCPU that entered the guest on >>>> that CPU. Before guest entry, flush the current CPU's VS-stage context for >>>> the VM if either the current vCPU migrated since its last exit, or this >>>> host CPU is switching from another vCPU of the same VM. Keep the existing >>>> HFENCE.GVMA behavior tied to vCPU migration. >>>> >>>> This also subsumes the Andes split-TLB workaround. That workaround was >>>> needed because the migration sanitize path previously relied on >>>> HFENCE.GVMA alone for most implementations, and only issued HFENCE.VVMA >>>> when kvm_riscv_vsstage_tlb_no_gpa was set. The new sanitize path always >>>> issues HFENCE.VVMA when reusing a host CPU for a VM context that may have >>>> missed a guest-local sfence.vma, so it no longer depends on HFENCE.GVMA >>>> invalidating VS-stage entries. The implementation-specific static key and >>>> setup hook are therefore no longer needed. >>>> >>>> Reported-by: Zhizun Wang <anzoso@outlook.com> >>>> Reviewed-by: Jiuyue Ma <majiuyue@bosc.ac.cn> >>>> Signed-off-by: Yaxing Guo <guoyaxing@bosc.ac.cn> >>>> --- >>>> arch/riscv/include/asm/kvm_host.h | 6 +++--- >>>> arch/riscv/kvm/main.c | 15 --------------- >>>> arch/riscv/kvm/tlb.c | 31 +++++++++++++++++++++++-------- >>>> arch/riscv/kvm/vm.c | 16 ++++++++++++++++ >>>> 4 files changed, 42 insertions(+), 26 deletions(-) >>>> >>>> diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/include/asm/kvm_host.h >>>> index 24585304c..396ac6209 100644 >>>> --- a/arch/riscv/include/asm/kvm_host.h >>>> +++ b/arch/riscv/include/asm/kvm_host.h >>>> @@ -91,6 +91,9 @@ struct kvm_arch { >>>> /* G-stage vmid */ >>>> struct kvm_vmid vmid; >>>> >>>> + /* Last VCPU that ran on each physical CPU */ >>>> + int __percpu *last_vcpu_ran; >>>> + >>>> /* G-stage page table */ >>>> pgd_t *pgd; >>>> phys_addr_t pgd_phys; >>>> @@ -330,7 +333,4 @@ bool kvm_riscv_vcpu_stopped(struct kvm_vcpu *vcpu); >>>> >>>> void kvm_riscv_vcpu_record_steal_time(struct kvm_vcpu *vcpu); >>>> >>>> -/* Flags representing implementation specific details */ >>>> -DECLARE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); >>>> - >>>> #endif /* __RISCV_KVM_HOST_H__ */ >>>> diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c >>>> index 0f3fe3986..b1bbc0835 100644 >>>> --- a/arch/riscv/kvm/main.c >>>> +++ b/arch/riscv/kvm/main.c >>>> @@ -10,23 +10,10 @@ >>>> #include <linux/err.h> >>>> #include <linux/module.h> >>>> #include <linux/kvm_host.h> >>>> -#include <asm/cpufeature.h> >>>> #include <asm/kvm_mmu.h> >>>> #include <asm/kvm_nacl.h> >>>> #include <asm/sbi.h> >>>> >>>> -DEFINE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); >>>> - >>>> -static void kvm_riscv_setup_vendor_features(void) >>>> -{ >>>> - /* Andes AX66: split two-stage TLBs */ >>>> - if (riscv_cached_mvendorid(0) == ANDES_VENDOR_ID && >>>> - (riscv_cached_marchid(0) & 0xFFFF) == 0x8A66) { >>>> - static_branch_enable(&kvm_riscv_vsstage_tlb_no_gpa); >>>> - kvm_info("VS-stage TLB does not cache guest physical address and VMID\n"); >>>> - } >>>> -} >>>> - >>>> long kvm_arch_dev_ioctl(struct file *filp, >>>> unsigned int ioctl, unsigned long arg) >>>> { >>>> @@ -172,8 +159,6 @@ static int __init riscv_kvm_init(void) >>>> kvm_info("AIA available with %d guest external interrupts\n", >>>> kvm_riscv_aia_nr_hgei); >>>> >>>> - kvm_riscv_setup_vendor_features(); >>>> - >>>> kvm_register_perf_callbacks(); >>>> >>>> rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE); >>>> diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c >>>> index ff1aeac4e..1541d059a 100644 >>>> --- a/arch/riscv/kvm/tlb.c >>>> +++ b/arch/riscv/kvm/tlb.c >>>> @@ -8,6 +8,7 @@ >>>> #include <linux/errno.h> >>>> #include <linux/err.h> >>>> #include <linux/module.h> >>>> +#include <linux/percpu.h> >>>> #include <linux/smp.h> >>>> #include <linux/kvm_host.h> >>>> #include <asm/cacheflush.h> >>>> @@ -160,12 +161,19 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) >>>> >>>> void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) >>>> { >>>> + bool vcpu_migrated; >>>> + bool vcpu_switched; >>>> unsigned long vmid; >>>> + int *last_ran; >>>> >>>> - if (!kvm_riscv_gstage_vmid_bits() || >>>> - vcpu->arch.last_exit_cpu == vcpu->cpu) >>>> + last_ran = this_cpu_ptr(vcpu->kvm->arch.last_vcpu_ran); >>>> + vcpu_migrated = (vcpu->arch.last_exit_cpu != vcpu->cpu); >>>> + vcpu_switched = (*last_ran != vcpu->vcpu_idx); >>>> + if (!vcpu_migrated && !vcpu_switched) >>>> return; >>>> >>>> + vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); >>>> + >>>> /* >>>> * On RISC-V platforms with hardware VMID support, we share same >>>> * VMID for all VCPUs of a particular Guest/VM. This means we might >>>> @@ -176,16 +184,23 @@ void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) >>>> * To cleanup stale TLB entries, we simply flush all G-stage TLB >>>> * entries by VMID whenever underlying Host CPU changes for a VCPU. >>>> */ >>>> - >>>> - vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); >>>> - kvm_riscv_local_hfence_gvma_vmid_all(vmid); >>>> + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) >>>> + kvm_riscv_local_hfence_gvma_vmid_all(vmid); >>>> >>>> /* >>>> - * Flush VS-stage TLB entries for implementation where VS-stage >>>> - * TLB does not cahce guest physical address and VMID. >>>> + * Guest-local sfence.vma only invalidates VS-stage translations on >>>> + * the Host CPU currently backing the VCPU. If a VCPU migrates, or >>>> + * if this Host CPU switches between VCPUs of the same VM, stale >>>> + * VS-stage entries can be left behind on a Host CPU that missed a >>>> + * guest-local flush. Flush the current CPU's VS-stage context before >>>> + * entering the guest. >>>> */ >>>> - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) >>>> + if (kvm_riscv_nacl_available()) >>>> + nacl_hfence_vvma_all(nacl_shmem(), vmid); >>>> + else >>>> kvm_riscv_local_hfence_vvma_all(vmid); >>>> + >>>> + *last_ran = vcpu->vcpu_idx; >>>> } >>>> >>>> void kvm_riscv_fence_i_process(struct kvm_vcpu *vcpu) >>>> diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c >>>> index 13c63ae1a..2907218a1 100644 >>>> --- a/arch/riscv/kvm/vm.c >>>> +++ b/arch/riscv/kvm/vm.c >>>> @@ -9,6 +9,7 @@ >>>> #include <linux/errno.h> >>>> #include <linux/err.h> >>>> #include <linux/module.h> >>>> +#include <linux/percpu.h> >>>> #include <linux/uaccess.h> >>>> #include <linux/kvm_host.h> >>>> #include <asm/kvm_mmu.h> >>>> @@ -30,7 +31,9 @@ const struct kvm_stats_header kvm_vm_stats_header = { >>>> >>>> int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) >>>> { >>>> + int *last_ran; >>>> int r; >>>> + int cpu; >>>> >>>> r = kvm_riscv_mmu_alloc_pgd(kvm); >>>> if (r) >>>> @@ -42,6 +45,17 @@ int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) >>>> return r; >>>> } >>>> >>>> + kvm->arch.last_vcpu_ran = alloc_percpu(int); >>>> + if (!kvm->arch.last_vcpu_ran) { >>>> + kvm_riscv_mmu_free_pgd(kvm); >>>> + return -ENOMEM; >>>> + } >>>> + >>>> + for_each_possible_cpu(cpu) { >>>> + last_ran = per_cpu_ptr(kvm->arch.last_vcpu_ran, cpu); >>>> + *last_ran = -1; >>>> + } >>>> + >>>> kvm_riscv_aia_init_vm(kvm); >>>> >>>> kvm_riscv_guest_timer_init(kvm); >>>> @@ -54,6 +68,8 @@ void kvm_arch_destroy_vm(struct kvm *kvm) >>>> kvm_destroy_vcpus(kvm); >>>> >>>> kvm_riscv_aia_destroy_vm(kvm); >>>> + >>>> + free_percpu(kvm->arch.last_vcpu_ran); >>>> } >>>> >>>> int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level *irql, >>>> -- >>>> 2.43.0 >>>> >> ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-10 9:06 ` guoyaxing @ 2026-10-21 9:32 ` BillXiang 2026-10-24 15:49 ` Anup Patel 1 sibling, 0 replies; 12+ messages in thread From: BillXiang @ 2026-10-21 9:32 UTC (permalink / raw) To: guoyaxing, Anup Patel Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso On 10/10/2026 5:06 PM, guoyaxing wrote: > > > 在 2026/10/10 15:50, Anup Patel 写道: >> On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >>> >>> >>> >>> 在 2026/10/10 02:51, Anup Patel 写道: >>>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> >>>> wrote: >>>>> >>>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs >>>>> to be >>>>> local or remote. Under KVM that mask tracks guest CPUs, not the >>>>> host CPUs >>>>> that previously backed a vCPU. >>>>> >>>>> This can miss stale VS-stage TLB entries when a vCPU migrates >>>>> across host >>>>> CPUs. For example: >>>>> >>>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. >>>>> - vcpu0 migrates to host CPU0. >>>>> - the guest updates the mapping on vcpu0 and issues a local >>>>> sfence.vma. >>>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by >>>>> host CPU1. >>>> >>>> When the vcpu0 is brought back to host CPU1, the >>>> kvm_riscv_local_tlb_sanitize() >>>> will ensure TLB entries associated with Guest/VM are nuked. >>>> >>>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do >>>> HFENCE.GVMA >>>> for the associated VMID because most RISC-V H-extension >>>> implemenation will >>>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both >>>> G-stage and VS-stage TLB entries for given VMID. >>>> >>>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy >>>> / inefficient >>>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in >>>> kvm_riscv_setup_vendor_features() >>>> as work-around. >>>> >>>> NACK to this patch from myside. >>>> >>>> Regards, >>>> Anup >>>> >>> Hi Anup, >>> Thanks for the review and clarification. >>> >>> Regarding “When the vcpu0 is brought back to host CPU1”: the failure >>> we hit is not caused by vcpu0 being brought back onto host CPU1. In our >>> case, after vcpu0 has already migrated away, neither vCPU migrates >>> again. The guest process is later scheduled inside the multi-vCPU guest >>> onto vcpu1, which is still running on host CPU1. So the stale TLB is >>> re-hit without any further KVM vCPU migration, and >>> kvm_riscv_local_tlb_sanitize() is not involved on that path. >>> >>> My commit message was a bit too terse on this point. The concrete >>> sequence we observed was described earlier in the RFC discussion: >>> >>> >>> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33- >>> e8c49c2331e1@bosc.ac.cn/ >>> >>> In short, the issue is: because vcpu0 migrated away, cpu1 still has >>> stale TLB entries while cpu0 has the new mapping. After that, neither >>> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 >>> (which is on cpu1), causing it to hit the stale TLB. Consider the >>> following scenario: >>> >>> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is >>> on cpu1, and a process on it reads a page, which gets cached in the TLB >>> (let's call this the "stale TLB entry"). >>> >>> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on >>> cpu0) does a CoW (copy-on-write) and establishes a new mapping. >>> >>> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the >>> process >>> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the >>> same page, and hits the stale TLB entry. >> >> This cannot happen because in step#2 when mapping was updated the >> Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 >> will also receive TLB flush and the stale TLB will be removed. This means >> when process is moved back to vcpu1 on cpu0, HW will fetch updated >> PTE and new TLB entry will be populated. >> > > Hi Anup, > > Thanks for the follow-up. I may be missing something, so I wanted to > double-check this against the guest TLB shootdown path. > > If I follow the Linux guest CoW path correctly, when the mapping is > updated in step#2 it goes through: > > wp_page_copy() > -> ptep_clear_flush() > -> flush_tlb_page() > -> __flush_tlb_range(mm, mm_cpumask(mm), ...) > > So the remote SFENCE.VMA appears to be targeted only at CPUs in > mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is > updated in switch_mm()/set_mm() for guest CPUs that have actually run > that mm. > > In our case, before the CoW the process had only ever run on vcpu0. From > the guest's point of view it did not migrate: the host migration of > vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees > vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least > does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. > The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is > currently running. The stale VS-stage TLB entry left on host cpu1 from > when vcpu0 previously ran there would then remain. > > Later, still without any further vCPU migration, the process is > scheduled inside the guest onto vcpu1, which remains on host cpu1, and > can hit that stale entry. > > This also seems more likely when the ASID allocator is enabled, because > set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So > even the first time the process lands on vcpu1, a stale ASID-tagged > entry on host cpu1 may still be present. > > In other words, I am not sure this case is covered by a full guest TLB > flush to all VCPUs on the mapping update. Guest shootdown looks > mm_cpumask-based, and host pCPU migration of a vCPU does not by itself > add other vCPUs that never ran the mm into that mask. > > The earlier RFC note has a bit more detail on the flow we observed: > https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33- > e8c49c2331e1@bosc.ac.cn/ > > Please let me know if I misunderstood the intended invalidation path here. > > Thanks, > Yaxing > Hi Yaxing, Thanks for your work. To help review and validate this patch, I am sharing the two programs below, which were generated with the help of an AI assistant. I reviewed the code and ran it on my test system: without this patch the reproducer triggers the stale VS-stage TLB entry, and with the patch applied it no longer does. The test consists of two cooperating components: - a host-side controller that discovers the QEMU vCPU thread TIDs, pins them to specific host CPUs with taskset(1), and synchronizes with the guest program over TCP; - a guest-side program that performs the memory operations that populate and then probe the VS-stage TLB entries. Start the host controller: sudo python3 vsstage_tlb_stale_host.py --qemu-pid <QEMU_PID> Build and run the guest program inside the guest: gcc -O0 -o vsstage_tlb_stale_guest vsstage_tlb_stale_guest.c ./vsstage_tlb_stale_guest The guest program connects to the host controller on TCP port 12345 by default. With QEMU user networking, the host is reachable from the guest at 10.0.2.2, and the guest program uses that address by default. Both the host controller and the guest program can be wrapped in a shell while loop to reproduce the bug repeatedly. Regards, Bill --- 8< --- vsstage_tlb_stale_host.py --- 8< --- #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 """ RISC-V KVM stale VS-stage TLB reproducer (Host controller) Scenario: Initially both vCPU0 and vCPU1 are pinned to host CPU1. After phase 1, vCPU0 is migrated to host CPU0. After phase 2, vCPU1 stays where it is (host CPU1). Phase 3 runs on vCPU1 and checks for a stale TLB entry. Usage: sudo python3 auto_test.py --qemu-pid <QEMU_PID> \ [--port 12345] [--host-cpu0 0] [--host-cpu1 1] """ import argparse import glob import os import re import socket import subprocess import sys import time # ---------------- vCPU TID discovery ---------------- VCPU_RE = re.compile(r"^(?:CPU\s+(\d+)/KVM|vcpu(\d+))$") def find_vcpu_tids(qemu_pid): """Identify vCPU threads under /proc/<pid>/task. Returns {index: tid}.""" result = {} for task_dir in glob.glob(f"/proc/{qemu_pid}/task/*"): try: comm = open(f"{task_dir}/comm").read().strip() except OSError: continue m = VCPU_RE.match(comm) if not m: continue idx = int(m.group(1) if m.group(1) is not None else m.group(2)) tid = int(os.path.basename(task_dir)) result[idx] = tid return result # ---------------- CPU affinity ---------------- def set_affinity(tid, cpu): subprocess.run(["taskset", "-pc", str(cpu), str(tid)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def get_affinity(tid): try: out = subprocess.check_output( ["taskset", "-pc", str(tid)], stderr=subprocess.DEVNULL ).decode() return out.strip().split(":")[-1].strip() except subprocess.CalledProcessError: return "?" def online_host_cpus(): cpus = [] try: raw = open("/sys/devices/system/cpu/online").read().strip() except OSError: sys.exit("Failed to read /sys/devices/system/cpu/online") for item in raw.split(","): if "-" in item: a, b = map(int, item.split("-")) cpus.extend(range(a, b + 1)) else: cpus.append(int(item)) return cpus # ---------------- TCP communication ---------------- def recv_line(conn): buf = b"" while not buf.endswith(b"\n"): chunk = conn.recv(1) if not chunk: raise EOFError("guest disconnected") buf += chunk return buf.decode().strip() def send_line(conn, msg): conn.sendall((msg + "\n").encode()) # ---------------- Main flow ---------------- def main(): ap = argparse.ArgumentParser() ap.add_argument("--qemu-pid", type=int, required=True, help="QEMU process PID") ap.add_argument("--port", type=int, default=12345) ap.add_argument("--host-cpu0", type=int, default=0, help="Target host CPU for migrating vCPU0 (default 0)") ap.add_argument("--host-cpu1", type=int, default=1, help="Initial shared host CPU, where vCPU1 stays (default 1)") ap.add_argument("--settle-ms", type=int, default=500, help="Wait time after each migration (milliseconds)") args = ap.parse_args() cpu0 = args.host_cpu0 cpu1 = args.host_cpu1 if cpu0 == cpu1: sys.exit("--host-cpu0 and --host-cpu1 must differ") online = online_host_cpus() for c in (cpu0, cpu1): if c not in online: sys.exit(f"Host CPU {c} is offline, online CPUs: {online}") # ---- 1. Locate vCPU TIDs ---- vcpus = find_vcpu_tids(args.qemu_pid) print(f"[Host] Found vCPU threads: {vcpus}") if 0 not in vcpus or 1 not in vcpus: sys.exit("vCPU0/vCPU1 threads not found; check the QEMU PID and " "vCPU thread naming") # ---- 2. Initial pinning: both vCPU0 and vCPU1 on host CPU1 ---- print(f"[Host] Initial pinning: vCPU0 -> CPU {cpu1}, vCPU1 -> CPU {cpu1}") set_affinity(vcpus[0], cpu1) set_affinity(vcpus[1], cpu1) time.sleep(0.3) print(f"[Host] vCPU0 affinity = {get_affinity(vcpus[0])}") print(f"[Host] vCPU1 affinity = {get_affinity(vcpus[1])}") # ---- 3. Listen for the guest ---- srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", args.port)) srv.listen(1) print(f"[Host] Listening on 0.0.0.0:{args.port}, waiting for guest ...") conn, addr = srv.accept() conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) print(f"[Host] Guest connected: {addr}") try: # ---- 4. Handshake ---- msg = recv_line(conn) if msg != "HELLO": sys.exit(f"Handshake failed, received: {msg}") send_line(conn, "GO_PHASE1") # ---- 5. Phase 1 done -> migrate vCPU0 from CPU1 to CPU0 ---- msg = recv_line(conn) if msg != "PHASE1_DONE": sys.exit(f"Expected PHASE1_DONE, received: {msg}") print(f"[Host] Phase 1 done; migrating vCPU0: CPU {cpu1} -> CPU {cpu0}") set_affinity(vcpus[0], cpu0) time.sleep(args.settle_ms / 1000.0) print(f"[Host] vCPU0 affinity = {get_affinity(vcpus[0])}") send_line(conn, "MIGRATED0") # ---- 6. Phase 2 done -> confirm vCPU1 stays on CPU1 ---- msg = recv_line(conn) if msg != "PHASE2_DONE": sys.exit(f"Expected PHASE2_DONE, received: {msg}") print(f"[Host] Phase 2 done; confirming vCPU1 is still on CPU {cpu1}, " f"no migration") print(f"[Host] vCPU1 affinity = {get_affinity(vcpus[1])}") send_line(conn, "MIGRATED1") # ---- 7. Collect the result ---- msg = recv_line(conn) print(f"[Host] Received result: {msg}") if msg == "RESULT:0x0": print() print("=" * 55) print(" *** BUG REPRODUCED *** (read the stale value 0x0)") print("=" * 55) rc = 0 elif msg == "RESULT:0xcafebabe": print() print("=" * 55) print(" Not reproduced (read the new value)") print("=" * 55) rc = 1 else: print(f" Unexpected result: {msg}") rc = 2 finally: conn.close() srv.close() sys.exit(rc) if __name__ == "__main__": main() --- 8< --- vsstage_tlb_stale_guest.c --- 8< --- // SPDX-License-Identifier: GPL-2.0 /* * RISC-V KVM stale VS-stage TLB entry reproducer (Guest side, automated) * * Scenario: * Initially both vCPU0 and vCPU1 are pinned to host CPU1. * Phase 1: the task runs on vCPU0 (host CPU1) and reads the shared page, * populating a stale VS-stage TLB entry on host CPU1. * Phase 2: the host migrates vCPU0 to host CPU0; the task writes the page * on vCPU0 (host CPU0), triggering COW. The guest-local * sfence.vma only invalidates entries on host CPU0. * Phase 3: the task is scheduled onto vCPU1 (host CPU1) and reads the same * page, hitting the stale entry left behind on host CPU1. * * Usage: * ./vsstage_tlb_stale_guest [host_ip] [port] * Default host_ip=10.0.2.2 port=12345 * * Build: * gcc -O0 -o vsstage_tlb_stale_guest vsstage_tlb_stale_guest.c */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sched.h> #include <errno.h> #include <stdint.h> #include <sys/mman.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <arpa/inet.h> #define PAGE_SIZE 4096 #define INITIAL_VALUE 0x0UL #define WRITE_VALUE 0xCAFEBABEUL static volatile uint64_t *shared_page; static int sock_fd = -1; /* ---------------- TCP communication ---------------- */ static int connect_host(const char *host, int port) { struct sockaddr_in sa; int fd, one = 1, i; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons(port); if (inet_pton(AF_INET, host, &sa.sin_addr) != 1) { fprintf(stderr, "Invalid host IP: %s\n", host); return -1; } fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("socket"); return -1; } setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); for (i = 0; i < 60; i++) { if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) == 0) return fd; if (i == 0) fprintf(stderr, "Waiting for host %s:%d ...\n", host, port); sleep(1); } fprintf(stderr, "Failed to connect to host: %s\n", strerror(errno)); close(fd); return -1; } static void send_msg(const char *msg) { size_t len = strlen(msg); if (send(sock_fd, msg, len, 0) != (ssize_t)len || send(sock_fd, "\n", 1, 0) != 1) perror("send"); } /* Read one line, blocking until '\n' is received. */ static int recv_msg(char *buf, size_t buflen) { size_t n = 0; while (n + 1 < buflen) { char c; ssize_t r = recv(sock_fd, &c, 1, 0); if (r <= 0) return -1; if (c == '\n') break; buf[n++] = c; } buf[n] = '\0'; return (int)n; } /* ---------------- CPU pinning ---------------- */ static int pin_guest_cpu(int cpu) { cpu_set_t set; CPU_ZERO(&set); CPU_SET(cpu, &set); if (sched_setaffinity(0, sizeof(set), &set) != 0) { fprintf(stderr, "Failed to pin to guest CPU %d: %s\n", cpu, strerror(errno)); return -1; } /* Give the scheduler time to actually place the task on the target vCPU. */ usleep(500000); printf(" [Guest] Now running on guest CPU %d\n", sched_getcpu()); fflush(stdout); return 0; } /* ---------------- Main flow ---------------- */ int main(int argc, char **argv) { const char *host = argc > 1 ? argv[1] : "10.0.2.2"; int port = argc > 2 ? atoi(argv[2]) : 12345; uint64_t val; char buf[128]; long ncpus; setvbuf(stdout, NULL, _IOLBF, 0); printf("=====================================================\n"); printf(" RISC-V KVM stale VS-stage TLB reproducer (Guest)\n"); printf("=====================================================\n"); printf("Connecting to host: %s:%d\n", host, port); ncpus = sysconf(_SC_NPROCESSORS_ONLN); if (ncpus < 2) { fprintf(stderr, "At least 2 guest CPUs are required\n"); return 1; } /* * Anonymous private mapping. The first read faults in the shared * read-only zero page; the first write triggers COW. */ shared_page = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (shared_page == MAP_FAILED) { perror("mmap"); return 1; } printf("Shared page: %p (initially mapped to the read-only zero page)\n", (void *)shared_page); /* ---- Connect to host ---- */ sock_fd = connect_host(host, port); if (sock_fd < 0) return 1; send_msg("HELLO"); if (recv_msg(buf, sizeof(buf)) < 0 || strcmp(buf, "GO_PHASE1") != 0) { fprintf(stderr, "Handshake failed: %s\n", buf); return 1; } /* ---- Phase 1: read on vCPU0 (host CPU1), populate stale TLB ---- */ printf("\n--- Phase 1: vCPU0 reads shared page (expected host CPU1) ---\n"); if (pin_guest_cpu(0) != 0) return 1; __sync_synchronize(); val = shared_page[0]; __sync_synchronize(); printf(" [Guest] Phase 1 read 0x%lx\n", (unsigned long)val); send_msg("PHASE1_DONE"); if (recv_msg(buf, sizeof(buf)) < 0 || strcmp(buf, "MIGRATED0") != 0) { fprintf(stderr, "Did not receive MIGRATED0: %s\n", buf); return 1; } printf(" [Guest] Host migrated vCPU0 to host CPU0\n"); /* ---- Phase 2: write on vCPU0 (host CPU0), COW + local sfence ---- */ printf("\n--- Phase 2: vCPU0 writes shared page, triggering COW " "(expected host CPU0) ---\n"); __sync_synchronize(); shared_page[0] = WRITE_VALUE; __sync_synchronize(); val = shared_page[0]; printf(" [Guest] Phase 2 read back 0x%lx\n", (unsigned long)val); send_msg("PHASE2_DONE"); if (recv_msg(buf, sizeof(buf)) < 0 || strcmp(buf, "MIGRATED1") != 0) { fprintf(stderr, "Did not receive MIGRATED1: %s\n", buf); return 1; } printf(" [Guest] Host confirmed vCPU1 is still on host CPU1\n"); /* ---- Phase 3: task migrates to vCPU1 (host CPU1), hit stale entry ---- */ printf("\n--- Phase 3: task migrates to vCPU1 and reads shared page " "(expected host CPU1) ---\n"); if (pin_guest_cpu(1) != 0) return 1; __sync_synchronize(); val = shared_page[0]; __sync_synchronize(); printf(" [Guest] Phase 3 read 0x%lx\n", (unsigned long)val); snprintf(buf, sizeof(buf), "RESULT:0x%lx", (unsigned long)val); send_msg(buf); printf("\n=====================================================\n"); if (val == WRITE_VALUE) { printf(" Verdict: OK (read the new value 0x%lx)\n", (unsigned long)val); } else if (val == INITIAL_VALUE) { printf(" Verdict: *** BUG REPRODUCED *** read the stale value 0x0\n"); } else { printf(" Verdict: unexpected value 0x%lx\n", (unsigned long)val); } printf("=====================================================\n"); close(sock_fd); munmap((void *)shared_page, PAGE_SIZE); return 0; } --- 8< --- end --- 8< --- Tested-by: BillXiang <xiangwencheng@lanxincomputing.com> >>> >>> So this is an intra-guest scheduling case onto a vCPU that stayed on >>> the original host CPU, rather than a “vcpu0 returns to host CPU1” case. >> >> My NACK still holds. >> >> Regards, >> Anup >> >>> >>> Thanks, >>> Yaxing >>> >>>>> >>>>> Host CPU1 did not observe the guest-local sfence.vma, and KVM can >>>>> enter >>>>> vcpu1 with a stale VS-stage translation left behind by vcpu0. >>>>> >>>>> The reported failure was seen on a XiangShan RISC-V system. A bash >>>>> task >>>>> ran on vcpu0 while vcpu0 was backed by host CPU1, and a read-only >>>>> page was >>>>> prefetched into CPU1's VS-stage TLB. vcpu0 later migrated to host >>>>> CPU0, >>>>> where the guest triggered COW, updated the mapping, and issued only >>>>> a local >>>>> sfence.vma. The stale VS-stage entry on CPU1 survived. When the >>>>> same guest >>>>> task later ran on another vCPU backed by host CPU1, it hit the stale >>>>> translation. The page contained a GOT pointer, and using the stale >>>>> data led >>>>> to a NULL dereference and a userspace segmentation fault. >>>>> >>>>> The existing local TLB sanitize path flushes G-stage entries when a >>>>> vCPU >>>>> migrates, and flushes VS-stage entries only for implementations >>>>> selected by >>>>> the Andes-specific kvm_riscv_vsstage_tlb_no_gpa static key. That >>>>> handles a >>>>> split two-stage TLB implementation where HFENCE.GVMA does not >>>>> invalidate >>>>> VS-stage entries, but it does not cover the generic guest-local >>>>> sfence.vma >>>>> case above. >>>>> >>>>> Track, per VM and per host CPU, the last vCPU that entered the >>>>> guest on >>>>> that CPU. Before guest entry, flush the current CPU's VS-stage >>>>> context for >>>>> the VM if either the current vCPU migrated since its last exit, or >>>>> this >>>>> host CPU is switching from another vCPU of the same VM. Keep the >>>>> existing >>>>> HFENCE.GVMA behavior tied to vCPU migration. >>>>> >>>>> This also subsumes the Andes split-TLB workaround. That workaround was >>>>> needed because the migration sanitize path previously relied on >>>>> HFENCE.GVMA alone for most implementations, and only issued >>>>> HFENCE.VVMA >>>>> when kvm_riscv_vsstage_tlb_no_gpa was set. The new sanitize path >>>>> always >>>>> issues HFENCE.VVMA when reusing a host CPU for a VM context that >>>>> may have >>>>> missed a guest-local sfence.vma, so it no longer depends on >>>>> HFENCE.GVMA >>>>> invalidating VS-stage entries. The implementation-specific static >>>>> key and >>>>> setup hook are therefore no longer needed. >>>>> >>>>> Reported-by: Zhizun Wang <anzoso@outlook.com> >>>>> Reviewed-by: Jiuyue Ma <majiuyue@bosc.ac.cn> >>>>> Signed-off-by: Yaxing Guo <guoyaxing@bosc.ac.cn> >>>>> --- >>>>> arch/riscv/include/asm/kvm_host.h | 6 +++--- >>>>> arch/riscv/kvm/main.c | 15 --------------- >>>>> arch/riscv/kvm/tlb.c | 31 ++++++++++++++++++++++ >>>>> +-------- >>>>> arch/riscv/kvm/vm.c | 16 ++++++++++++++++ >>>>> 4 files changed, 42 insertions(+), 26 deletions(-) >>>>> >>>>> diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/ >>>>> include/asm/kvm_host.h >>>>> index 24585304c..396ac6209 100644 >>>>> --- a/arch/riscv/include/asm/kvm_host.h >>>>> +++ b/arch/riscv/include/asm/kvm_host.h >>>>> @@ -91,6 +91,9 @@ struct kvm_arch { >>>>> /* G-stage vmid */ >>>>> struct kvm_vmid vmid; >>>>> >>>>> + /* Last VCPU that ran on each physical CPU */ >>>>> + int __percpu *last_vcpu_ran; >>>>> + >>>>> /* G-stage page table */ >>>>> pgd_t *pgd; >>>>> phys_addr_t pgd_phys; >>>>> @@ -330,7 +333,4 @@ bool kvm_riscv_vcpu_stopped(struct kvm_vcpu >>>>> *vcpu); >>>>> >>>>> void kvm_riscv_vcpu_record_steal_time(struct kvm_vcpu *vcpu); >>>>> >>>>> -/* Flags representing implementation specific details */ >>>>> -DECLARE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); >>>>> - >>>>> #endif /* __RISCV_KVM_HOST_H__ */ >>>>> diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c >>>>> index 0f3fe3986..b1bbc0835 100644 >>>>> --- a/arch/riscv/kvm/main.c >>>>> +++ b/arch/riscv/kvm/main.c >>>>> @@ -10,23 +10,10 @@ >>>>> #include <linux/err.h> >>>>> #include <linux/module.h> >>>>> #include <linux/kvm_host.h> >>>>> -#include <asm/cpufeature.h> >>>>> #include <asm/kvm_mmu.h> >>>>> #include <asm/kvm_nacl.h> >>>>> #include <asm/sbi.h> >>>>> >>>>> -DEFINE_STATIC_KEY_FALSE(kvm_riscv_vsstage_tlb_no_gpa); >>>>> - >>>>> -static void kvm_riscv_setup_vendor_features(void) >>>>> -{ >>>>> - /* Andes AX66: split two-stage TLBs */ >>>>> - if (riscv_cached_mvendorid(0) == ANDES_VENDOR_ID && >>>>> - (riscv_cached_marchid(0) & 0xFFFF) == 0x8A66) { >>>>> - static_branch_enable(&kvm_riscv_vsstage_tlb_no_gpa); >>>>> - kvm_info("VS-stage TLB does not cache guest >>>>> physical address and VMID\n"); >>>>> - } >>>>> -} >>>>> - >>>>> long kvm_arch_dev_ioctl(struct file *filp, >>>>> unsigned int ioctl, unsigned long arg) >>>>> { >>>>> @@ -172,8 +159,6 @@ static int __init riscv_kvm_init(void) >>>>> kvm_info("AIA available with %d guest external >>>>> interrupts\n", >>>>> kvm_riscv_aia_nr_hgei); >>>>> >>>>> - kvm_riscv_setup_vendor_features(); >>>>> - >>>>> kvm_register_perf_callbacks(); >>>>> >>>>> rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE); >>>>> diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c >>>>> index ff1aeac4e..1541d059a 100644 >>>>> --- a/arch/riscv/kvm/tlb.c >>>>> +++ b/arch/riscv/kvm/tlb.c >>>>> @@ -8,6 +8,7 @@ >>>>> #include <linux/errno.h> >>>>> #include <linux/err.h> >>>>> #include <linux/module.h> >>>>> +#include <linux/percpu.h> >>>>> #include <linux/smp.h> >>>>> #include <linux/kvm_host.h> >>>>> #include <asm/cacheflush.h> >>>>> @@ -160,12 +161,19 @@ void kvm_riscv_local_hfence_vvma_all(unsigned >>>>> long vmid) >>>>> >>>>> void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) >>>>> { >>>>> + bool vcpu_migrated; >>>>> + bool vcpu_switched; >>>>> unsigned long vmid; >>>>> + int *last_ran; >>>>> >>>>> - if (!kvm_riscv_gstage_vmid_bits() || >>>>> - vcpu->arch.last_exit_cpu == vcpu->cpu) >>>>> + last_ran = this_cpu_ptr(vcpu->kvm->arch.last_vcpu_ran); >>>>> + vcpu_migrated = (vcpu->arch.last_exit_cpu != vcpu->cpu); >>>>> + vcpu_switched = (*last_ran != vcpu->vcpu_idx); >>>>> + if (!vcpu_migrated && !vcpu_switched) >>>>> return; >>>>> >>>>> + vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); >>>>> + >>>>> /* >>>>> * On RISC-V platforms with hardware VMID support, we >>>>> share same >>>>> * VMID for all VCPUs of a particular Guest/VM. This >>>>> means we might >>>>> @@ -176,16 +184,23 @@ void kvm_riscv_local_tlb_sanitize(struct >>>>> kvm_vcpu *vcpu) >>>>> * To cleanup stale TLB entries, we simply flush all G- >>>>> stage TLB >>>>> * entries by VMID whenever underlying Host CPU changes >>>>> for a VCPU. >>>>> */ >>>>> - >>>>> - vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); >>>>> - kvm_riscv_local_hfence_gvma_vmid_all(vmid); >>>>> + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) >>>>> + kvm_riscv_local_hfence_gvma_vmid_all(vmid); >>>>> >>>>> /* >>>>> - * Flush VS-stage TLB entries for implementation where VS- >>>>> stage >>>>> - * TLB does not cahce guest physical address and VMID. >>>>> + * Guest-local sfence.vma only invalidates VS-stage >>>>> translations on >>>>> + * the Host CPU currently backing the VCPU. If a VCPU >>>>> migrates, or >>>>> + * if this Host CPU switches between VCPUs of the same VM, >>>>> stale >>>>> + * VS-stage entries can be left behind on a Host CPU that >>>>> missed a >>>>> + * guest-local flush. Flush the current CPU's VS-stage >>>>> context before >>>>> + * entering the guest. >>>>> */ >>>>> - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) >>>>> + if (kvm_riscv_nacl_available()) >>>>> + nacl_hfence_vvma_all(nacl_shmem(), vmid); >>>>> + else >>>>> kvm_riscv_local_hfence_vvma_all(vmid); >>>>> + >>>>> + *last_ran = vcpu->vcpu_idx; >>>>> } >>>>> >>>>> void kvm_riscv_fence_i_process(struct kvm_vcpu *vcpu) >>>>> diff --git a/arch/riscv/kvm/vm.c b/arch/riscv/kvm/vm.c >>>>> index 13c63ae1a..2907218a1 100644 >>>>> --- a/arch/riscv/kvm/vm.c >>>>> +++ b/arch/riscv/kvm/vm.c >>>>> @@ -9,6 +9,7 @@ >>>>> #include <linux/errno.h> >>>>> #include <linux/err.h> >>>>> #include <linux/module.h> >>>>> +#include <linux/percpu.h> >>>>> #include <linux/uaccess.h> >>>>> #include <linux/kvm_host.h> >>>>> #include <asm/kvm_mmu.h> >>>>> @@ -30,7 +31,9 @@ const struct kvm_stats_header kvm_vm_stats_header >>>>> = { >>>>> >>>>> int kvm_arch_init_vm(struct kvm *kvm, unsigned long type) >>>>> { >>>>> + int *last_ran; >>>>> int r; >>>>> + int cpu; >>>>> >>>>> r = kvm_riscv_mmu_alloc_pgd(kvm); >>>>> if (r) >>>>> @@ -42,6 +45,17 @@ int kvm_arch_init_vm(struct kvm *kvm, unsigned >>>>> long type) >>>>> return r; >>>>> } >>>>> >>>>> + kvm->arch.last_vcpu_ran = alloc_percpu(int); >>>>> + if (!kvm->arch.last_vcpu_ran) { >>>>> + kvm_riscv_mmu_free_pgd(kvm); >>>>> + return -ENOMEM; >>>>> + } >>>>> + >>>>> + for_each_possible_cpu(cpu) { >>>>> + last_ran = per_cpu_ptr(kvm->arch.last_vcpu_ran, cpu); >>>>> + *last_ran = -1; >>>>> + } >>>>> + >>>>> kvm_riscv_aia_init_vm(kvm); >>>>> >>>>> kvm_riscv_guest_timer_init(kvm); >>>>> @@ -54,6 +68,8 @@ void kvm_arch_destroy_vm(struct kvm *kvm) >>>>> kvm_destroy_vcpus(kvm); >>>>> >>>>> kvm_riscv_aia_destroy_vm(kvm); >>>>> + >>>>> + free_percpu(kvm->arch.last_vcpu_ran); >>>>> } >>>>> >>>>> int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level >>>>> *irql, >>>>> -- >>>>> 2.43.0 >>>>> >>> > > > _______________________________________________ > linux-riscv mailing list > linux-riscv@lists.infradead.org > http://lists.infradead.org/mailman/listinfo/linux-riscv ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-10 9:06 ` guoyaxing 2026-10-21 9:32 ` BillXiang @ 2026-10-24 15:49 ` Anup Patel 2026-10-26 10:18 ` guoyaxing 2026-10-29 4:05 ` BillXiang 1 sibling, 2 replies; 12+ messages in thread From: Anup Patel @ 2026-10-24 15:49 UTC (permalink / raw) To: guoyaxing Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso On Sat, Oct 10, 2026 at 2:36 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > > > > 在 2026/10/10 15:50, Anup Patel 写道: > > On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > >> > >> > >> > >> 在 2026/10/10 02:51, Anup Patel 写道: > >>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: > >>>> > >>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be > >>>> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs > >>>> that previously backed a vCPU. > >>>> > >>>> This can miss stale VS-stage TLB entries when a vCPU migrates across host > >>>> CPUs. For example: > >>>> > >>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. > >>>> - vcpu0 migrates to host CPU0. > >>>> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. > >>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. > >>> > >>> When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() > >>> will ensure TLB entries associated with Guest/VM are nuked. > >>> > >>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA > >>> for the associated VMID because most RISC-V H-extension implemenation will > >>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both > >>> G-stage and VS-stage TLB entries for given VMID. > >>> > >>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy > >>> / inefficient > >>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in > >>> kvm_riscv_setup_vendor_features() > >>> as work-around. > >>> > >>> NACK to this patch from myside. > >>> > >>> Regards, > >>> Anup > >>> > >> Hi Anup, > >> Thanks for the review and clarification. > >> > >> Regarding “When the vcpu0 is brought back to host CPU1”: the failure > >> we hit is not caused by vcpu0 being brought back onto host CPU1. In our > >> case, after vcpu0 has already migrated away, neither vCPU migrates > >> again. The guest process is later scheduled inside the multi-vCPU guest > >> onto vcpu1, which is still running on host CPU1. So the stale TLB is > >> re-hit without any further KVM vCPU migration, and > >> kvm_riscv_local_tlb_sanitize() is not involved on that path. > >> > >> My commit message was a bit too terse on this point. The concrete > >> sequence we observed was described earlier in the RFC discussion: > >> > >> > >> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ > >> > >> In short, the issue is: because vcpu0 migrated away, cpu1 still has > >> stale TLB entries while cpu0 has the new mapping. After that, neither > >> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 > >> (which is on cpu1), causing it to hit the stale TLB. Consider the > >> following scenario: > >> > >> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is > >> on cpu1, and a process on it reads a page, which gets cached in the TLB > >> (let's call this the "stale TLB entry"). > >> > >> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on > >> cpu0) does a CoW (copy-on-write) and establishes a new mapping. > >> > >> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process > >> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the > >> same page, and hits the stale TLB entry. > > > > This cannot happen because in step#2 when mapping was updated the > > Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 > > will also receive TLB flush and the stale TLB will be removed. This means > > when process is moved back to vcpu1 on cpu0, HW will fetch updated > > PTE and new TLB entry will be populated. > > > > Hi Anup, > > Thanks for the follow-up. I may be missing something, so I wanted to > double-check this against the guest TLB shootdown path. > > If I follow the Linux guest CoW path correctly, when the mapping is > updated in step#2 it goes through: > > wp_page_copy() > -> ptep_clear_flush() > -> flush_tlb_page() > -> __flush_tlb_range(mm, mm_cpumask(mm), ...) > > So the remote SFENCE.VMA appears to be targeted only at CPUs in > mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is > updated in switch_mm()/set_mm() for guest CPUs that have actually run > that mm. > > In our case, before the CoW the process had only ever run on vcpu0. From > the guest's point of view it did not migrate: the host migration of > vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees > vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least > does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. > The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is > currently running. The stale VS-stage TLB entry left on host cpu1 from > when vcpu0 previously ran there would then remain. > > Later, still without any further vCPU migration, the process is > scheduled inside the guest onto vcpu1, which remains on host cpu1, and > can hit that stale entry. > > This also seems more likely when the ASID allocator is enabled, because > set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So > even the first time the process lands on vcpu1, a stale ASID-tagged > entry on host cpu1 may still be present. > > In other words, I am not sure this case is covered by a full guest TLB > flush to all VCPUs on the mapping update. Guest shootdown looks > mm_cpumask-based, and host pCPU migration of a vCPU does not by itself > add other vCPUs that never ran the mm into that mask. This makes sense now considering ASID allocator in Guest Linux is global and not local. In any case, KVM RISC-V must not assume that the Guest OS has local ASID allocator hence the kvm_riscv_local_tlb_sanitize() must also santize TLB for other VCPUs of the same Guest which can potentially run on the previous host CPU where current VCPU ran. Having said the above, there are multiple issues with the current patch: 1) The kvm_riscv_local_hfence_gvma_vmid_all() is sufficient for most implementations to sanitize both G-stage and VS-stage TLB entries because both G-stage and VS-stage TLB entries are generally tagged with VMID. The kvm_riscv_vsstage_tlb_no_gpa work-around is for implmentations which do not tag VS-stage TLB entries with VMID and this patch remove this work-around. 2) The other VCPUs on the previous host CPU may be already running and may still hit stale TLB entry before kvm_riscv_local_tlb_sanitize() gets a chance to sanitize TLB on previous host CPU. 3) The per-CPU pointer in struct kvm_arch is racy because we may have two VCPUs on same host CPU which are migrated to other host CPUs. Instead, it is much simpler to have an atomic cpumask in each VCPU where any part of KVM can mark a host CPU in the cpumask of the VCPU to force TLB sanitization whenever the VCPU runs on that particular host CPU. How about the following change ? diff --git a/arch/riscv/include/asm/kvm_host.h b/arch/riscv/include/asm/kvm_host.h index a30600579231..c975eee7240c 100644 --- a/arch/riscv/include/asm/kvm_host.h +++ b/arch/riscv/include/asm/kvm_host.h @@ -9,6 +9,7 @@ #ifndef __RISCV_KVM_HOST_H__ #define __RISCV_KVM_HOST_H__ +#include <linux/cpumask.h> #include <linux/types.h> #include <linux/kvm.h> #include <linux/kvm_types.h> @@ -184,6 +185,9 @@ struct kvm_vcpu_arch { /* ISA feature bits (similar to MISA) */ DECLARE_BITMAP(isa, RISCV_ISA_EXT_MAX); + /* Sanitize TLB host CPU mask */ + cpumask_t tlb_sanitize_mask; + /* Vendor, Arch, and Implementation details */ unsigned long mvendorid; unsigned long marchid; diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c index 2ae34632cdcb..b1387a00c8b0 100644 --- a/arch/riscv/kvm/tlb.c +++ b/arch/riscv/kvm/tlb.c @@ -207,11 +207,31 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) { - unsigned long vmid; + struct kvm_vcpu *tvcpu; + unsigned long i, vmid; - if (!kvm_riscv_gstage_vmid_bits() || - vcpu->arch.last_exit_cpu == vcpu->cpu) - return; + if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { + if (!kvm_riscv_gstage_vmid_bits() || + vcpu->arch.last_exit_cpu == vcpu->cpu) + return; + } + + /* + * The VCPU might have left stale TLBs on the previous host CPU so + * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. + */ + if (vcpu->arch.last_exit_cpu > -1 && vcpu->arch.last_exit_cpu != vcpu->cpu) { + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { + if (tvcpu == vcpu) + continue; + cpumask_set_cpu(vcpu->arch.last_exit_cpu, + &tvcpu->arch.tlb_sanitize_mask); + kvm_vcpu_kick(tvcpu); + } + } + + /* Clear current host CPU from the tlb_sanitize_mask */ + cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); /* * On RISC-V platforms with hardware VMID support, we share same diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c index dd786f758eb7..c00dc0b191dc 100644 --- a/arch/riscv/kvm/vcpu.c +++ b/arch/riscv/kvm/vcpu.c @@ -107,6 +107,8 @@ static void kvm_riscv_reset_vcpu(struct kvm_vcpu *vcpu, bool kvm_sbi_reset) vcpu->arch.last_exit_cpu = -1; + cpumask_clear(&vcpu->arch.tlb_sanitize_mask); + kvm_riscv_vcpu_context_reset(vcpu, kvm_sbi_reset); kvm_riscv_vcpu_fp_reset(vcpu); Regards, Anup ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-24 15:49 ` Anup Patel @ 2026-10-26 10:18 ` guoyaxing 2026-10-28 6:14 ` Anup Patel 2026-10-29 4:05 ` BillXiang 1 sibling, 1 reply; 12+ messages in thread From: guoyaxing @ 2026-10-26 10:18 UTC (permalink / raw) To: Anup Patel Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso, xiangwencheng 在 2026/10/24 23:49, Anup Patel 写道: > On Sat, Oct 10, 2026 at 2:36 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >> >> >> >> 在 2026/10/10 15:50, Anup Patel 写道: >>> On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >>>> >>>> >>>> >>>> 在 2026/10/10 02:51, Anup Patel 写道: >>>>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: >>>>>> >>>>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be >>>>>> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs >>>>>> that previously backed a vCPU. >>>>>> >>>>>> This can miss stale VS-stage TLB entries when a vCPU migrates across host >>>>>> CPUs. For example: >>>>>> >>>>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. >>>>>> - vcpu0 migrates to host CPU0. >>>>>> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. >>>>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. >>>>> >>>>> When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() >>>>> will ensure TLB entries associated with Guest/VM are nuked. >>>>> >>>>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA >>>>> for the associated VMID because most RISC-V H-extension implemenation will >>>>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both >>>>> G-stage and VS-stage TLB entries for given VMID. >>>>> >>>>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy >>>>> / inefficient >>>>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in >>>>> kvm_riscv_setup_vendor_features() >>>>> as work-around. >>>>> >>>>> NACK to this patch from myside. >>>>> >>>>> Regards, >>>>> Anup >>>>> >>>> Hi Anup, >>>> Thanks for the review and clarification. >>>> >>>> Regarding “When the vcpu0 is brought back to host CPU1”: the failure >>>> we hit is not caused by vcpu0 being brought back onto host CPU1. In our >>>> case, after vcpu0 has already migrated away, neither vCPU migrates >>>> again. The guest process is later scheduled inside the multi-vCPU guest >>>> onto vcpu1, which is still running on host CPU1. So the stale TLB is >>>> re-hit without any further KVM vCPU migration, and >>>> kvm_riscv_local_tlb_sanitize() is not involved on that path. >>>> >>>> My commit message was a bit too terse on this point. The concrete >>>> sequence we observed was described earlier in the RFC discussion: >>>> >>>> >>>> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ >>>> >>>> In short, the issue is: because vcpu0 migrated away, cpu1 still has >>>> stale TLB entries while cpu0 has the new mapping. After that, neither >>>> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 >>>> (which is on cpu1), causing it to hit the stale TLB. Consider the >>>> following scenario: >>>> >>>> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is >>>> on cpu1, and a process on it reads a page, which gets cached in the TLB >>>> (let's call this the "stale TLB entry"). >>>> >>>> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on >>>> cpu0) does a CoW (copy-on-write) and establishes a new mapping. >>>> >>>> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process >>>> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the >>>> same page, and hits the stale TLB entry. >>> >>> This cannot happen because in step#2 when mapping was updated the >>> Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 >>> will also receive TLB flush and the stale TLB will be removed. This means >>> when process is moved back to vcpu1 on cpu0, HW will fetch updated >>> PTE and new TLB entry will be populated. >>> >> >> Hi Anup, >> >> Thanks for the follow-up. I may be missing something, so I wanted to >> double-check this against the guest TLB shootdown path. >> >> If I follow the Linux guest CoW path correctly, when the mapping is >> updated in step#2 it goes through: >> >> wp_page_copy() >> -> ptep_clear_flush() >> -> flush_tlb_page() >> -> __flush_tlb_range(mm, mm_cpumask(mm), ...) >> >> So the remote SFENCE.VMA appears to be targeted only at CPUs in >> mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is >> updated in switch_mm()/set_mm() for guest CPUs that have actually run >> that mm. >> >> In our case, before the CoW the process had only ever run on vcpu0. From >> the guest's point of view it did not migrate: the host migration of >> vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees >> vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least >> does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. >> The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is >> currently running. The stale VS-stage TLB entry left on host cpu1 from >> when vcpu0 previously ran there would then remain. >> >> Later, still without any further vCPU migration, the process is >> scheduled inside the guest onto vcpu1, which remains on host cpu1, and >> can hit that stale entry. >> >> This also seems more likely when the ASID allocator is enabled, because >> set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So >> even the first time the process lands on vcpu1, a stale ASID-tagged >> entry on host cpu1 may still be present. >> >> In other words, I am not sure this case is covered by a full guest TLB >> flush to all VCPUs on the mapping update. Guest shootdown looks >> mm_cpumask-based, and host pCPU migration of a vCPU does not by itself >> add other vCPUs that never ran the mm into that mask. > > This makes sense now considering ASID allocator in Guest Linux is global > and not local. In any case, KVM RISC-V must not assume that the Guest OS > has local ASID allocator hence the kvm_riscv_local_tlb_sanitize() must also > santize TLB for other VCPUs of the same Guest which can potentially run > on the previous host CPU where current VCPU ran. > > Having said the above, there are multiple issues with the current patch: > 1) The kvm_riscv_local_hfence_gvma_vmid_all() is sufficient for most > implementations to sanitize both G-stage and VS-stage TLB entries > because both G-stage and VS-stage TLB entries are generally tagged > with VMID. The kvm_riscv_vsstage_tlb_no_gpa work-around is for > implmentations which do not tag VS-stage TLB entries with VMID > and this patch remove this work-around. > 2) The other VCPUs on the previous host CPU may be already running > and may still hit stale TLB entry before kvm_riscv_local_tlb_sanitize() > gets a chance to sanitize TLB on previous host CPU. > 3) The per-CPU pointer in struct kvm_arch is racy because we > may have two VCPUs on same host CPU which are migrated to > other host CPUs. Instead, it is much simpler to have an atomic > cpumask in each VCPU where any part of KVM can mark a host > CPU in the cpumask of the VCPU to force TLB sanitization whenever > the VCPU runs on that particular host CPU. > > How about the following change ? > > diff --git a/arch/riscv/include/asm/kvm_host.h > b/arch/riscv/include/asm/kvm_host.h > index a30600579231..c975eee7240c 100644 > --- a/arch/riscv/include/asm/kvm_host.h > +++ b/arch/riscv/include/asm/kvm_host.h > @@ -9,6 +9,7 @@ > #ifndef __RISCV_KVM_HOST_H__ > #define __RISCV_KVM_HOST_H__ > > +#include <linux/cpumask.h> > #include <linux/types.h> > #include <linux/kvm.h> > #include <linux/kvm_types.h> > @@ -184,6 +185,9 @@ struct kvm_vcpu_arch { > /* ISA feature bits (similar to MISA) */ > DECLARE_BITMAP(isa, RISCV_ISA_EXT_MAX); > > + /* Sanitize TLB host CPU mask */ > + cpumask_t tlb_sanitize_mask; > + > /* Vendor, Arch, and Implementation details */ > unsigned long mvendorid; > unsigned long marchid; > diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c > index 2ae34632cdcb..b1387a00c8b0 100644 > --- a/arch/riscv/kvm/tlb.c > +++ b/arch/riscv/kvm/tlb.c > @@ -207,11 +207,31 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) > > void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > { > - unsigned long vmid; > + struct kvm_vcpu *tvcpu; > + unsigned long i, vmid; > > - if (!kvm_riscv_gstage_vmid_bits() || > - vcpu->arch.last_exit_cpu == vcpu->cpu) > - return; > + if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { > + if (!kvm_riscv_gstage_vmid_bits() || > + vcpu->arch.last_exit_cpu == vcpu->cpu) > + return; > + } > + > + /* > + * The VCPU might have left stale TLBs on the previous host CPU so > + * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. > + */ > + if (vcpu->arch.last_exit_cpu > -1 && vcpu->arch.last_exit_cpu != > vcpu->cpu) { > + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { > + if (tvcpu == vcpu) > + continue; > + cpumask_set_cpu(vcpu->arch.last_exit_cpu, > + &tvcpu->arch.tlb_sanitize_mask); > + kvm_vcpu_kick(tvcpu); > + } > + } > + > + /* Clear current host CPU from the tlb_sanitize_mask */ > + cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); > > /* > * On RISC-V platforms with hardware VMID support, we share same > diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c > index dd786f758eb7..c00dc0b191dc 100644 > --- a/arch/riscv/kvm/vcpu.c > +++ b/arch/riscv/kvm/vcpu.c > @@ -107,6 +107,8 @@ static void kvm_riscv_reset_vcpu(struct kvm_vcpu > *vcpu, bool kvm_sbi_reset) > > vcpu->arch.last_exit_cpu = -1; > > + cpumask_clear(&vcpu->arch.tlb_sanitize_mask); > + > kvm_riscv_vcpu_context_reset(vcpu, kvm_sbi_reset); > > kvm_riscv_vcpu_fp_reset(vcpu); > > Regards, > Anup Hi Anup, Thanks for the explanation and for suggesting the per-vCPU sanitization mask. Building on your proposal, I would like to suggest two changes: 1. Separate the invalidation strategy for vCPU migration and a mask hit. Keep HFENCE.GVMA for vCPU migration, identified by last_exit_cpu != vcpu->cpu, when hardware VMIDs are available. Use HFENCE.VVMA when the current host CPU is marked in tlb_sanitize_mask. A mask hit caused by another vCPU of the same VM running on this CPU requires VS-stage sanitization; it does not by itself require G-stage invalidation. For example, if vCPU0 and vCPU1 both last ran on CPU0, switching from vCPU1 back to vCPU0 only needs the mask-triggered VVMA. This avoids requesting G-stage invalidation when the shared G-stage mappings have not changed. If both migration and a mask hit occur, both fences run. 2. Move mask updates to kvm_arch_vcpu_put(). Mark the outgoing host CPU in the masks of the other vCPUs of the same VM when a vCPU is put. This also covers a switch between vCPUs on one host CPU without either vCPU migrating. Marking only when migration is detected in kvm_riscv_local_tlb_sanitize() would miss that case. The marking now happens on the outgoing CPU before another vCPU can enter guest mode there. The migration-time marking and kicks are therefore removed. The trade-off is an O(nr_vcpus) walk on each qualifying put(). How about the following change ? diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c index 2ac3c0a20fce..ae8a40b3429c 100644 --- a/arch/riscv/kvm/tlb.c +++ b/arch/riscv/kvm/tlb.c @@ -160,32 +160,15 @@ void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) { - struct kvm_vcpu *tvcpu; - unsigned long i, vmid; - - if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { - if (!kvm_riscv_gstage_vmid_bits() || - vcpu->arch.last_exit_cpu == vcpu->cpu) - return; - } - - /* - * The VCPU might have left stale TLBs on the previous host CPU so - * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. - */ - if (vcpu->arch.last_exit_cpu > -1 && - vcpu->arch.last_exit_cpu != vcpu->cpu) { - kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { - if (tvcpu == vcpu) - continue; - cpumask_set_cpu(vcpu->arch.last_exit_cpu, - &tvcpu->arch.tlb_sanitize_mask); - kvm_vcpu_kick(tvcpu); - } - } - - /* Clear current host CPU from the tlb_sanitize_mask */ - cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); + unsigned long vmid; + bool vcpu_migrated, sanitize_vsstage; + + vcpu_migrated = vcpu->arch.last_exit_cpu != vcpu->cpu; + sanitize_vsstage = cpumask_test_and_clear_cpu(vcpu->cpu, + &vcpu->arch.tlb_sanitize_mask); + if (!sanitize_vsstage && + (!kvm_riscv_gstage_vmid_bits() || !vcpu_migrated)) + return; /* * On RISC-V platforms with hardware VMID support, we share same @@ -199,13 +182,16 @@ */ vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); - kvm_riscv_local_hfence_gvma_vmid_all(vmid); + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) + kvm_riscv_local_hfence_gvma_vmid_all(vmid); /* - * Flush VS-stage TLB entries for implementation where VS-stage - * TLB does not cahce guest physical address and VMID. + * Flush VS-stage entries marked when another VCPU left this CPU, or when + * required by the split-TLB vendor workaround on this VCPU's migration. */ - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) + if (sanitize_vsstage || + (vcpu_migrated && + static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa))) kvm_riscv_local_hfence_vvma_all(vmid); } diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c index f2e52f3b0786..6209c66b24e1 100644 --- a/arch/riscv/kvm/vcpu.c +++ b/arch/riscv/kvm/vcpu.c @@ -644,8 +644,18 @@ void kvm_arch_vcpu_put(struct kvm_vcpu *vcpu) { + struct kvm_vcpu *tvcpu; + unsigned long i; + int cpu = vcpu->cpu; void *nsh; struct kvm_vcpu_csr *csr = &vcpu->arch.guest_csr; + + if (cpu >= 0 && vcpu->arch.last_exit_cpu == cpu) { + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { + if (tvcpu != vcpu) + cpumask_set_cpu(cpu, &tvcpu->arch.tlb_sanitize_mask); + } + } vcpu->cpu = -1; Would these changes make sense to you, or is there anythingI might be overlooking? Regards, Yaxing ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-26 10:18 ` guoyaxing @ 2026-10-28 6:14 ` Anup Patel 2026-10-28 7:15 ` BillXiang 0 siblings, 1 reply; 12+ messages in thread From: Anup Patel @ 2026-10-28 6:14 UTC (permalink / raw) To: guoyaxing Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso, xiangwencheng On Mon, Oct 26, 2026 at 3:49 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > > > > 在 2026/10/24 23:49, Anup Patel 写道: > > On Sat, Oct 10, 2026 at 2:36 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > >> > >> > >> > >> 在 2026/10/10 15:50, Anup Patel 写道: > >>> On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > >>>> > >>>> > >>>> > >>>> 在 2026/10/10 02:51, Anup Patel 写道: > >>>>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: > >>>>>> > >>>>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be > >>>>>> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs > >>>>>> that previously backed a vCPU. > >>>>>> > >>>>>> This can miss stale VS-stage TLB entries when a vCPU migrates across host > >>>>>> CPUs. For example: > >>>>>> > >>>>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. > >>>>>> - vcpu0 migrates to host CPU0. > >>>>>> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. > >>>>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. > >>>>> > >>>>> When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() > >>>>> will ensure TLB entries associated with Guest/VM are nuked. > >>>>> > >>>>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA > >>>>> for the associated VMID because most RISC-V H-extension implemenation will > >>>>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both > >>>>> G-stage and VS-stage TLB entries for given VMID. > >>>>> > >>>>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy > >>>>> / inefficient > >>>>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in > >>>>> kvm_riscv_setup_vendor_features() > >>>>> as work-around. > >>>>> > >>>>> NACK to this patch from myside. > >>>>> > >>>>> Regards, > >>>>> Anup > >>>>> > >>>> Hi Anup, > >>>> Thanks for the review and clarification. > >>>> > >>>> Regarding “When the vcpu0 is brought back to host CPU1”: the failure > >>>> we hit is not caused by vcpu0 being brought back onto host CPU1. In our > >>>> case, after vcpu0 has already migrated away, neither vCPU migrates > >>>> again. The guest process is later scheduled inside the multi-vCPU guest > >>>> onto vcpu1, which is still running on host CPU1. So the stale TLB is > >>>> re-hit without any further KVM vCPU migration, and > >>>> kvm_riscv_local_tlb_sanitize() is not involved on that path. > >>>> > >>>> My commit message was a bit too terse on this point. The concrete > >>>> sequence we observed was described earlier in the RFC discussion: > >>>> > >>>> > >>>> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ > >>>> > >>>> In short, the issue is: because vcpu0 migrated away, cpu1 still has > >>>> stale TLB entries while cpu0 has the new mapping. After that, neither > >>>> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 > >>>> (which is on cpu1), causing it to hit the stale TLB. Consider the > >>>> following scenario: > >>>> > >>>> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is > >>>> on cpu1, and a process on it reads a page, which gets cached in the TLB > >>>> (let's call this the "stale TLB entry"). > >>>> > >>>> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on > >>>> cpu0) does a CoW (copy-on-write) and establishes a new mapping. > >>>> > >>>> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process > >>>> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the > >>>> same page, and hits the stale TLB entry. > >>> > >>> This cannot happen because in step#2 when mapping was updated the > >>> Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 > >>> will also receive TLB flush and the stale TLB will be removed. This means > >>> when process is moved back to vcpu1 on cpu0, HW will fetch updated > >>> PTE and new TLB entry will be populated. > >>> > >> > >> Hi Anup, > >> > >> Thanks for the follow-up. I may be missing something, so I wanted to > >> double-check this against the guest TLB shootdown path. > >> > >> If I follow the Linux guest CoW path correctly, when the mapping is > >> updated in step#2 it goes through: > >> > >> wp_page_copy() > >> -> ptep_clear_flush() > >> -> flush_tlb_page() > >> -> __flush_tlb_range(mm, mm_cpumask(mm), ...) > >> > >> So the remote SFENCE.VMA appears to be targeted only at CPUs in > >> mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is > >> updated in switch_mm()/set_mm() for guest CPUs that have actually run > >> that mm. > >> > >> In our case, before the CoW the process had only ever run on vcpu0. From > >> the guest's point of view it did not migrate: the host migration of > >> vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees > >> vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least > >> does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. > >> The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is > >> currently running. The stale VS-stage TLB entry left on host cpu1 from > >> when vcpu0 previously ran there would then remain. > >> > >> Later, still without any further vCPU migration, the process is > >> scheduled inside the guest onto vcpu1, which remains on host cpu1, and > >> can hit that stale entry. > >> > >> This also seems more likely when the ASID allocator is enabled, because > >> set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So > >> even the first time the process lands on vcpu1, a stale ASID-tagged > >> entry on host cpu1 may still be present. > >> > >> In other words, I am not sure this case is covered by a full guest TLB > >> flush to all VCPUs on the mapping update. Guest shootdown looks > >> mm_cpumask-based, and host pCPU migration of a vCPU does not by itself > >> add other vCPUs that never ran the mm into that mask. > > > > This makes sense now considering ASID allocator in Guest Linux is global > > and not local. In any case, KVM RISC-V must not assume that the Guest OS > > has local ASID allocator hence the kvm_riscv_local_tlb_sanitize() must also > > santize TLB for other VCPUs of the same Guest which can potentially run > > on the previous host CPU where current VCPU ran. > > > > Having said the above, there are multiple issues with the current patch: > > 1) The kvm_riscv_local_hfence_gvma_vmid_all() is sufficient for most > > implementations to sanitize both G-stage and VS-stage TLB entries > > because both G-stage and VS-stage TLB entries are generally tagged > > with VMID. The kvm_riscv_vsstage_tlb_no_gpa work-around is for > > implmentations which do not tag VS-stage TLB entries with VMID > > and this patch remove this work-around. > > 2) The other VCPUs on the previous host CPU may be already running > > and may still hit stale TLB entry before kvm_riscv_local_tlb_sanitize() > > gets a chance to sanitize TLB on previous host CPU. > > 3) The per-CPU pointer in struct kvm_arch is racy because we > > may have two VCPUs on same host CPU which are migrated to > > other host CPUs. Instead, it is much simpler to have an atomic > > cpumask in each VCPU where any part of KVM can mark a host > > CPU in the cpumask of the VCPU to force TLB sanitization whenever > > the VCPU runs on that particular host CPU. > > > > How about the following change ? > > > > diff --git a/arch/riscv/include/asm/kvm_host.h > > b/arch/riscv/include/asm/kvm_host.h > > index a30600579231..c975eee7240c 100644 > > --- a/arch/riscv/include/asm/kvm_host.h > > +++ b/arch/riscv/include/asm/kvm_host.h > > @@ -9,6 +9,7 @@ > > #ifndef __RISCV_KVM_HOST_H__ > > #define __RISCV_KVM_HOST_H__ > > > > +#include <linux/cpumask.h> > > #include <linux/types.h> > > #include <linux/kvm.h> > > #include <linux/kvm_types.h> > > @@ -184,6 +185,9 @@ struct kvm_vcpu_arch { > > /* ISA feature bits (similar to MISA) */ > > DECLARE_BITMAP(isa, RISCV_ISA_EXT_MAX); > > > > + /* Sanitize TLB host CPU mask */ > > + cpumask_t tlb_sanitize_mask; > > + > > /* Vendor, Arch, and Implementation details */ > > unsigned long mvendorid; > > unsigned long marchid; > > diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c > > index 2ae34632cdcb..b1387a00c8b0 100644 > > --- a/arch/riscv/kvm/tlb.c > > +++ b/arch/riscv/kvm/tlb.c > > @@ -207,11 +207,31 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) > > > > void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > > { > > - unsigned long vmid; > > + struct kvm_vcpu *tvcpu; > > + unsigned long i, vmid; > > > > - if (!kvm_riscv_gstage_vmid_bits() || > > - vcpu->arch.last_exit_cpu == vcpu->cpu) > > - return; > > + if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { > > + if (!kvm_riscv_gstage_vmid_bits() || > > + vcpu->arch.last_exit_cpu == vcpu->cpu) > > + return; > > + } > > + > > + /* > > + * The VCPU might have left stale TLBs on the previous host CPU so > > + * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. > > + */ > > + if (vcpu->arch.last_exit_cpu > -1 && vcpu->arch.last_exit_cpu != > > vcpu->cpu) { > > + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { > > + if (tvcpu == vcpu) > > + continue; > > + cpumask_set_cpu(vcpu->arch.last_exit_cpu, > > + &tvcpu->arch.tlb_sanitize_mask); > > + kvm_vcpu_kick(tvcpu); > > + } > > + } > > + > > + /* Clear current host CPU from the tlb_sanitize_mask */ > > + cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); > > > > /* > > * On RISC-V platforms with hardware VMID support, we share same > > diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c > > index dd786f758eb7..c00dc0b191dc 100644 > > --- a/arch/riscv/kvm/vcpu.c > > +++ b/arch/riscv/kvm/vcpu.c > > @@ -107,6 +107,8 @@ static void kvm_riscv_reset_vcpu(struct kvm_vcpu > > *vcpu, bool kvm_sbi_reset) > > > > vcpu->arch.last_exit_cpu = -1; > > > > + cpumask_clear(&vcpu->arch.tlb_sanitize_mask); > > + > > kvm_riscv_vcpu_context_reset(vcpu, kvm_sbi_reset); > > > > kvm_riscv_vcpu_fp_reset(vcpu); > > > > Regards, > > Anup > > Hi Anup, > > Thanks for the explanation and for suggesting the per-vCPU sanitization > mask. Building on your proposal, I would like to suggest two changes: > > 1. Separate the invalidation strategy for vCPU migration and a mask hit. > > Keep HFENCE.GVMA for vCPU migration, identified by > last_exit_cpu != vcpu->cpu, when hardware VMIDs are available. Use > HFENCE.VVMA when the current host CPU is marked in tlb_sanitize_mask. > A mask hit caused by another vCPU of the same VM running on this CPU > requires VS-stage sanitization; it does not by itself require G-stage > invalidation. > > For example, if vCPU0 and vCPU1 both last ran on CPU0, switching from > vCPU1 back to vCPU0 only needs the mask-triggered VVMA. This avoids > requesting G-stage invalidation when the shared G-stage mappings have > not changed. If both migration and a mask hit occur, both fences run. HFENCE.GVMA is sufficient for most implementations so to sanitize all TLB entries matching a particular VMID. Refer, the following text from RISC-V Privileged specification section "5.3.2. Hypervisor Memory-Management Fence Instructions": Conceptually, an implementation might contain two address-translation caches: one that maps guest virtual addresses to guest physical addresses, and another that maps guest physical addresses to supervisor physical addresses. HFENCE.GVMA need not flush the former cache, but it must flush entries from the latter cache that match the HFENCE.GVMA’s address and VMID arguments. More commonly, implementations contain address-translation caches that map guest virtual addresses directly to supervisor physical addresses, removing a level of indirection. For such implementations, any entry whose guest virtual address maps to a guest physical address that matches the HFENCE.GVMA’s address and VMID arguments must be flushed. Selectively flushing entries in this fashion requires tagging them with the guest physical address, which is costly, and so a common technique is to flush all entries that match the HFENCE.GVMA’s VMID argument, regardless of the address argument. Doing HFENCE.GVMA in certain cases and HFENCE.VVMA other cases is maintainence burden without much benefits. Also, VCPU migrating from one host CPU to another is an infrequent event so no point over optimizing without much benefits. In short, NACK to your suggestion #1. > > 2. Move mask updates to kvm_arch_vcpu_put(). > > Mark the outgoing host CPU in the masks of the other vCPUs of the > same VM when a vCPU is put. This also covers a switch between vCPUs > on one host CPU without either vCPU migrating. Marking only when > migration is detected in kvm_riscv_local_tlb_sanitize() would miss > that case. > > The marking now happens on the outgoing CPU before another vCPU can > enter guest mode there. The migration-time marking and kicks are > therefore removed. The trade-off is an O(nr_vcpus) walk on each > qualifying put(). > > How about the following change ? > > diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c > index 2ac3c0a20fce..ae8a40b3429c 100644 > --- a/arch/riscv/kvm/tlb.c > +++ b/arch/riscv/kvm/tlb.c > @@ -160,32 +160,15 @@ > > void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > { > - struct kvm_vcpu *tvcpu; > - unsigned long i, vmid; > - > - if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { > - if (!kvm_riscv_gstage_vmid_bits() || > - vcpu->arch.last_exit_cpu == vcpu->cpu) > - return; > - } > - > - /* > - * The VCPU might have left stale TLBs on the previous host CPU so > - * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. > - */ > - if (vcpu->arch.last_exit_cpu > -1 && > - vcpu->arch.last_exit_cpu != vcpu->cpu) { > - kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { > - if (tvcpu == vcpu) > - continue; > - cpumask_set_cpu(vcpu->arch.last_exit_cpu, > - &tvcpu->arch.tlb_sanitize_mask); > - kvm_vcpu_kick(tvcpu); > - } > - } > - > - /* Clear current host CPU from the tlb_sanitize_mask */ > - cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); > + unsigned long vmid; > + bool vcpu_migrated, sanitize_vsstage; > + > + vcpu_migrated = vcpu->arch.last_exit_cpu != vcpu->cpu; > + sanitize_vsstage = cpumask_test_and_clear_cpu(vcpu->cpu, > + &vcpu->arch.tlb_sanitize_mask); > + if (!sanitize_vsstage && > + (!kvm_riscv_gstage_vmid_bits() || !vcpu_migrated)) > + return; > > /* > * On RISC-V platforms with hardware VMID support, we share same > @@ -199,13 +182,16 @@ > */ > > vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); > - kvm_riscv_local_hfence_gvma_vmid_all(vmid); > + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) > + kvm_riscv_local_hfence_gvma_vmid_all(vmid); > > /* > - * Flush VS-stage TLB entries for implementation where VS-stage > - * TLB does not cahce guest physical address and VMID. > + * Flush VS-stage entries marked when another VCPU left this CPU, or when > + * required by the split-TLB vendor workaround on this VCPU's migration. > */ > - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) > + if (sanitize_vsstage || > + (vcpu_migrated && > + static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa))) > kvm_riscv_local_hfence_vvma_all(vmid); > } > > diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c > index f2e52f3b0786..6209c66b24e1 100644 > --- a/arch/riscv/kvm/vcpu.c > +++ b/arch/riscv/kvm/vcpu.c > @@ -644,8 +644,18 @@ > > void kvm_arch_vcpu_put(struct kvm_vcpu *vcpu) > { > + struct kvm_vcpu *tvcpu; > + unsigned long i; > + int cpu = vcpu->cpu; > void *nsh; > struct kvm_vcpu_csr *csr = &vcpu->arch.guest_csr; > + > + if (cpu >= 0 && vcpu->arch.last_exit_cpu == cpu) { > + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { > + if (tvcpu != vcpu) > + cpumask_set_cpu(cpu, &tvcpu->arch.tlb_sanitize_mask); > + } > + } > > vcpu->cpu = -1; > > Would these changes make sense to you, or is there anythingI might > be overlooking? The kvm_arch_vcpu_put() is called every time a VCPU exits to user space for MMIO emulation and handling other exits. Now, imagine VCPU0 and VCPU1 running on same host CPU where VCPU0 is doing lot of MMIO exits causing VCPU1 to sanitize TLB frequently on the same host CPU. Based on above, NACK to your suggestion #2 Regards, Anup ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-28 6:14 ` Anup Patel @ 2026-10-28 7:15 ` BillXiang 2026-10-28 10:23 ` Anup Patel 0 siblings, 1 reply; 12+ messages in thread From: BillXiang @ 2026-10-28 7:15 UTC (permalink / raw) To: Anup Patel, guoyaxing Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso, BillXiang On 10/28/2026 2:14 PM, Anup Patel wrote: > On Mon, Oct 26, 2026 at 3:49 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >> >> >> >> 在 2026/10/24 23:49, Anup Patel 写道: >>> On Sat, Oct 10, 2026 at 2:36 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >>>> >>>> >>>> >>>> 在 2026/10/10 15:50, Anup Patel 写道: >>>>> On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >>>>>> >>>>>> >>>>>> >>>>>> 在 2026/10/10 02:51, Anup Patel 写道: >>>>>>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: >>>>>>>> >>>>>>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be >>>>>>>> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs >>>>>>>> that previously backed a vCPU. >>>>>>>> >>>>>>>> This can miss stale VS-stage TLB entries when a vCPU migrates across host >>>>>>>> CPUs. For example: >>>>>>>> >>>>>>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. >>>>>>>> - vcpu0 migrates to host CPU0. >>>>>>>> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. >>>>>>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. >>>>>>> >>>>>>> When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() >>>>>>> will ensure TLB entries associated with Guest/VM are nuked. >>>>>>> >>>>>>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA >>>>>>> for the associated VMID because most RISC-V H-extension implemenation will >>>>>>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both >>>>>>> G-stage and VS-stage TLB entries for given VMID. >>>>>>> >>>>>>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy >>>>>>> / inefficient >>>>>>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in >>>>>>> kvm_riscv_setup_vendor_features() >>>>>>> as work-around. >>>>>>> >>>>>>> NACK to this patch from myside. >>>>>>> >>>>>>> Regards, >>>>>>> Anup >>>>>>> >>>>>> Hi Anup, >>>>>> Thanks for the review and clarification. >>>>>> >>>>>> Regarding “When the vcpu0 is brought back to host CPU1”: the failure >>>>>> we hit is not caused by vcpu0 being brought back onto host CPU1. In our >>>>>> case, after vcpu0 has already migrated away, neither vCPU migrates >>>>>> again. The guest process is later scheduled inside the multi-vCPU guest >>>>>> onto vcpu1, which is still running on host CPU1. So the stale TLB is >>>>>> re-hit without any further KVM vCPU migration, and >>>>>> kvm_riscv_local_tlb_sanitize() is not involved on that path. >>>>>> >>>>>> My commit message was a bit too terse on this point. The concrete >>>>>> sequence we observed was described earlier in the RFC discussion: >>>>>> >>>>>> >>>>>> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ >>>>>> >>>>>> In short, the issue is: because vcpu0 migrated away, cpu1 still has >>>>>> stale TLB entries while cpu0 has the new mapping. After that, neither >>>>>> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 >>>>>> (which is on cpu1), causing it to hit the stale TLB. Consider the >>>>>> following scenario: >>>>>> >>>>>> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is >>>>>> on cpu1, and a process on it reads a page, which gets cached in the TLB >>>>>> (let's call this the "stale TLB entry"). >>>>>> >>>>>> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on >>>>>> cpu0) does a CoW (copy-on-write) and establishes a new mapping. >>>>>> >>>>>> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process >>>>>> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the >>>>>> same page, and hits the stale TLB entry. >>>>> >>>>> This cannot happen because in step#2 when mapping was updated the >>>>> Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 >>>>> will also receive TLB flush and the stale TLB will be removed. This means >>>>> when process is moved back to vcpu1 on cpu0, HW will fetch updated >>>>> PTE and new TLB entry will be populated. >>>>> >>>> >>>> Hi Anup, >>>> >>>> Thanks for the follow-up. I may be missing something, so I wanted to >>>> double-check this against the guest TLB shootdown path. >>>> >>>> If I follow the Linux guest CoW path correctly, when the mapping is >>>> updated in step#2 it goes through: >>>> >>>> wp_page_copy() >>>> -> ptep_clear_flush() >>>> -> flush_tlb_page() >>>> -> __flush_tlb_range(mm, mm_cpumask(mm), ...) >>>> >>>> So the remote SFENCE.VMA appears to be targeted only at CPUs in >>>> mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is >>>> updated in switch_mm()/set_mm() for guest CPUs that have actually run >>>> that mm. >>>> >>>> In our case, before the CoW the process had only ever run on vcpu0. From >>>> the guest's point of view it did not migrate: the host migration of >>>> vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees >>>> vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least >>>> does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. >>>> The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is >>>> currently running. The stale VS-stage TLB entry left on host cpu1 from >>>> when vcpu0 previously ran there would then remain. >>>> >>>> Later, still without any further vCPU migration, the process is >>>> scheduled inside the guest onto vcpu1, which remains on host cpu1, and >>>> can hit that stale entry. >>>> >>>> This also seems more likely when the ASID allocator is enabled, because >>>> set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So >>>> even the first time the process lands on vcpu1, a stale ASID-tagged >>>> entry on host cpu1 may still be present. >>>> >>>> In other words, I am not sure this case is covered by a full guest TLB >>>> flush to all VCPUs on the mapping update. Guest shootdown looks >>>> mm_cpumask-based, and host pCPU migration of a vCPU does not by itself >>>> add other vCPUs that never ran the mm into that mask. >>> >>> This makes sense now considering ASID allocator in Guest Linux is global >>> and not local. In any case, KVM RISC-V must not assume that the Guest OS >>> has local ASID allocator hence the kvm_riscv_local_tlb_sanitize() must also >>> santize TLB for other VCPUs of the same Guest which can potentially run >>> on the previous host CPU where current VCPU ran. >>> >>> Having said the above, there are multiple issues with the current patch: >>> 1) The kvm_riscv_local_hfence_gvma_vmid_all() is sufficient for most >>> implementations to sanitize both G-stage and VS-stage TLB entries >>> because both G-stage and VS-stage TLB entries are generally tagged >>> with VMID. The kvm_riscv_vsstage_tlb_no_gpa work-around is for >>> implmentations which do not tag VS-stage TLB entries with VMID >>> and this patch remove this work-around. >>> 2) The other VCPUs on the previous host CPU may be already running >>> and may still hit stale TLB entry before kvm_riscv_local_tlb_sanitize() >>> gets a chance to sanitize TLB on previous host CPU. >>> 3) The per-CPU pointer in struct kvm_arch is racy because we >>> may have two VCPUs on same host CPU which are migrated to >>> other host CPUs. Instead, it is much simpler to have an atomic >>> cpumask in each VCPU where any part of KVM can mark a host >>> CPU in the cpumask of the VCPU to force TLB sanitization whenever >>> the VCPU runs on that particular host CPU. >>> >>> How about the following change ? >>> >>> diff --git a/arch/riscv/include/asm/kvm_host.h >>> b/arch/riscv/include/asm/kvm_host.h >>> index a30600579231..c975eee7240c 100644 >>> --- a/arch/riscv/include/asm/kvm_host.h >>> +++ b/arch/riscv/include/asm/kvm_host.h >>> @@ -9,6 +9,7 @@ >>> #ifndef __RISCV_KVM_HOST_H__ >>> #define __RISCV_KVM_HOST_H__ >>> >>> +#include <linux/cpumask.h> >>> #include <linux/types.h> >>> #include <linux/kvm.h> >>> #include <linux/kvm_types.h> >>> @@ -184,6 +185,9 @@ struct kvm_vcpu_arch { >>> /* ISA feature bits (similar to MISA) */ >>> DECLARE_BITMAP(isa, RISCV_ISA_EXT_MAX); >>> >>> + /* Sanitize TLB host CPU mask */ >>> + cpumask_t tlb_sanitize_mask; >>> + >>> /* Vendor, Arch, and Implementation details */ >>> unsigned long mvendorid; >>> unsigned long marchid; >>> diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c >>> index 2ae34632cdcb..b1387a00c8b0 100644 >>> --- a/arch/riscv/kvm/tlb.c >>> +++ b/arch/riscv/kvm/tlb.c >>> @@ -207,11 +207,31 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) >>> >>> void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) >>> { >>> - unsigned long vmid; >>> + struct kvm_vcpu *tvcpu; >>> + unsigned long i, vmid; >>> >>> - if (!kvm_riscv_gstage_vmid_bits() || >>> - vcpu->arch.last_exit_cpu == vcpu->cpu) >>> - return; >>> + if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { >>> + if (!kvm_riscv_gstage_vmid_bits() || >>> + vcpu->arch.last_exit_cpu == vcpu->cpu) >>> + return; >>> + } >>> + >>> + /* >>> + * The VCPU might have left stale TLBs on the previous host CPU so >>> + * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. >>> + */ >>> + if (vcpu->arch.last_exit_cpu > -1 && vcpu->arch.last_exit_cpu != >>> vcpu->cpu) { >>> + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { >>> + if (tvcpu == vcpu) >>> + continue; >>> + cpumask_set_cpu(vcpu->arch.last_exit_cpu, >>> + &tvcpu->arch.tlb_sanitize_mask); Hi Anup, Thanks for the detailed analysis and for proposing the direction. I’d like to suggest a small refinement on top of your diff: add if (READ_ONCE(tvcpu->cpu) == vcpu->arch.last_exit_cpu) here so that we only kick the vCPUs that are currently running on the previous host CPU, while still setting tlb_sanitize_mask for all other vCPUs. Would that be OK? Regards, Bill >>> + kvm_vcpu_kick(tvcpu); >>> + } >>> + } >>> + >>> + /* Clear current host CPU from the tlb_sanitize_mask */ >>> + cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); >>> >>> /* >>> * On RISC-V platforms with hardware VMID support, we share same >>> diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c >>> index dd786f758eb7..c00dc0b191dc 100644 >>> --- a/arch/riscv/kvm/vcpu.c >>> +++ b/arch/riscv/kvm/vcpu.c >>> @@ -107,6 +107,8 @@ static void kvm_riscv_reset_vcpu(struct kvm_vcpu >>> *vcpu, bool kvm_sbi_reset) >>> >>> vcpu->arch.last_exit_cpu = -1; >>> >>> + cpumask_clear(&vcpu->arch.tlb_sanitize_mask); >>> + >>> kvm_riscv_vcpu_context_reset(vcpu, kvm_sbi_reset); >>> >>> kvm_riscv_vcpu_fp_reset(vcpu); >>> >>> Regards, >>> Anup >> >> Hi Anup, >> >> Thanks for the explanation and for suggesting the per-vCPU sanitization >> mask. Building on your proposal, I would like to suggest two changes: >> >> 1. Separate the invalidation strategy for vCPU migration and a mask hit. >> >> Keep HFENCE.GVMA for vCPU migration, identified by >> last_exit_cpu != vcpu->cpu, when hardware VMIDs are available. Use >> HFENCE.VVMA when the current host CPU is marked in tlb_sanitize_mask. >> A mask hit caused by another vCPU of the same VM running on this CPU >> requires VS-stage sanitization; it does not by itself require G-stage >> invalidation. >> >> For example, if vCPU0 and vCPU1 both last ran on CPU0, switching from >> vCPU1 back to vCPU0 only needs the mask-triggered VVMA. This avoids >> requesting G-stage invalidation when the shared G-stage mappings have >> not changed. If both migration and a mask hit occur, both fences run. > > HFENCE.GVMA is sufficient for most implementations so to sanitize all TLB > entries matching a particular VMID. > > Refer, the following text from RISC-V Privileged specification section "5.3.2. > Hypervisor Memory-Management Fence Instructions": > > Conceptually, an implementation might contain two > address-translation caches: one that > maps guest virtual addresses to guest physical addresses, and > another that maps guest > physical addresses to supervisor physical addresses. HFENCE.GVMA > need not flush the > former cache, but it must flush entries from the latter cache that > match the HFENCE.GVMA’s > address and VMID arguments. > > More commonly, implementations contain address-translation caches > that map guest virtual > addresses directly to supervisor physical addresses, removing a > level of indirection. For such > implementations, any entry whose guest virtual address maps to a > guest physical address that > matches the HFENCE.GVMA’s address and VMID arguments must be > flushed. Selectively > flushing entries in this fashion requires tagging them with the > guest physical address, which is > costly, and so a common technique is to flush all entries that match > the HFENCE.GVMA’s > VMID argument, regardless of the address argument. > > Doing HFENCE.GVMA in certain cases and HFENCE.VVMA other cases is > maintainence burden without much benefits. Also, VCPU migrating from one > host CPU to another is an infrequent event so no point over optimizing without > much benefits. > > In short, NACK to your suggestion #1. > >> >> 2. Move mask updates to kvm_arch_vcpu_put(). >> >> Mark the outgoing host CPU in the masks of the other vCPUs of the >> same VM when a vCPU is put. This also covers a switch between vCPUs >> on one host CPU without either vCPU migrating. Marking only when >> migration is detected in kvm_riscv_local_tlb_sanitize() would miss >> that case. >> >> The marking now happens on the outgoing CPU before another vCPU can >> enter guest mode there. The migration-time marking and kicks are >> therefore removed. The trade-off is an O(nr_vcpus) walk on each >> qualifying put(). >> >> How about the following change ? >> >> diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c >> index 2ac3c0a20fce..ae8a40b3429c 100644 >> --- a/arch/riscv/kvm/tlb.c >> +++ b/arch/riscv/kvm/tlb.c >> @@ -160,32 +160,15 @@ >> >> void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) >> { >> - struct kvm_vcpu *tvcpu; >> - unsigned long i, vmid; >> - >> - if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { >> - if (!kvm_riscv_gstage_vmid_bits() || >> - vcpu->arch.last_exit_cpu == vcpu->cpu) >> - return; >> - } >> - >> - /* >> - * The VCPU might have left stale TLBs on the previous host CPU so >> - * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. >> - */ >> - if (vcpu->arch.last_exit_cpu > -1 && >> - vcpu->arch.last_exit_cpu != vcpu->cpu) { >> - kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { >> - if (tvcpu == vcpu) >> - continue; >> - cpumask_set_cpu(vcpu->arch.last_exit_cpu, >> - &tvcpu->arch.tlb_sanitize_mask); >> - kvm_vcpu_kick(tvcpu); >> - } >> - } >> - >> - /* Clear current host CPU from the tlb_sanitize_mask */ >> - cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); >> + unsigned long vmid; >> + bool vcpu_migrated, sanitize_vsstage; >> + >> + vcpu_migrated = vcpu->arch.last_exit_cpu != vcpu->cpu; >> + sanitize_vsstage = cpumask_test_and_clear_cpu(vcpu->cpu, >> + &vcpu->arch.tlb_sanitize_mask); >> + if (!sanitize_vsstage && >> + (!kvm_riscv_gstage_vmid_bits() || !vcpu_migrated)) >> + return; >> >> /* >> * On RISC-V platforms with hardware VMID support, we share same >> @@ -199,13 +182,16 @@ >> */ >> >> vmid = READ_ONCE(vcpu->kvm->arch.vmid.vmid); >> - kvm_riscv_local_hfence_gvma_vmid_all(vmid); >> + if (vcpu_migrated && kvm_riscv_gstage_vmid_bits()) >> + kvm_riscv_local_hfence_gvma_vmid_all(vmid); >> >> /* >> - * Flush VS-stage TLB entries for implementation where VS-stage >> - * TLB does not cahce guest physical address and VMID. >> + * Flush VS-stage entries marked when another VCPU left this CPU, or when >> + * required by the split-TLB vendor workaround on this VCPU's migration. >> */ >> - if (static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa)) >> + if (sanitize_vsstage || >> + (vcpu_migrated && >> + static_branch_unlikely(&kvm_riscv_vsstage_tlb_no_gpa))) >> kvm_riscv_local_hfence_vvma_all(vmid); >> } >> >> diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c >> index f2e52f3b0786..6209c66b24e1 100644 >> --- a/arch/riscv/kvm/vcpu.c >> +++ b/arch/riscv/kvm/vcpu.c >> @@ -644,8 +644,18 @@ >> >> void kvm_arch_vcpu_put(struct kvm_vcpu *vcpu) >> { >> + struct kvm_vcpu *tvcpu; >> + unsigned long i; >> + int cpu = vcpu->cpu; >> void *nsh; >> struct kvm_vcpu_csr *csr = &vcpu->arch.guest_csr; >> + >> + if (cpu >= 0 && vcpu->arch.last_exit_cpu == cpu) { >> + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { >> + if (tvcpu != vcpu) >> + cpumask_set_cpu(cpu, &tvcpu->arch.tlb_sanitize_mask); >> + } >> + } >> >> vcpu->cpu = -1; >> >> Would these changes make sense to you, or is there anythingI might >> be overlooking? > > The kvm_arch_vcpu_put() is called every time a VCPU exits to > user space for MMIO emulation and handling other exits. Now, > imagine VCPU0 and VCPU1 running on same host CPU where > VCPU0 is doing lot of MMIO exits causing VCPU1 to sanitize > TLB frequently on the same host CPU. > > Based on above, NACK to your suggestion #2 > > Regards, > Anup ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-28 7:15 ` BillXiang @ 2026-10-28 10:23 ` Anup Patel 0 siblings, 0 replies; 12+ messages in thread From: Anup Patel @ 2026-10-28 10:23 UTC (permalink / raw) To: BillXiang Cc: guoyaxing, Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso On Wed, Oct 28, 2026 at 12:46 PM BillXiang <xiangwencheng@lanxincomputing.com> wrote: > > On 10/28/2026 2:14 PM, Anup Patel wrote: > > On Mon, Oct 26, 2026 at 3:49 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > >> > >> > >> > >> 在 2026/10/24 23:49, Anup Patel 写道: > >>> On Sat, Oct 10, 2026 at 2:36 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > >>>> > >>>> > >>>> > >>>> 在 2026/10/10 15:50, Anup Patel 写道: > >>>>> On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: > >>>>>> > >>>>>> > >>>>>> > >>>>>> 在 2026/10/10 02:51, Anup Patel 写道: > >>>>>>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: > >>>>>>>> > >>>>>>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be > >>>>>>>> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs > >>>>>>>> that previously backed a vCPU. > >>>>>>>> > >>>>>>>> This can miss stale VS-stage TLB entries when a vCPU migrates across host > >>>>>>>> CPUs. For example: > >>>>>>>> > >>>>>>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. > >>>>>>>> - vcpu0 migrates to host CPU0. > >>>>>>>> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. > >>>>>>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. > >>>>>>> > >>>>>>> When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() > >>>>>>> will ensure TLB entries associated with Guest/VM are nuked. > >>>>>>> > >>>>>>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA > >>>>>>> for the associated VMID because most RISC-V H-extension implemenation will > >>>>>>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both > >>>>>>> G-stage and VS-stage TLB entries for given VMID. > >>>>>>> > >>>>>>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy > >>>>>>> / inefficient > >>>>>>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in > >>>>>>> kvm_riscv_setup_vendor_features() > >>>>>>> as work-around. > >>>>>>> > >>>>>>> NACK to this patch from myside. > >>>>>>> > >>>>>>> Regards, > >>>>>>> Anup > >>>>>>> > >>>>>> Hi Anup, > >>>>>> Thanks for the review and clarification. > >>>>>> > >>>>>> Regarding “When the vcpu0 is brought back to host CPU1”: the failure > >>>>>> we hit is not caused by vcpu0 being brought back onto host CPU1. In our > >>>>>> case, after vcpu0 has already migrated away, neither vCPU migrates > >>>>>> again. The guest process is later scheduled inside the multi-vCPU guest > >>>>>> onto vcpu1, which is still running on host CPU1. So the stale TLB is > >>>>>> re-hit without any further KVM vCPU migration, and > >>>>>> kvm_riscv_local_tlb_sanitize() is not involved on that path. > >>>>>> > >>>>>> My commit message was a bit too terse on this point. The concrete > >>>>>> sequence we observed was described earlier in the RFC discussion: > >>>>>> > >>>>>> > >>>>>> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ > >>>>>> > >>>>>> In short, the issue is: because vcpu0 migrated away, cpu1 still has > >>>>>> stale TLB entries while cpu0 has the new mapping. After that, neither > >>>>>> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 > >>>>>> (which is on cpu1), causing it to hit the stale TLB. Consider the > >>>>>> following scenario: > >>>>>> > >>>>>> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is > >>>>>> on cpu1, and a process on it reads a page, which gets cached in the TLB > >>>>>> (let's call this the "stale TLB entry"). > >>>>>> > >>>>>> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on > >>>>>> cpu0) does a CoW (copy-on-write) and establishes a new mapping. > >>>>>> > >>>>>> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process > >>>>>> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the > >>>>>> same page, and hits the stale TLB entry. > >>>>> > >>>>> This cannot happen because in step#2 when mapping was updated the > >>>>> Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 > >>>>> will also receive TLB flush and the stale TLB will be removed. This means > >>>>> when process is moved back to vcpu1 on cpu0, HW will fetch updated > >>>>> PTE and new TLB entry will be populated. > >>>>> > >>>> > >>>> Hi Anup, > >>>> > >>>> Thanks for the follow-up. I may be missing something, so I wanted to > >>>> double-check this against the guest TLB shootdown path. > >>>> > >>>> If I follow the Linux guest CoW path correctly, when the mapping is > >>>> updated in step#2 it goes through: > >>>> > >>>> wp_page_copy() > >>>> -> ptep_clear_flush() > >>>> -> flush_tlb_page() > >>>> -> __flush_tlb_range(mm, mm_cpumask(mm), ...) > >>>> > >>>> So the remote SFENCE.VMA appears to be targeted only at CPUs in > >>>> mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is > >>>> updated in switch_mm()/set_mm() for guest CPUs that have actually run > >>>> that mm. > >>>> > >>>> In our case, before the CoW the process had only ever run on vcpu0. From > >>>> the guest's point of view it did not migrate: the host migration of > >>>> vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees > >>>> vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least > >>>> does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. > >>>> The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is > >>>> currently running. The stale VS-stage TLB entry left on host cpu1 from > >>>> when vcpu0 previously ran there would then remain. > >>>> > >>>> Later, still without any further vCPU migration, the process is > >>>> scheduled inside the guest onto vcpu1, which remains on host cpu1, and > >>>> can hit that stale entry. > >>>> > >>>> This also seems more likely when the ASID allocator is enabled, because > >>>> set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So > >>>> even the first time the process lands on vcpu1, a stale ASID-tagged > >>>> entry on host cpu1 may still be present. > >>>> > >>>> In other words, I am not sure this case is covered by a full guest TLB > >>>> flush to all VCPUs on the mapping update. Guest shootdown looks > >>>> mm_cpumask-based, and host pCPU migration of a vCPU does not by itself > >>>> add other vCPUs that never ran the mm into that mask. > >>> > >>> This makes sense now considering ASID allocator in Guest Linux is global > >>> and not local. In any case, KVM RISC-V must not assume that the Guest OS > >>> has local ASID allocator hence the kvm_riscv_local_tlb_sanitize() must also > >>> santize TLB for other VCPUs of the same Guest which can potentially run > >>> on the previous host CPU where current VCPU ran. > >>> > >>> Having said the above, there are multiple issues with the current patch: > >>> 1) The kvm_riscv_local_hfence_gvma_vmid_all() is sufficient for most > >>> implementations to sanitize both G-stage and VS-stage TLB entries > >>> because both G-stage and VS-stage TLB entries are generally tagged > >>> with VMID. The kvm_riscv_vsstage_tlb_no_gpa work-around is for > >>> implmentations which do not tag VS-stage TLB entries with VMID > >>> and this patch remove this work-around. > >>> 2) The other VCPUs on the previous host CPU may be already running > >>> and may still hit stale TLB entry before kvm_riscv_local_tlb_sanitize() > >>> gets a chance to sanitize TLB on previous host CPU. > >>> 3) The per-CPU pointer in struct kvm_arch is racy because we > >>> may have two VCPUs on same host CPU which are migrated to > >>> other host CPUs. Instead, it is much simpler to have an atomic > >>> cpumask in each VCPU where any part of KVM can mark a host > >>> CPU in the cpumask of the VCPU to force TLB sanitization whenever > >>> the VCPU runs on that particular host CPU. > >>> > >>> How about the following change ? > >>> > >>> diff --git a/arch/riscv/include/asm/kvm_host.h > >>> b/arch/riscv/include/asm/kvm_host.h > >>> index a30600579231..c975eee7240c 100644 > >>> --- a/arch/riscv/include/asm/kvm_host.h > >>> +++ b/arch/riscv/include/asm/kvm_host.h > >>> @@ -9,6 +9,7 @@ > >>> #ifndef __RISCV_KVM_HOST_H__ > >>> #define __RISCV_KVM_HOST_H__ > >>> > >>> +#include <linux/cpumask.h> > >>> #include <linux/types.h> > >>> #include <linux/kvm.h> > >>> #include <linux/kvm_types.h> > >>> @@ -184,6 +185,9 @@ struct kvm_vcpu_arch { > >>> /* ISA feature bits (similar to MISA) */ > >>> DECLARE_BITMAP(isa, RISCV_ISA_EXT_MAX); > >>> > >>> + /* Sanitize TLB host CPU mask */ > >>> + cpumask_t tlb_sanitize_mask; > >>> + > >>> /* Vendor, Arch, and Implementation details */ > >>> unsigned long mvendorid; > >>> unsigned long marchid; > >>> diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c > >>> index 2ae34632cdcb..b1387a00c8b0 100644 > >>> --- a/arch/riscv/kvm/tlb.c > >>> +++ b/arch/riscv/kvm/tlb.c > >>> @@ -207,11 +207,31 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) > >>> > >>> void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > >>> { > >>> - unsigned long vmid; > >>> + struct kvm_vcpu *tvcpu; > >>> + unsigned long i, vmid; > >>> > >>> - if (!kvm_riscv_gstage_vmid_bits() || > >>> - vcpu->arch.last_exit_cpu == vcpu->cpu) > >>> - return; > >>> + if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { > >>> + if (!kvm_riscv_gstage_vmid_bits() || > >>> + vcpu->arch.last_exit_cpu == vcpu->cpu) > >>> + return; > >>> + } > >>> + > >>> + /* > >>> + * The VCPU might have left stale TLBs on the previous host CPU so > >>> + * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. > >>> + */ > >>> + if (vcpu->arch.last_exit_cpu > -1 && vcpu->arch.last_exit_cpu != > >>> vcpu->cpu) { > >>> + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { > >>> + if (tvcpu == vcpu) > >>> + continue; > >>> + cpumask_set_cpu(vcpu->arch.last_exit_cpu, > >>> + &tvcpu->arch.tlb_sanitize_mask); > > Hi Anup, > > Thanks for the detailed analysis and for proposing the direction. I’d > like to suggest a small refinement on top of your diff: add > > if (READ_ONCE(tvcpu->cpu) == vcpu->arch.last_exit_cpu) > > here so that we only kick the vCPUs that are currently running on the > previous host CPU, while still setting tlb_sanitize_mask for all other > vCPUs. Would that be OK? I am okay with this refinement but please add a comment about this check. Regards, Anup ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU 2026-10-24 15:49 ` Anup Patel 2026-10-26 10:18 ` guoyaxing @ 2026-10-29 4:05 ` BillXiang 1 sibling, 0 replies; 12+ messages in thread From: BillXiang @ 2026-10-29 4:05 UTC (permalink / raw) To: Anup Patel, guoyaxing Cc: Atish Patra, Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti, kvm, kvm-riscv, linux-riscv, linux-kernel, majiuyue, anzoso, BillXiang On 10/24/2026 11:49 PM, Anup Patel wrote: > On Sat, Oct 10, 2026 at 2:36 PM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >> >> >> >> 在 2026/10/10 15:50, Anup Patel 写道: >>> On Sat, Oct 10, 2026 at 4:12 AM guoyaxing <guoyaxing@bosc.ac.cn> wrote: >>>> >>>> >>>> >>>> 在 2026/10/10 02:51, Anup Patel 写道: >>>>> On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing@bosc.ac.cn> wrote: >>>>>> >>>>>> Guest Linux uses mm_cpumask to decide whether an sfence.vma needs to be >>>>>> local or remote. Under KVM that mask tracks guest CPUs, not the host CPUs >>>>>> that previously backed a vCPU. >>>>>> >>>>>> This can miss stale VS-stage TLB entries when a vCPU migrates across host >>>>>> CPUs. For example: >>>>>> >>>>>> - vcpu0 runs on host CPU1 and fills a VS-stage TLB entry. >>>>>> - vcpu0 migrates to host CPU0. >>>>>> - the guest updates the mapping on vcpu0 and issues a local sfence.vma. >>>>>> - later, the guest task runs on vcpu1 while vcpu1 is backed by host CPU1. >>>>> >>>>> When the vcpu0 is brought back to host CPU1, the kvm_riscv_local_tlb_sanitize() >>>>> will ensure TLB entries associated with Guest/VM are nuked. >>>>> >>>>> By default, kvm_riscv_local_hfence_gvma_vmid_all() will only do HFENCE.GVMA >>>>> for the associated VMID because most RISC-V H-extension implemenation will >>>>> tag VS-stage TLB entries with VMID and HFENCE.GVMA must remove both >>>>> G-stage and VS-stage TLB entries for given VMID. >>>>> >>>>> If your HW does not tag VS-stage TLB enteries with VMID then it buggy >>>>> / inefficient >>>>> so you have to set kvm_riscv_vsstage_tlb_no_gpa in >>>>> kvm_riscv_setup_vendor_features() >>>>> as work-around. >>>>> >>>>> NACK to this patch from myside. >>>>> >>>>> Regards, >>>>> Anup >>>>> >>>> Hi Anup, >>>> Thanks for the review and clarification. >>>> >>>> Regarding “When the vcpu0 is brought back to host CPU1”: the failure >>>> we hit is not caused by vcpu0 being brought back onto host CPU1. In our >>>> case, after vcpu0 has already migrated away, neither vCPU migrates >>>> again. The guest process is later scheduled inside the multi-vCPU guest >>>> onto vcpu1, which is still running on host CPU1. So the stale TLB is >>>> re-hit without any further KVM vCPU migration, and >>>> kvm_riscv_local_tlb_sanitize() is not involved on that path. >>>> >>>> My commit message was a bit too terse on this point. The concrete >>>> sequence we observed was described earlier in the RFC discussion: >>>> >>>> >>>> https://lore.kernel.org/kvm/0d15b098-b8f7-4080-9f33-e8c49c2331e1@bosc.ac.cn/ >>>> >>>> In short, the issue is: because vcpu0 migrated away, cpu1 still has >>>> stale TLB entries while cpu0 has the new mapping. After that, neither >>>> vcpu migrated again, but the process was scheduled from vcpu0 to vcpu1 >>>> (which is on cpu1), causing it to hit the stale TLB. Consider the >>>> following scenario: >>>> >>>> 1. vcpu0 and vcpu1 are both running on cpu1. At this point, vcpu0 is >>>> on cpu1, and a process on it reads a page, which gets cached in the TLB >>>> (let's call this the "stale TLB entry"). >>>> >>>> 2. vcpu0 migrates to cpu0, and then the process (on vcpu0, now on >>>> cpu0) does a CoW (copy-on-write) and establishes a new mapping. >>>> >>>> 3. Neither vcpu0 nor vcpu1 migrates again. At this point, the process >>>> gets scheduled inside the VM onto vcpu1 (which is on cpu1), reads the >>>> same page, and hits the stale TLB entry. >>> >>> This cannot happen because in step#2 when mapping was updated the >>> Guest OS will do TLB flush for all its VCPUs which means vcpu1 on cpu0 >>> will also receive TLB flush and the stale TLB will be removed. This means >>> when process is moved back to vcpu1 on cpu0, HW will fetch updated >>> PTE and new TLB entry will be populated. >>> >> >> Hi Anup, >> >> Thanks for the follow-up. I may be missing something, so I wanted to >> double-check this against the guest TLB shootdown path. >> >> If I follow the Linux guest CoW path correctly, when the mapping is >> updated in step#2 it goes through: >> >> wp_page_copy() >> -> ptep_clear_flush() >> -> flush_tlb_page() >> -> __flush_tlb_range(mm, mm_cpumask(mm), ...) >> >> So the remote SFENCE.VMA appears to be targeted only at CPUs in >> mm_cpumask(mm), rather than at all VCPUs of the guest. mm_cpumask is >> updated in switch_mm()/set_mm() for guest CPUs that have actually run >> that mm. >> >> In our case, before the CoW the process had only ever run on vcpu0. From >> the guest's point of view it did not migrate: the host migration of >> vcpu0 from cpu1 to cpu0 is not visible to guest Linux, which only sees >> vCPUs. So at CoW time mm_cpumask still contains only vcpu0 (or at least >> does not contain vcpu1), and __flush_tlb_range() would not IPI vcpu1. >> The local SFENCE on vcpu0 would only cover host cpu0, where vcpu0 is >> currently running. The stale VS-stage TLB entry left on host cpu1 from >> when vcpu0 previously ran there would then remain. >> >> Later, still without any further vCPU migration, the process is >> scheduled inside the guest onto vcpu1, which remains on host cpu1, and >> can hit that stale entry. >> >> This also seems more likely when the ASID allocator is enabled, because >> set_mm_asid() does not always local_flush_tlb_all() on switch_mm(). So >> even the first time the process lands on vcpu1, a stale ASID-tagged >> entry on host cpu1 may still be present. >> >> In other words, I am not sure this case is covered by a full guest TLB >> flush to all VCPUs on the mapping update. Guest shootdown looks >> mm_cpumask-based, and host pCPU migration of a vCPU does not by itself >> add other vCPUs that never ran the mm into that mask. Hi Anup, I have a few more questions about the issues and changes you raised. > > This makes sense now considering ASID allocator in Guest Linux is global > and not local. In any case, KVM RISC-V must not assume that the Guest OS > has local ASID allocator hence the kvm_riscv_local_tlb_sanitize() must also > santize TLB for other VCPUs of the same Guest which can potentially run > on the previous host CPU where current VCPU ran. > > Having said the above, there are multiple issues with the current patch: > 1) The kvm_riscv_local_hfence_gvma_vmid_all() is sufficient for most > implementations to sanitize both G-stage and VS-stage TLB entries > because both G-stage and VS-stage TLB entries are generally tagged > with VMID. The kvm_riscv_vsstage_tlb_no_gpa work-around is for > implmentations which do not tag VS-stage TLB entries with VMID > and this patch remove this work-around. > 2) The other VCPUs on the previous host CPU may be already running > and may still hit stale TLB entry before kvm_riscv_local_tlb_sanitize() > gets a chance to sanitize TLB on previous host CPU. First, the migrated vCPU have run on the previous host CPU before the other vCPUs do, and kvm_riscv_local_tlb_sanitize() will have been called before the other vCPUs run on that previous host CPU. Therefore, with vcpu_switched in Yaxing's original patch, no stale TLB entries remain. > 3) The per-CPU pointer in struct kvm_arch is racy because we > may have two VCPUs on same host CPU which are migrated to Seconed, kvm_riscv_local_tlb_sanitize() is called under preempt_disabled() in kvm_arch_vcpu_ioctl_run, so it is not racy between two vCPUs. > other host CPUs. Instead, it is much simpler to have an atomic > cpumask in each VCPU where any part of KVM can mark a host > CPU in the cpumask of the VCPU to force TLB sanitization whenever > the VCPU runs on that particular host CPU. > > How about the following change ? > > diff --git a/arch/riscv/include/asm/kvm_host.h > b/arch/riscv/include/asm/kvm_host.h > index a30600579231..c975eee7240c 100644 > --- a/arch/riscv/include/asm/kvm_host.h > +++ b/arch/riscv/include/asm/kvm_host.h > @@ -9,6 +9,7 @@ > #ifndef __RISCV_KVM_HOST_H__ > #define __RISCV_KVM_HOST_H__ > > +#include <linux/cpumask.h> > #include <linux/types.h> > #include <linux/kvm.h> > #include <linux/kvm_types.h> > @@ -184,6 +185,9 @@ struct kvm_vcpu_arch { > /* ISA feature bits (similar to MISA) */ > DECLARE_BITMAP(isa, RISCV_ISA_EXT_MAX); > > + /* Sanitize TLB host CPU mask */ > + cpumask_t tlb_sanitize_mask; > + > /* Vendor, Arch, and Implementation details */ > unsigned long mvendorid; > unsigned long marchid; > diff --git a/arch/riscv/kvm/tlb.c b/arch/riscv/kvm/tlb.c > index 2ae34632cdcb..b1387a00c8b0 100644 > --- a/arch/riscv/kvm/tlb.c > +++ b/arch/riscv/kvm/tlb.c > @@ -207,11 +207,31 @@ void kvm_riscv_local_hfence_vvma_all(unsigned long vmid) > > void kvm_riscv_local_tlb_sanitize(struct kvm_vcpu *vcpu) > { > - unsigned long vmid; > + struct kvm_vcpu *tvcpu; > + unsigned long i, vmid; > > - if (!kvm_riscv_gstage_vmid_bits() || > - vcpu->arch.last_exit_cpu == vcpu->cpu) > - return; > + if (!cpumask_test_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask)) { > + if (!kvm_riscv_gstage_vmid_bits() || > + vcpu->arch.last_exit_cpu == vcpu->cpu) > + return; > + } > + > + /* > + * The VCPU might have left stale TLBs on the previous host CPU so > + * mark previous host CPU in the tlb_sanitize_mask of other VCPUs. > + */ > + if (vcpu->arch.last_exit_cpu > -1 && vcpu->arch.last_exit_cpu != > vcpu->cpu) { > + kvm_for_each_vcpu(i, tvcpu, vcpu->kvm) { > + if (tvcpu == vcpu) > + continue; > + cpumask_set_cpu(vcpu->arch.last_exit_cpu, > + &tvcpu->arch.tlb_sanitize_mask); > + kvm_vcpu_kick(tvcpu); Last, vcpu_kick is an asynchronous operation, so there is still a chance that the other vCPUs will hit stale TLB entries. Please let me know what you think. Thanks in advance! Regards, Bill > + } > + } > + > + /* Clear current host CPU from the tlb_sanitize_mask */ > + cpumask_clear_cpu(vcpu->cpu, &vcpu->arch.tlb_sanitize_mask); > > /*_kick > * On RISC-V platforms with hardware VMID support, we share same > diff --git a/arch/riscv/kvm/vcpu.c b/arch/riscv/kvm/vcpu.c > index dd786f758eb7..c00dc0b191dc 100644 > --- a/arch/riscv/kvm/vcpu.c > +++ b/arch/riscv/kvm/vcpu.c > @@ -107,6 +107,8 @@ static void kvm_riscv_reset_vcpu(struct kvm_vcpu > *vcpu, bool kvm_sbi_reset) > > vcpu->arch.last_exit_cpu = -1; > > + cpumask_clear(&vcpu->arch.tlb_sanitize_mask); > + > kvm_riscv_vcpu_context_reset(vcpu, kvm_sbi_reset); > > kvm_riscv_vcpu_fp_reset(vcpu); > > Regards, > Anup > > _______________________________________________ > linux-riscv mailing list > linux-riscv@lists.infradead.org > http://lists.infradead.org/mailman/listinfo/linux-riscv ^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-10-29 4:05 UTC | newest] Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-08-17 10:24 [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU Yaxing Guo 2026-10-09 18:51 ` Anup Patel 2026-10-10 2:11 ` guoyaxing 2026-10-10 7:50 ` Anup Patel 2026-10-10 9:06 ` guoyaxing 2026-10-21 9:32 ` BillXiang 2026-10-24 15:49 ` Anup Patel 2026-10-26 10:18 ` guoyaxing 2026-10-28 6:14 ` Anup Patel 2026-10-28 7:15 ` BillXiang 2026-10-28 10:23 ` Anup Patel 2026-10-29 4:05 ` BillXiang
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®