Re: [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF
From: Venkata Rao Kakani
Date: Mon Sep 28 2026 - 05:21:08 EST
On 28-09-2026 12:16 pm, Fuad Tabba wrote:
Userspace can hide a feature from a guest by clearing its field in aReturn missing?
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@xxxxxxxxx>
---
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@xxxxxxxxx>
+ */
+#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();
+}