From: "BillXiang" <xiangwencheng@lanxincomputing.com>
To: "guoyaxing" <guoyaxing@bosc.ac.cn>, "Anup Patel" <anup@brainfault.org>
Cc: "Atish Patra" <atish.patra@linux.dev>,
"Paul Walmsley" <pjw@kernel.org>,
"Palmer Dabbelt" <palmer@dabbelt.com>,
"Albert Ou" <aou@eecs.berkeley.edu>,
"Alexandre Ghiti" <alex@ghiti.fr>, <kvm@vger.kernel.org>,
<kvm-riscv@lists.infradead.org>,
<linux-riscv@lists.infradead.org>,
<linux-kernel@vger.kernel.org>, <majiuyue@bosc.ac.cn>,
<anzoso@outlook.com>
Subject: Re: [PATCH] RISC-V: KVM: Flush VS-stage TLB before reusing a host CPU
Date: Wed, 21 Oct 2026 17:32:44 +0800 [thread overview]
Message-ID: <bddcd13b-b97d-4c24-8f08-e0a239065e68@lanxincomputing.com> (raw)
In-Reply-To: <c050c85c-482a-4351-b627-ac074f7f7952@bosc.ac.cn>
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
prev parent reply other threads:[~2026-10-21 9:32 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-17 10:24 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 [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=bddcd13b-b97d-4c24-8f08-e0a239065e68@lanxincomputing.com \
--to=xiangwencheng@lanxincomputing.com \
--cc=alex@ghiti.fr \
--cc=anup@brainfault.org \
--cc=anzoso@outlook.com \
--cc=aou@eecs.berkeley.edu \
--cc=atish.patra@linux.dev \
--cc=guoyaxing@bosc.ac.cn \
--cc=kvm-riscv@lists.infradead.org \
--cc=kvm@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-riscv@lists.infradead.org \
--cc=majiuyue@bosc.ac.cn \
--cc=palmer@dabbelt.com \
--cc=pjw@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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®