// SPDX-License-Identifier: GPL-2.0-only /* * Test KVM returns to userspace with KVM_EXIT_ARM_SEA if host APEI fails * to handle SEA and userspace has opt-ed in KVM_CAP_ARM_SEA_TO_USER. * * After reaching userspace with expected arm_sea info, also test userspace * injecting a synchronous external data abort into the guest. * * This test utilizes EINJ to generate a REAL synchronous external data * abort by consuming a recoverable uncorrectable memory error. Therefore * the device under test must support EINJ in both firmware and host kernel, * including the notrigger feature. Otherwise the test will be skipped. * The under-test platform's APEI should be unable to claim SEA. Otherwise * the test will also be skipped. */ #include #include #include #include #include "test_util.h" #include "kvm_util.h" #include "processor.h" #include "guest_modes.h" #define PAGE_PRESENT (1ULL << 63) #define PAGE_PHYSICAL 0x007fffffffffffffULL #define PAGE_ADDR_MASK (~(0xfffULL)) /* Group ISV and ISS[23:14]. */ #define ESR_ELx_INST_SYNDROME ((ESR_ELx_ISV) | (ESR_ELx_SAS) | \ (ESR_ELx_SSE) | (ESR_ELx_SRT_MASK) | \ (ESR_ELx_SF) | (ESR_ELx_AR)) #define EINJ_ETYPE "/sys/kernel/debug/apei/einj/error_type" #define EINJ_ADDR "/sys/kernel/debug/apei/einj/param1" #define EINJ_MASK "/sys/kernel/debug/apei/einj/param2" #define EINJ_FLAGS "/sys/kernel/debug/apei/einj/flags" #define EINJ_NOTRIGGER "/sys/kernel/debug/apei/einj/notrigger" #define EINJ_DOIT "/sys/kernel/debug/apei/einj/error_inject" /* Memory Uncorrectable non-fatal. */ #define ERROR_TYPE_MEMORY_UER 0x10 /* Memory address and mask valid (param1 and param2). */ #define MASK_MEMORY_UER 0b10 /* Guest virtual address region = [2G, 3G). */ #define START_GVA 0x80000000UL #define VM_MEM_SIZE 0x40000000UL /* Note: EINJ_OFFSET must < VM_MEM_SIZE. */ #define EINJ_OFFSET 0x01234badUL #define EINJ_GVA ((START_GVA) + (EINJ_OFFSET)) static vm_paddr_t einj_gpa; static void *einj_hva; static uint64_t einj_hpa; static bool far_invalid; static uint64_t translate_to_host_paddr(unsigned long vaddr) { uint64_t pinfo; int64_t offset = vaddr / getpagesize() * sizeof(pinfo); int fd; uint64_t page_addr; uint64_t paddr; fd = open("/proc/self/pagemap", O_RDONLY); if (fd < 0) ksft_exit_fail_perror("Failed to open /proc/self/pagemap"); if (pread(fd, &pinfo, sizeof(pinfo), offset) != sizeof(pinfo)) { close(fd); ksft_exit_fail_perror("Failed to read /proc/self/pagemap"); } close(fd); if ((pinfo & PAGE_PRESENT) == 0) ksft_exit_fail_perror("Page not present"); page_addr = (pinfo & PAGE_PHYSICAL) << MIN_PAGE_SHIFT; paddr = page_addr + (vaddr & (getpagesize() - 1)); return paddr; } static void write_einj_entry(const char *einj_path, uint64_t val) { char cmd[256] = {0}; FILE *cmdfile = NULL; sprintf(cmd, "echo %#lx > %s", val, einj_path); cmdfile = popen(cmd, "r"); if (pclose(cmdfile) == 0) ksft_print_msg("echo %#lx > %s - done\n", val, einj_path); else ksft_exit_fail_perror("Failed to write EINJ entry"); } static void inject_uer(uint64_t paddr) { if (access("/sys/firmware/acpi/tables/EINJ", R_OK) == -1) ksft_test_result_skip("EINJ table no available in firmware"); if (access(EINJ_ETYPE, R_OK | W_OK) == -1) ksft_test_result_skip("EINJ module probably not loaded?"); write_einj_entry(EINJ_ETYPE, ERROR_TYPE_MEMORY_UER); write_einj_entry(EINJ_FLAGS, MASK_MEMORY_UER); write_einj_entry(EINJ_ADDR, paddr); write_einj_entry(EINJ_MASK, ~0x0UL); write_einj_entry(EINJ_NOTRIGGER, 1); write_einj_entry(EINJ_DOIT, 1); } /* * When host APEI successfully claims the SEA caused by guest_code, kernel * will send SIGBUS signal with BUS_MCEERR_AR to test thread. * * We set up this SIGBUS handler to skip the test for that case. */ static void sigbus_signal_handler(int sig, siginfo_t *si, void *v) { ksft_print_msg("SIGBUS (%d) received, dumping siginfo...\n", sig); ksft_print_msg("si_signo=%d, si_errno=%d, si_code=%d, si_addr=%p\n", si->si_signo, si->si_errno, si->si_code, si->si_addr); if (si->si_code == BUS_MCEERR_AR) ksft_test_result_skip("SEA is claimed by host APEI\n"); else ksft_test_result_fail("Exit with signal unhandled\n"); exit(0); } static void setup_sigbus_handler(void) { struct sigaction act; memset(&act, 0, sizeof(act)); sigemptyset(&act.sa_mask); act.sa_sigaction = sigbus_signal_handler; act.sa_flags = SA_SIGINFO; TEST_ASSERT(sigaction(SIGBUS, &act, NULL) == 0, "Failed to setup SIGBUS handler"); } static void guest_code(void) { uint64_t guest_data; /* Consumes error will cause a SEA. */ guest_data = *(uint64_t *)EINJ_GVA; GUEST_FAIL("Poison not protected by SEA: gva=%#lx, guest_data=%#lx\n", EINJ_GVA, guest_data); } static void expect_sea_handler(struct ex_regs *regs) { u64 esr = read_sysreg(esr_el1); u64 far = read_sysreg(far_el1); bool expect_far_invalid = far_invalid; GUEST_PRINTF("Handling Guest SEA\n"); GUEST_PRINTF("ESR_EL1=%#lx, FAR_EL1=%#lx\n", esr, far); GUEST_ASSERT_EQ(ESR_ELx_EC(esr), ESR_ELx_EC_DABT_CUR); GUEST_ASSERT_EQ(esr & ESR_ELx_FSC_TYPE, ESR_ELx_FSC_EXTABT); if (expect_far_invalid) { GUEST_ASSERT_EQ(esr & ESR_ELx_FnV, ESR_ELx_FnV); GUEST_PRINTF("Guest observed garbage value in FAR\n"); } else { GUEST_ASSERT_EQ(esr & ESR_ELx_FnV, 0); GUEST_ASSERT_EQ(far, EINJ_GVA); } GUEST_DONE(); } static void vcpu_inject_sea(struct kvm_vcpu *vcpu) { struct kvm_vcpu_events events = {}; events.exception.ext_dabt_pending = true; vcpu_events_set(vcpu, &events); } static void run_vm(struct kvm_vm *vm, struct kvm_vcpu *vcpu) { struct ucall uc; bool guest_done = false; struct kvm_run *run = vcpu->run; u64 esr; /* Resume the vCPU after error injection to consume the error. */ vcpu_run(vcpu); ksft_print_msg("Dump kvm_run info about KVM_EXIT_%s\n", exit_reason_str(run->exit_reason)); ksft_print_msg("kvm_run.arm_sea: esr=%#llx, flags=%#llx\n", run->arm_sea.esr, run->arm_sea.flags); ksft_print_msg("kvm_run.arm_sea: gva=%#llx, gpa=%#llx\n", run->arm_sea.gva, run->arm_sea.gpa); TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_ARM_SEA); esr = run->arm_sea.esr; TEST_ASSERT_EQ(ESR_ELx_EC(esr), ESR_ELx_EC_DABT_LOW); TEST_ASSERT_EQ(esr & ESR_ELx_FSC_TYPE, ESR_ELx_FSC_EXTABT); TEST_ASSERT_EQ(ESR_ELx_ISS2(esr), 0); TEST_ASSERT_EQ((esr & ESR_ELx_INST_SYNDROME), 0); TEST_ASSERT_EQ(esr & ESR_ELx_VNCR, 0); if (!(esr & ESR_ELx_FnV)) { ksft_print_msg("Expect gva to match given FnV bit is 0\n"); TEST_ASSERT_EQ(run->arm_sea.gva, EINJ_GVA); } if (run->arm_sea.flags & KVM_EXIT_ARM_SEA_FLAG_GPA_VALID) { ksft_print_msg("Expect gpa to match given KVM_EXIT_ARM_SEA_FLAG_GPA_VALID is set\n"); TEST_ASSERT_EQ(run->arm_sea.gpa, einj_gpa & PAGE_ADDR_MASK); } far_invalid = esr & ESR_ELx_FnV; /* Inject a SEA into guest and expect handled in SEA handler. */ vcpu_inject_sea(vcpu); /* Expect the guest to reach GUEST_DONE gracefully. */ do { vcpu_run(vcpu); switch (get_ucall(vcpu, &uc)) { case UCALL_PRINTF: ksft_print_msg("From guest: %s", uc.buffer); break; case UCALL_DONE: ksft_print_msg("Guest done gracefully!\n"); guest_done = 1; break; case UCALL_ABORT: ksft_print_msg("Guest aborted!\n"); guest_done = 1; REPORT_GUEST_ASSERT(uc); break; default: TEST_FAIL("Unexpected ucall: %lu\n", uc.cmd); } } while (!guest_done); } static struct kvm_vm *vm_create_with_sea_handler(struct kvm_vcpu **vcpu) { size_t backing_page_size; size_t guest_page_size; size_t alignment; uint64_t num_guest_pages; vm_paddr_t start_gpa; enum vm_mem_backing_src_type src_type = VM_MEM_SRC_ANONYMOUS_HUGETLB_1GB; struct kvm_vm *vm; backing_page_size = get_backing_src_pagesz(src_type); guest_page_size = vm_guest_mode_params[VM_MODE_DEFAULT].page_size; alignment = max(backing_page_size, guest_page_size); num_guest_pages = VM_MEM_SIZE / guest_page_size; vm = __vm_create_with_one_vcpu(vcpu, num_guest_pages, guest_code); vm_init_descriptor_tables(vm); vcpu_init_descriptor_tables(*vcpu); vm_install_sync_handler(vm, /*vector=*/VECTOR_SYNC_CURRENT, /*ec=*/ESR_ELx_EC_DABT_CUR, /*handler=*/expect_sea_handler); start_gpa = (vm->max_gfn - num_guest_pages) * guest_page_size; start_gpa = align_down(start_gpa, alignment); vm_userspace_mem_region_add( /*vm=*/vm, /*src_type=*/src_type, /*guest_paddr=*/start_gpa, /*slot=*/1, /*npages=*/num_guest_pages, /*flags=*/0); virt_map(vm, START_GVA, start_gpa, num_guest_pages); ksft_print_msg("Mapped %#lx pages: gva=%#lx to gpa=%#lx\n", num_guest_pages, START_GVA, start_gpa); return vm; } static void vm_inject_memory_uer(struct kvm_vm *vm) { uint64_t guest_data; einj_gpa = addr_gva2gpa(vm, EINJ_GVA); einj_hva = addr_gva2hva(vm, EINJ_GVA); /* Populate certain data before injecting UER. */ *(uint64_t *)einj_hva = 0xBAADCAFE; guest_data = *(uint64_t *)einj_hva; ksft_print_msg("Before EINJect: data=%#lx\n", guest_data); einj_hpa = translate_to_host_paddr((unsigned long)einj_hva); ksft_print_msg("EINJ_GVA=%#lx, einj_gpa=%#lx, einj_hva=%p, einj_hpa=%#lx\n", EINJ_GVA, einj_gpa, einj_hva, einj_hpa); inject_uer(einj_hpa); ksft_print_msg("Memory UER EINJected\n"); } int main(int argc, char *argv[]) { struct kvm_vm *vm; struct kvm_vcpu *vcpu; TEST_REQUIRE(kvm_has_cap(KVM_CAP_ARM_SEA_TO_USER)); setup_sigbus_handler(); vm = vm_create_with_sea_handler(&vcpu); vm_enable_cap(vm, KVM_CAP_ARM_SEA_TO_USER, 0); vm_inject_memory_uer(vm); run_vm(vm, vcpu); kvm_vm_free(vm); return 0; }