summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/kvm/arm64/hidden_features.c
blob: 194d746e7605e5013be492bd25e451cd027d28aa (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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@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();
}