[PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU

Anup Patel anup at brainfault.org
Fri Oct 9 11:51:13 PDT 2026


On Mon, Aug 17, 2026 at 12:25 PM Yaxing Guo <guoyaxing at 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 at outlook.com>
> Reviewed-by: Jiuyue Ma <majiuyue at bosc.ac.cn>
> Signed-off-by: Yaxing Guo <guoyaxing at 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
>



More information about the kvm-riscv mailing list