From 3cb0cf6ff370e518dfe4c3cdbfb19f3bf6448fe9 Mon Sep 17 00:00:00 2001 From: Farrah Chen Date: Mon, 22 Dec 2025 20:37:40 +0800 Subject: [PATCH] KVM: Add KVM ras SRAO case Signed-off-by: Farrah Chen --- KVM/qemu/deps/ras/victim.c | 370 +++++++++++++++++++++++++++++++++++++ KVM/qemu/ras.cfg | 17 ++ KVM/qemu/tests/ras.py | 127 +++++++++++++ 3 files changed, 514 insertions(+) create mode 100755 KVM/qemu/deps/ras/victim.c create mode 100644 KVM/qemu/ras.cfg create mode 100644 KVM/qemu/tests/ras.py diff --git a/KVM/qemu/deps/ras/victim.c b/KVM/qemu/deps/ras/victim.c new file mode 100755 index 00000000..d2b81848 --- /dev/null +++ b/KVM/qemu/deps/ras/victim.c @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2012-2015 Intel corporation + * + * Set up to get zapped by a machine check (injected elsewhere) + * To use this test case please ensure your SUT(System Under Test) + * can support MCE/SRAR. + * + * Author: + * Tony Luck + * Gong Chen + * Wen Jin + * + * Copy from mce-test + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Definition of /proc/pid/pagemap + * Bits 0-54 page frame number (PFN) if present + * Bits 0-4 swap type if swapped + * Bits 5-54 swap offset if swapped + * Bits 55-60 page shift, the bits definition is legacy. + * Bit 61 reserved for future use + * Bit 62 page swapped + * Bit 63 page present + */ + +struct pagemaps { + unsigned long long pfn:55; + unsigned long long pgshift:6; /*legacy*/ + unsigned long long rsvd:1; + unsigned long long swapped:1; + unsigned long long present:1; +}; + +static int pagesize; + +/* + * dummyfunc size should be less than one page after complied, + * otherwise, caller will not return from this function + */ +void dummyfunc(void) +{ + int fatarray[64]; + + fatarray[0] = 0xdeadbeaf; + fatarray[8] = 0xdeadbeaf; + fatarray[16] = 0xdeadbeaf; + fatarray[32] = 0xdeadbeaf; +} + +/* + * get information about address from /proc/{pid}/pagemap + */ +unsigned long long vtop(unsigned long long addr, pid_t pid) +{ + struct pagemaps pinfo; + unsigned int pinfo_size = sizeof(pinfo); + unsigned long long offset = addr / pagesize * pinfo_size; + int fd, pgmask; + char pagemapname[64]; + + sprintf(pagemapname, "/proc/%d/pagemap", pid); + fd = open(pagemapname, O_RDONLY); + if (fd == -1) { + perror(pagemapname); + return 0; + } + if (pread(fd, (void *)&pinfo, pinfo_size, offset) != pinfo_size) { + perror(pagemapname); + close(fd); + return 0; + } + close(fd); + pgmask = pagesize - 1; + return (pinfo.pfn * pagesize) | (addr & pgmask); +} + +static void usage(void) +{ + int i; + static const char *const msg[] = { + "victim [options]", + "-a|--address vaddr=val, pid=val Translate process virtual address into physical address", + "-d|--data Inject data error(DCU error) under user context", + "-i|--instruction Inject instruction error(IFU error) under user context", + "-k|--kick 0/1 Kick off trigger. Auto(0), Manual(1, by default)", + "-p|--pfa test memory PFA(Predictive Failure Analysis) function", + "-h|--help Show this help message", + NULL, + }; + + for (i = 0; msg[i]; i++) + printf("%s\n", msg[i]); +} + +static const struct option opts[] = { + { "address", 1, NULL, 'a' }, + { "data", 0, NULL, 'd' }, + { "instruction", 0, NULL, 'i' }, + { "help", 0, NULL, 'h' }, + { "kick", 1, NULL, 'k' }, + { "pfa", 0, NULL, 'p' }, + { NULL, 0, NULL, 0 } +}; + +static void pfa_helper(char *p, pid_t pid, unsigned long long old_phys) +{ + int i; + int total; + unsigned long long new_phys; + + for (;;) { + for (i = 0; i < pagesize; i += sizeof(int)) { + total += *(int *)(p + i); + *(int *)(p + i) = total; + } + + new_phys = vtop((unsigned long long)p, pid); + if (old_phys == new_phys) { + for (i = 0; i < pagesize; i += sizeof(int)) { + total += *(int *)(p + i); + *(int *)(p + i) = i; + } + sleep(2); + new_phys = vtop((unsigned long long)p, pid); + if (old_phys != new_phys) { + printf("Page was replaced. New physical address = 0x%llx\n", + new_phys); + fflush(stdout); + old_phys = new_phys; + } + } else { + printf("Page was replaced. New physical address = 0x%llx\n", new_phys); + fflush(stdout); + old_phys = new_phys; + } + } +} + +static int parse_addr_subopts(char *subopts, unsigned long long *virt, + pid_t *pid) +{ + int err = 0; + int index; + enum { + I_VADDR = 0, + I_PID + }; + char *const token[] = { + [I_VADDR] = "vaddr", + [I_PID] = "pid", + NULL + }; + char *p = subopts; + char *subval; + char *svaddr; + char *spid; + + while (*p != '\0' && !err) { + index = getsubopt(&p, token, &subval); + switch (index) { + case I_VADDR: + if (subval) { + svaddr = subval; + break; + } + fprintf(stderr, + "miss value for %s\n", + token[I_VADDR]); + err++; + continue; + case I_PID: + if (subval) { + spid = subval; + break; + } + fprintf(stderr, + "miss value for %s\n", + token[I_PID]); + err++; + continue; + default: + err++; + break; + } + } + if (err > 0) { + usage(); + return 1; + } + errno = 0; + *virt = strtoull(svaddr, NULL, 0); + if ((*virt == 0 && svaddr[0] != '0') || errno != 0) { + fprintf(stderr, "Invalid virtual address: %s\n", + svaddr); + return 1; + } + errno = 0; + *pid = strtoul(spid, NULL, 0); + if ((*pid == 0 && spid[0] != '0') || errno != 0) { + fprintf(stderr, "Invalid process pid number: %s\n", + spid); + return 1; + } + return 0; +} + +/* + * The "SRAR DCU" test case failed on a CLX-AP server. It's root caused + * that the gcc v8.2.1 optimized out the access to the injected location. + * + * If keep "total" as a local, even mark it "volatile", the gcc v8.2.1 + * still optimizes out the memory access. Therefore, move the "total" from + * being a local variable to a global to avoid such optimization. + */ +long total; + +int main(int argc, char **argv) +{ + unsigned long long virt, phys; + char *buf; + int c, i; + int iflag = 0, dflag = 0; + int kick = 1; + int pfa = 0; + pid_t pid; + const char *trigger = "./trigger_start"; + const char *trigger_flag = "trigger"; + int fd; + int count = 100; + char trigger_buf[16]; + char answer[16]; + time_t now; + + if (argc <= 1) { + usage(); + return 0; + } + + pagesize = getpagesize(); + + while ((c = getopt_long(argc, argv, "a:dihk:p", opts, NULL)) != -1) { + switch (c) { + case 'a': + if (parse_addr_subopts(optarg, &virt, &pid) == 0) { + phys = vtop(virt, pid); + printf("physical address of (%d,0x%llx) = 0x%llx\n", + pid, virt, phys); + return 0; + } + return 1; + case 'd': + dflag = 1; + break; + case 'i': + iflag = 1; + break; + case 'k': + errno = 0; + kick = strtol(optarg, NULL, 0); + if ((kick == 0 && optarg[0] != '0') || errno != 0) { + fprintf(stderr, "Invalid parameter: %s\n", optarg); + return 1; + } + if (kick != 0 && kick != 1) { + fprintf(stderr, "Invalid parameter: %s\n", optarg); + return 1; + } + break; + case 'p': + pfa = 1; + break; + case 'h': + default: + usage(); + return 0; + } + } + + /* The MAP_LOCKED flag should not be used here, because it will cause a failure + * in KVM mce-inject test. But to prevent the mapped buffer from being swapped out, + * the system under test should not run in a heavy load environment. + */ + buf = mmap(NULL, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + + if (buf == MAP_FAILED) { + fprintf(stderr, "Can't get a single page of memory!\n"); + return 1; + } + memset(buf, '*', pagesize); + pid = getpid(); + phys = vtop((unsigned long long)buf, pid); + if (phys == 0) { + fprintf(stderr, "Can't get physical address of the page!\n"); + return 1; + } + + if (iflag) + memcpy(buf, (void *)dummyfunc, pagesize); + + printf("physical address of (0x%llx) = 0x%llx\n", + (unsigned long long)buf, phys); + fflush(stdout); + + if (pfa == 1) + pfa_helper(buf, pid, phys); + + if (kick == 0) { + errno = 0; + if (unlink(trigger) < 0 && errno != ENOENT) { + fprintf(stderr, "fail to remove trigger file\n"); + return 1; + } + memset(trigger_buf, 0, sizeof(trigger_buf)); + while (count--) { + fd = open(trigger, O_RDONLY); + if (fd < 0) { + sleep(1); + continue; + } + if (read(fd, trigger_buf, sizeof(trigger_buf)) > 0 && + strstr(trigger_buf, trigger_flag)) { + break; + } + sleep(1); + } + if (count == 0) { + fprintf(stderr, + "Timeout to get trigger flag file\n"); + return 1; + } + } else { + printf("Hit any key to trigger error: "); + fflush(stdout); + read(0, answer, 16); + now = time(NULL); + printf("Access time at %s\n", ctime(&now)); + } + + if (iflag) { + void (*f)(void) = (void (*)(void))buf; + + while (1) + f(); + } + + if (dflag) { + while (1) { + for (i = 0; i < pagesize; i += sizeof(int)) + total += *(int *)(buf + i); + } + } + + return 0; +} diff --git a/KVM/qemu/ras.cfg b/KVM/qemu/ras.cfg new file mode 100644 index 00000000..43a7da85 --- /dev/null +++ b/KVM/qemu/ras.cfg @@ -0,0 +1,17 @@ +- ras: + no Windows + type = ras + virt_test_type = qemu + vm_accelerator = kvm + force_create_image = no + remove_image = no + start_vm = yes + kill_vm = yes + auto_cpu_model = "no" + cpu_model = host + test_dir = '/tmp' + source_file = 'victim.c' + exec_file = 'victim' + variants: + - srao: + error_type = "0x10" diff --git a/KVM/qemu/tests/ras.py b/KVM/qemu/tests/ras.py new file mode 100644 index 00000000..90062052 --- /dev/null +++ b/KVM/qemu/tests/ras.py @@ -0,0 +1,127 @@ +#!/usr/bin/python3 + +# SPDX-License-Identifier: GPL-2.0-only +# Copyright (c) 2025 Intel Corporation + +# Author: Farrah Chen +# +# History: Nov. 2025 - Farrah Chen - creation + +import os +import re +from avocado.utils import process +from avocado.core import exceptions +from virttest import error_context, env_process +from virttest import utils_package +from virttest import data_dir as virttest_data_dir + + +def prepare_vm_victim(test, params, vm, session): + """ + Compile test tool victim in guest. + Return the execuable test tool with absolute path. + :param test: QEMU test object + :param params: Dictionary with the test parameters + :param vm: The vm object + :param session: Guest session + """ + source_file = params["source_file"] + exec_file = params["exec_file"] + test_dir = params["test_dir"] + deps_dir = virttest_data_dir.get_deps_dir('ras') + src_path = os.path.join(deps_dir, source_file) + vm.copy_files_to(src_path, test_dir) + if not utils_package.package_install("gcc", session): + test.cancel("Failed to install package gcc.") + compile_cmd = "cd %s && gcc %s -o %s" % (test_dir, source_file, exec_file) + status = session.cmd_status(compile_cmd) + if status: + raise exceptions.TestError("Victim compile failed.") + session.cmd("rm -rf %s/%s" % (test_dir, source_file)) + + return os.path.join(test_dir, exec_file) + + +def error_inject(test, params, addr): + """ + Check if kernel module einj is loaded, if not, load it. + Inject error via einj + :param test: QEMU test object + :param params: Dictionary with the test parameters + :param addr: Host physical address + """ + module = 'einj' + if module not in process.system_output('lsmod').decode('utf-8'): + if process.system('modprobe %s' % module, shell=True) != 0: + test.cancel("module %s isn't supported ?" % module) + debugfs = '/sys/kernel/debug' + einj_path = os.path.join(debugfs, 'apei/einj/') + if not os.path.exists(einj_path): + test.cancel("error injection isn't supported, check your BIOS setting") + error_type = params.get('error_type') + status = process.system("echo %s > %s/error_type" % (error_type, einj_path), shell=True) + if status: + raise exceptions.TestError("Failed to inject error %s" % error_type) + status = process.system("echo %s > %s/param1" % (addr, einj_path), shell=True) + if status: + raise exceptions.TestError("Failed to inject error to address %s" % addr) + status = process.system("echo 0xfffffffffffff000 > %s/param2" % einj_path, shell=True) + if status: + raise exceptions.TestError("Failed to inject mask to param2") + status = process.system("echo 1 > %s/notrigger" % einj_path, shell=True) + if status: + raise exceptions.TestError("Failed to enable notrigger") + status = process.system("echo 1 > %s/error_inject" % einj_path, shell=True) + if status: + raise exceptions.TestError("Failed to inject error") + + +@error_context.context_aware +def run(test, params, env): + """ + Inject error to guest memory. + 0) Before executing this case, enable error injection, disable Patrol Scrub in BIOS + 1) Boot up guest + 2) Run victim in guest to get a physical address in guest + 3) Run gpa2hpa in QEMU monitor to get it's host physical address + 4) Return to host, inject error to this address by einj + 5) Return to guest victim, "enter" to trigger error + 6) Shutdown guest + :param test: QEMU test object + :param params: Dictionary with the test parameters + :param env: Dictionary with test environment. + """ + try: + vm_name = params['main_vm'] + env_process.preprocess_vm(test, params, env, vm_name) + vm = env.get_vm(vm_name) + session = vm.wait_for_login() + vm_exec_bin = prepare_vm_victim(test, params, vm, session) + victim_cmd = '%s -d -k 0 > /tmp/vmpha.log &' % vm_exec_bin + session.cmd(victim_cmd) + vmpha_cmd = 'cat /tmp/vmpha.log' + output = session.cmd_output(vmpha_cmd) + guest_pha = output.split()[5] + output = vm.monitor.send_args_cmd("gpa2hpa %s" % guest_pha) + host_pha = output.split()[7] + error_inject(test, params, host_pha) + test_dir = params["test_dir"] + vm_trigger_cmd = 'echo "trigger" > %s/trigger_start' % test_dir + session.cmd(vm_trigger_cmd) + hw_mce = 'err_code:0x00a0:0x0090 SystemAddress:0x%s' % host_pha.lstrip('0x') + vm_mce = 'mce: Uncorrected hardware memory error in user-access at %s' % guest_pha.lstrip('0x') + hw_dmesg = process.system_output('dmesg').decode('utf-8') + vm_dmesg = session.cmd_output('dmesg') + hw_status = re.search('%s' % hw_mce, hw_dmesg) + if not hw_status: + raise exceptions.TestError("Failed to trigger MCE in host") + vm_status = re.search('%s' % vm_mce, vm_dmesg) + if not vm_status: + raise exceptions.TestError("Failed to trigger MCE in guest") + vm.verify_dmesg() + + finally: + session.cmd("rm -rf /tmp/vmpha.log") + session.cmd("rm -rf %s/trigger_start" % test_dir) + session.cmd("rm -rf %s" % vm_exec_bin) + session.close()