[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