| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * mmio_sign_ext - Test sign-extending MMIO load emulation (LDRSB/LDRSH/LDRSW) |
| * |
| * Copyright (c) 2026 Google LLC |
| * Author: Fuad Tabba <fuad.tabba@linux.dev> |
| */ |
| |
| #include <asm/ptrace.h> |
| |
| #include "processor.h" |
| #include "test_util.h" |
| |
| #define MMIO_ADDR 0x8000000ULL |
| |
| /* AP[1]: allow unprivileged (EL0) access to a mapping. */ |
| #define PTE_USER BIT(6) |
| |
| /* SPSR for ERET to EL0t with DAIF masked. */ |
| #define SPSR_EL0 (PSR_MODE_EL0t | PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT) |
| |
| struct mmio_test { |
| const char *name; |
| uint64_t data; /* access-width value, host byte order */ |
| uint8_t len; |
| uint64_t expected; /* sign-extended result; same for LE and BE */ |
| }; |
| |
| /* Paired 1:1, in order, with the loads in guest_loads_le() and el0_be_loads. */ |
| static const struct mmio_test tests[] = { |
| /* LDRSB Xt: byte sign-extended to 64 bits */ |
| { "LDRSB Xt 0xFF", 0xFF, 1, 0xFFFFFFFFFFFFFFFFULL }, |
| { "LDRSB Xt 0x7F", 0x7F, 1, 0x7FULL }, |
| |
| /* LDRSB Wt: byte sign-extended to 32 bits, upper 32 bits zeroed */ |
| { "LDRSB Wt 0xFF", 0xFF, 1, 0xFFFFFFFFULL }, |
| { "LDRSB Wt 0x7F", 0x7F, 1, 0x7FULL }, |
| |
| /* LDRSH Xt: halfword sign-extended to 64 bits */ |
| { "LDRSH Xt 0x8001", 0x8001, 2, 0xFFFFFFFFFFFF8001ULL }, |
| { "LDRSH Xt 0x7FFF", 0x7FFF, 2, 0x7FFFULL }, |
| |
| /* LDRSH Wt: halfword sign-extended to 32 bits, upper 32 bits zeroed */ |
| { "LDRSH Wt 0x8001", 0x8001, 2, 0xFFFF8001ULL }, |
| { "LDRSH Wt 0x7FFF", 0x7FFF, 2, 0x7FFFULL }, |
| |
| /* LDRSW Xt: word sign-extended to 64 bits (no Wt form) */ |
| { "LDRSW Xt 0x80000001", 0x80000001, 4, 0xFFFFFFFF80000001ULL }, |
| { "LDRSW Xt 0x7FFFFFFF", 0x7FFFFFFF, 4, 0x7FFFFFFFULL }, |
| }; |
| |
| /* Issue one sign-extending load from MMIO and report the result. */ |
| #define GUEST_LDRS(load) do { \ |
| uint64_t val; \ |
| \ |
| asm volatile(load : "=r"(val) : "r"(MMIO_ADDR) : "memory"); \ |
| GUEST_SYNC(val); \ |
| } while (0) |
| |
| /* Little-endian pass: loads issued at EL1. */ |
| static void guest_loads_le(void) |
| { |
| GUEST_LDRS("ldrsb %0, [%1]"); |
| GUEST_LDRS("ldrsb %0, [%1]"); |
| GUEST_LDRS("ldrsb %w0, [%1]"); |
| GUEST_LDRS("ldrsb %w0, [%1]"); |
| GUEST_LDRS("ldrsh %0, [%1]"); |
| GUEST_LDRS("ldrsh %0, [%1]"); |
| GUEST_LDRS("ldrsh %w0, [%1]"); |
| GUEST_LDRS("ldrsh %w0, [%1]"); |
| GUEST_LDRS("ldrsw %0, [%1]"); |
| GUEST_LDRS("ldrsw %0, [%1]"); |
| } |
| |
| /* |
| * Run the big-endian loads at EL0, where SCTLR_EL1.E0E flips only the data |
| * endianness; at EL1, SCTLR_EL1.EE would also flip the page-table walk and |
| * fault on the little-endian tables. x0 holds MMIO_ADDR; results return in |
| * x19..x28 (tests[] order) via a single SVC. |
| */ |
| extern char el0_be_loads[]; |
| asm( |
| " .pushsection .text, \"ax\"\n" |
| " .global el0_be_loads\n" |
| "el0_be_loads:\n" |
| " ldrsb x19, [x0]\n" |
| " ldrsb x20, [x0]\n" |
| " ldrsb w21, [x0]\n" |
| " ldrsb w22, [x0]\n" |
| " ldrsh x23, [x0]\n" |
| " ldrsh x24, [x0]\n" |
| " ldrsh w25, [x0]\n" |
| " ldrsh w26, [x0]\n" |
| " ldrsw x27, [x0]\n" |
| " ldrsw x28, [x0]\n" |
| " svc #0\n" |
| " .popsection\n" |
| ); |
| |
| /* EL1 handler for the EL0 SVC: report the results, then finish. */ |
| static void el0_svc_handler(struct ex_regs *regs) |
| { |
| int i; |
| |
| for (i = 0; i < ARRAY_SIZE(tests); i++) |
| GUEST_SYNC(regs->regs[19 + i]); |
| |
| GUEST_DONE(); |
| } |
| |
| static bool guest_mixed_endian_el0(void) |
| { |
| uint64_t mmfr0 = read_sysreg(id_aa64mmfr0_el1); |
| |
| return SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGEND, mmfr0) || |
| SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGENDEL0, mmfr0); |
| } |
| |
| static void guest_code(void) |
| { |
| guest_loads_le(); |
| |
| if (guest_mixed_endian_el0()) { |
| write_sysreg(read_sysreg(sctlr_el1) | SCTLR_EL1_E0E, sctlr_el1); |
| isb(); |
| |
| asm volatile( |
| " msr elr_el1, %[pc]\n" |
| " msr spsr_el1, %[spsr]\n" |
| " mov x0, %[mmio]\n" |
| " isb\n" |
| " eret\n" |
| : |
| : [pc] "r"(el0_be_loads), |
| [spsr] "r"((uint64_t)SPSR_EL0), |
| [mmio] "r"(MMIO_ADDR) |
| : "x0", "memory"); |
| __builtin_unreachable(); /* el0_svc_handler ends the test */ |
| } |
| |
| GUEST_DONE(); |
| } |
| |
| static void handle_mmio(struct kvm_run *run, const struct mmio_test *t, bool be) |
| { |
| int i; |
| |
| TEST_ASSERT_EQ(run->mmio.phys_addr, MMIO_ADDR); |
| TEST_ASSERT(!run->mmio.is_write, "Expected MMIO read for %s", t->name); |
| TEST_ASSERT_EQ(run->mmio.len, t->len); |
| |
| memset(run->mmio.data, 0, sizeof(run->mmio.data)); |
| if (be) { |
| /* The guest reads the device bytes most-significant first. */ |
| for (i = 0; i < t->len; i++) |
| run->mmio.data[i] = t->data >> (8 * (t->len - 1 - i)); |
| } else { |
| /* Works because arm64 KVM hosts are always little-endian. */ |
| memcpy(run->mmio.data, &t->data, t->len); |
| } |
| } |
| |
| static void expect_sync(struct kvm_vcpu *vcpu, struct ucall *uc, |
| const struct mmio_test *t) |
| { |
| switch (get_ucall(vcpu, uc)) { |
| case UCALL_SYNC: |
| TEST_ASSERT(uc->args[1] == t->expected, |
| "%s: got %#lx, want %#lx", t->name, |
| (unsigned long)uc->args[1], (unsigned long)t->expected); |
| break; |
| case UCALL_ABORT: |
| REPORT_GUEST_ASSERT(*uc); |
| break; |
| default: |
| TEST_FAIL("Unexpected ucall for %s", t->name); |
| } |
| } |
| |
| /* OR PTE_USER into the leaf descriptors covering [gva, gva + len). */ |
| static void make_el0_accessible(struct kvm_vm *vm, uint64_t gva, uint64_t len) |
| { |
| uint64_t addr; |
| |
| for (addr = gva & ~((uint64_t)vm->page_size - 1); addr < gva + len; |
| addr += vm->page_size) |
| *virt_get_pte_hva(vm, addr) |= PTE_USER; |
| } |
| |
| static bool vcpu_mixed_endian_el0(struct kvm_vcpu *vcpu) |
| { |
| uint64_t mmfr0 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR0_EL1)); |
| |
| return SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGEND, mmfr0) || |
| SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGENDEL0, mmfr0); |
| } |
| |
| int main(void) |
| { |
| struct kvm_vcpu *vcpu; |
| struct kvm_vm *vm; |
| struct ucall uc; |
| unsigned int i; |
| bool be; |
| |
| vm = vm_create_with_one_vcpu(&vcpu, guest_code); |
| virt_map(vm, MMIO_ADDR, MMIO_ADDR, 1); |
| |
| vm_init_descriptor_tables(vm); |
| vcpu_init_descriptor_tables(vcpu); |
| vm_install_sync_handler(vm, VECTOR_SYNC_LOWER_64, ESR_ELx_EC_SVC64, |
| el0_svc_handler); |
| |
| be = vcpu_mixed_endian_el0(vcpu); |
| if (be) |
| make_el0_accessible(vm, MMIO_ADDR, vm->page_size); |
| |
| ksft_print_header(); |
| ksft_set_plan(ARRAY_SIZE(tests) * (be ? 2 : 1)); |
| |
| /* Little-endian pass: one load and one result per iteration. */ |
| for (i = 0; i < ARRAY_SIZE(tests); i++) { |
| const struct mmio_test *t = &tests[i]; |
| |
| vcpu_run(vcpu); |
| TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_MMIO); |
| handle_mmio(vcpu->run, t, false); |
| |
| vcpu_run(vcpu); |
| expect_sync(vcpu, &uc, t); |
| |
| ksft_test_result_pass("%s\n", t->name); |
| } |
| |
| if (be) { |
| /* The EL0 stub issues all the loads, then reports the results. */ |
| for (i = 0; i < ARRAY_SIZE(tests); i++) { |
| vcpu_run(vcpu); |
| TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_MMIO); |
| handle_mmio(vcpu->run, &tests[i], true); |
| } |
| for (i = 0; i < ARRAY_SIZE(tests); i++) { |
| vcpu_run(vcpu); |
| expect_sync(vcpu, &uc, &tests[i]); |
| ksft_test_result_pass("BE %s\n", tests[i].name); |
| } |
| } |
| |
| vcpu_run(vcpu); |
| TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE, "Expected UCALL_DONE"); |
| |
| kvm_vm_free(vm); |
| |
| ksft_finished(); |
| } |