root/tools/testing/selftests/kvm/arm64/sea_to_user.c
// 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 <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#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;
}