| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * hidden_features - Check that a feature's instruction runs in the guest when | |
| * its ID register field is advertised, and is UNDEFINED when userspace clears | |
| * the field. | |
| * | |
| * Copyright (c) 2026 Google LLC | |
| * Author: Fuad Tabba <fuad.tabba@linux.dev> | |
| */ | |
| #include "kvm_util.h" | |
| #include "processor.h" | |
| #include "test_util.h" | |
| static volatile bool undef; | |
| static void guest_tlbi_os(void) | |
| { | |
| /* tlbi vmalle1os */ | |
| asm volatile("sys #0, c8, c1, #0\n\tdsb ish\n\tisb" ::: "memory"); | |
| } | |
| static void guest_mops(void) | |
| { | |
| register u64 *d asm("x0"); | |
| register u64 n asm("x1"); | |
| register u64 s asm("x2"); | |
| u64 buf[8]; | |
| d = buf; | |
| n = sizeof(buf); | |
| s = 0; | |
| /* setp [x0]!, x1!, x2; setm; sete */ | |
| asm volatile(".inst 0x19c20420\n\t.inst 0x19c24420\n\t.inst 0x19c28420" | |
| : "+r"(d), "+r"(n) : "r"(s) : "cc", "memory"); | |
| } | |
| static void guest_tcr2(void) | |
| { | |
| read_sysreg_s(SYS_TCR2_EL1); | |
| } | |
| static void guest_fpmr(void) | |
| { | |
| read_sysreg_s(SYS_FPMR); | |
| } | |
| struct feature { | |
| const char *name; | |
| u64 id_reg; | |
| u64 mask; | |
| u8 shift; | |
| u64 min; | |
| void (*insn)(void); | |
| bool (*trappable)(struct kvm_vcpu *vcpu); | |
| }; | |
| /* Without FGT, KVM traps a hidden TLBI OS only through HCR_EL2.TTLBOS (FEAT_EVT2). */ | |
| static bool tlbi_os_trappable(struct kvm_vcpu *vcpu) | |
| { | |
| u64 mmfr0 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR0_EL1)); | |
| u64 mmfr2 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR2_EL1)); | |
| return SYS_FIELD_GET(ID_AA64MMFR0_EL1, FGT, mmfr0) >= ID_AA64MMFR0_EL1_FGT_IMP || | |
| SYS_FIELD_GET(ID_AA64MMFR2_EL1, EVT, mmfr2) >= ID_AA64MMFR2_EL1_EVT_TTLBxS; | |
| } | |
| #define FEATURE(n, reg, field, min_val, fn, trap) \ | |
| { \ | |
| .name = n, \ | |
| .id_reg = SYS_##reg, \ | |
| .mask = reg##_##field##_MASK, \ | |
| .shift = reg##_##field##_SHIFT, \ | |
| .min = reg##_##field##_##min_val, \ | |
| .insn = fn, \ | |
| .trappable = trap, \ | |
| } | |
| static const struct feature features[] = { | |
| FEATURE("TLBI OS", ID_AA64ISAR0_EL1, TLB, OS, guest_tlbi_os, tlbi_os_trappable), | |
| FEATURE("MOPS", ID_AA64ISAR2_EL1, MOPS, IMP, guest_mops, NULL), | |
| FEATURE("TCR2_EL1", ID_AA64MMFR3_EL1, TCRX, IMP, guest_tcr2, NULL), | |
| FEATURE("FPMR", ID_AA64PFR2_EL1, FPMR, IMP, guest_fpmr, NULL), | |
| }; | |
| static void guest_code(const struct feature *feat) | |
| { | |
| undef = false; | |
| feat->insn(); | |
| GUEST_SYNC(undef); | |
| GUEST_DONE(); | |
| } | |
| static void guest_undef_handler(struct ex_regs *regs) | |
| { | |
| undef = true; | |
| regs->pc += 4; | |
| } | |
| static bool run(const struct feature *feat, bool hide) | |
| { | |
| struct kvm_vcpu *vcpu; | |
| struct kvm_vm *vm; | |
| struct ucall uc; | |
| bool got = false; | |
| u64 val; | |
| vm = vm_create_with_one_vcpu(&vcpu, (void *)guest_code); | |
| vm_init_descriptor_tables(vm); | |
| vcpu_init_descriptor_tables(vcpu); | |
| vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ESR_ELx_EC_UNKNOWN, guest_undef_handler); | |
| vcpu_args_set(vcpu, 1, feat); | |
| if (hide) { | |
| val = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg)); | |
| vcpu_set_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg), val & ~feat->mask); | |
| } | |
| for (;;) { | |
| vcpu_run(vcpu); | |
| switch (get_ucall(vcpu, &uc)) { | |
| case UCALL_SYNC: | |
| got = uc.args[1]; | |
| break; | |
| case UCALL_ABORT: | |
| REPORT_GUEST_ASSERT(uc); | |
| break; | |
| case UCALL_DONE: | |
| kvm_vm_free(vm); | |
| return got; | |
| default: | |
| TEST_FAIL("Unknown ucall %lu", uc.cmd); | |
| } | |
| } | |
| } | |
| static void probe_feature(const struct feature *feat, bool *present, bool *trappable) | |
| { | |
| struct kvm_vcpu *vcpu; | |
| struct kvm_vm *vm; | |
| u64 val; | |
| vm = vm_create_with_one_vcpu(&vcpu, NULL); | |
| val = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg)); | |
| *present = ((val & feat->mask) >> feat->shift) >= feat->min; | |
| *trappable = !feat->trappable || feat->trappable(vcpu); | |
| kvm_vm_free(vm); | |
| } | |
| int main(void) | |
| { | |
| const struct feature *feat; | |
| bool present, trappable; | |
| int i; | |
| test_disable_default_vgic(); | |
| ksft_print_header(); | |
| ksft_set_plan(ARRAY_SIZE(features) * 2); | |
| for (i = 0; i < ARRAY_SIZE(features); i++) { | |
| feat = &features[i]; | |
| probe_feature(feat, &present, &trappable); | |
| if (!present) { | |
| ksft_test_result_skip("%s advertised, not supported\n", feat->name); | |
| ksft_test_result_skip("%s hidden, not supported\n", feat->name); | |
| continue; | |
| } | |
| if (run(feat, false)) | |
| ksft_test_result_fail("%s advertised, UNDEF\n", feat->name); | |
| else | |
| ksft_test_result_pass("%s advertised\n", feat->name); | |
| if (!trappable) | |
| ksft_test_result_skip("%s hidden, not trappable\n", feat->name); | |
| else if (run(feat, true)) | |
| ksft_test_result_pass("%s hidden\n", feat->name); | |
| else | |
| ksft_test_result_fail("%s hidden, no UNDEF\n", feat->name); | |
| } | |
| ksft_finished(); | |
| } |