[PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF
Venkata Rao Kakani
venkata.kakani at oss.qualcomm.com
Mon Sep 28 02:20:11 PDT 2026
On 28-09-2026 12:16 pm, Fuad Tabba wrote:
> Userspace can hide a feature from a guest by clearing its field in a
> writable ID register, and KVM then makes the feature's instructions
> UNDEFINED in the guest by trapping or disabling them. No selftest checks
> that.
>
> Add a test that runs the instruction of each of TLBI OS, MOPS, TCR2_EL1
> and FPMR once with its field as advertised and once with it cleared, and
> expects an UNDEF only when cleared. A feature the vCPU doesn't advertise
> is skipped, as is hidden TLBI OS on a CPU with neither FGT nor
> FEAT_EVT2, where KVM can't trap it.
>
> Signed-off-by: Fuad Tabba <fuad.tabba at linux.dev>
> ---
> tools/testing/selftests/kvm/Makefile.kvm | 1 +
> .../selftests/kvm/arm64/hidden_features.c | 184 ++++++++++++++++++
> 2 files changed, 185 insertions(+)
> create mode 100644 tools/testing/selftests/kvm/arm64/hidden_features.c
>
> diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
> index 96bab7002d39e..864fdca7f362e 100644
> --- a/tools/testing/selftests/kvm/Makefile.kvm
> +++ b/tools/testing/selftests/kvm/Makefile.kvm
> @@ -194,6 +194,7 @@ TEST_GEN_PROGS_arm64 += arm64/vgic_v5
> TEST_GEN_PROGS_arm64 += arm64/vpmu_counter_access
> TEST_GEN_PROGS_arm64 += arm64/no-vgic
> TEST_GEN_PROGS_arm64 += arm64/idreg-idst
> +TEST_GEN_PROGS_arm64 += arm64/hidden_features
> TEST_GEN_PROGS_arm64 += arm64/kvm-uuid
> TEST_GEN_PROGS_arm64 += access_tracking_perf_test
> TEST_GEN_PROGS_arm64 += arch_timer
> diff --git a/tools/testing/selftests/kvm/arm64/hidden_features.c b/tools/testing/selftests/kvm/arm64/hidden_features.c
> new file mode 100644
> index 0000000000000..194d746e7605e
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/arm64/hidden_features.c
> @@ -0,0 +1,184 @@
> +// 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 at 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);
> + }
> + }
Return missing?
> +}
> +
> +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();
> +}
More information about the linux-arm-kernel
mailing list