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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
// SPDX-License-Identifier: GPL-2.0-only
/*
* mmio_sign_ext - Test sign-extending MMIO load emulation (LDRSB/LDRSH/LDRSW)
*
* Copyright (c) 2026 Google LLC
* Author: Fuad Tabba <fuad.tabba@linux.dev>
*/
#include <asm/ptrace.h>
#include "processor.h"
#include "test_util.h"
#define MMIO_ADDR 0x8000000ULL
/* AP[1]: allow unprivileged (EL0) access to a mapping. */
#define PTE_USER BIT(6)
/* SPSR for ERET to EL0t with DAIF masked. */
#define SPSR_EL0 (PSR_MODE_EL0t | PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT)
struct mmio_test {
const char *name;
uint64_t data; /* access-width value, host byte order */
uint8_t len;
uint64_t expected; /* sign-extended result; same for LE and BE */
};
/* Paired 1:1, in order, with the loads in guest_loads_le() and el0_be_loads. */
static const struct mmio_test tests[] = {
/* LDRSB Xt: byte sign-extended to 64 bits */
{ "LDRSB Xt 0xFF", 0xFF, 1, 0xFFFFFFFFFFFFFFFFULL },
{ "LDRSB Xt 0x7F", 0x7F, 1, 0x7FULL },
/* LDRSB Wt: byte sign-extended to 32 bits, upper 32 bits zeroed */
{ "LDRSB Wt 0xFF", 0xFF, 1, 0xFFFFFFFFULL },
{ "LDRSB Wt 0x7F", 0x7F, 1, 0x7FULL },
/* LDRSH Xt: halfword sign-extended to 64 bits */
{ "LDRSH Xt 0x8001", 0x8001, 2, 0xFFFFFFFFFFFF8001ULL },
{ "LDRSH Xt 0x7FFF", 0x7FFF, 2, 0x7FFFULL },
/* LDRSH Wt: halfword sign-extended to 32 bits, upper 32 bits zeroed */
{ "LDRSH Wt 0x8001", 0x8001, 2, 0xFFFF8001ULL },
{ "LDRSH Wt 0x7FFF", 0x7FFF, 2, 0x7FFFULL },
/* LDRSW Xt: word sign-extended to 64 bits (no Wt form) */
{ "LDRSW Xt 0x80000001", 0x80000001, 4, 0xFFFFFFFF80000001ULL },
{ "LDRSW Xt 0x7FFFFFFF", 0x7FFFFFFF, 4, 0x7FFFFFFFULL },
};
/* Issue one sign-extending load from MMIO and report the result. */
#define GUEST_LDRS(load) do { \
uint64_t val; \
\
asm volatile(load : "=r"(val) : "r"(MMIO_ADDR) : "memory"); \
GUEST_SYNC(val); \
} while (0)
/* Little-endian pass: loads issued at EL1. */
static void guest_loads_le(void)
{
GUEST_LDRS("ldrsb %0, [%1]");
GUEST_LDRS("ldrsb %0, [%1]");
GUEST_LDRS("ldrsb %w0, [%1]");
GUEST_LDRS("ldrsb %w0, [%1]");
GUEST_LDRS("ldrsh %0, [%1]");
GUEST_LDRS("ldrsh %0, [%1]");
GUEST_LDRS("ldrsh %w0, [%1]");
GUEST_LDRS("ldrsh %w0, [%1]");
GUEST_LDRS("ldrsw %0, [%1]");
GUEST_LDRS("ldrsw %0, [%1]");
}
/*
* Run the big-endian loads at EL0, where SCTLR_EL1.E0E flips only the data
* endianness; at EL1, SCTLR_EL1.EE would also flip the page-table walk and
* fault on the little-endian tables. x0 holds MMIO_ADDR; results return in
* x19..x28 (tests[] order) via a single SVC.
*/
extern char el0_be_loads[];
asm(
" .pushsection .text, \"ax\"\n"
" .global el0_be_loads\n"
"el0_be_loads:\n"
" ldrsb x19, [x0]\n"
" ldrsb x20, [x0]\n"
" ldrsb w21, [x0]\n"
" ldrsb w22, [x0]\n"
" ldrsh x23, [x0]\n"
" ldrsh x24, [x0]\n"
" ldrsh w25, [x0]\n"
" ldrsh w26, [x0]\n"
" ldrsw x27, [x0]\n"
" ldrsw x28, [x0]\n"
" svc #0\n"
" .popsection\n"
);
/* EL1 handler for the EL0 SVC: report the results, then finish. */
static void el0_svc_handler(struct ex_regs *regs)
{
int i;
for (i = 0; i < ARRAY_SIZE(tests); i++)
GUEST_SYNC(regs->regs[19 + i]);
GUEST_DONE();
}
static bool guest_mixed_endian_el0(void)
{
uint64_t mmfr0 = read_sysreg(id_aa64mmfr0_el1);
return SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGEND, mmfr0) ||
SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGENDEL0, mmfr0);
}
static void guest_code(void)
{
guest_loads_le();
if (guest_mixed_endian_el0()) {
write_sysreg(read_sysreg(sctlr_el1) | SCTLR_EL1_E0E, sctlr_el1);
isb();
asm volatile(
" msr elr_el1, %[pc]\n"
" msr spsr_el1, %[spsr]\n"
" mov x0, %[mmio]\n"
" isb\n"
" eret\n"
:
: [pc] "r"(el0_be_loads),
[spsr] "r"((uint64_t)SPSR_EL0),
[mmio] "r"(MMIO_ADDR)
: "x0", "memory");
__builtin_unreachable(); /* el0_svc_handler ends the test */
}
GUEST_DONE();
}
static void handle_mmio(struct kvm_run *run, const struct mmio_test *t, bool be)
{
int i;
TEST_ASSERT_EQ(run->mmio.phys_addr, MMIO_ADDR);
TEST_ASSERT(!run->mmio.is_write, "Expected MMIO read for %s", t->name);
TEST_ASSERT_EQ(run->mmio.len, t->len);
memset(run->mmio.data, 0, sizeof(run->mmio.data));
if (be) {
/* The guest reads the device bytes most-significant first. */
for (i = 0; i < t->len; i++)
run->mmio.data[i] = t->data >> (8 * (t->len - 1 - i));
} else {
/* Works because arm64 KVM hosts are always little-endian. */
memcpy(run->mmio.data, &t->data, t->len);
}
}
static void expect_sync(struct kvm_vcpu *vcpu, struct ucall *uc,
const struct mmio_test *t)
{
switch (get_ucall(vcpu, uc)) {
case UCALL_SYNC:
TEST_ASSERT(uc->args[1] == t->expected,
"%s: got %#lx, want %#lx", t->name,
(unsigned long)uc->args[1], (unsigned long)t->expected);
break;
case UCALL_ABORT:
REPORT_GUEST_ASSERT(*uc);
break;
default:
TEST_FAIL("Unexpected ucall for %s", t->name);
}
}
/* OR PTE_USER into the leaf descriptors covering [gva, gva + len). */
static void make_el0_accessible(struct kvm_vm *vm, uint64_t gva, uint64_t len)
{
uint64_t addr;
for (addr = gva & ~((uint64_t)vm->page_size - 1); addr < gva + len;
addr += vm->page_size)
*virt_get_pte_hva(vm, addr) |= PTE_USER;
}
static bool vcpu_mixed_endian_el0(struct kvm_vcpu *vcpu)
{
uint64_t mmfr0 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR0_EL1));
return SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGEND, mmfr0) ||
SYS_FIELD_GET(ID_AA64MMFR0_EL1, BIGENDEL0, mmfr0);
}
int main(void)
{
struct kvm_vcpu *vcpu;
struct kvm_vm *vm;
struct ucall uc;
unsigned int i;
bool be;
vm = vm_create_with_one_vcpu(&vcpu, guest_code);
virt_map(vm, MMIO_ADDR, MMIO_ADDR, 1);
vm_init_descriptor_tables(vm);
vcpu_init_descriptor_tables(vcpu);
vm_install_sync_handler(vm, VECTOR_SYNC_LOWER_64, ESR_ELx_EC_SVC64,
el0_svc_handler);
be = vcpu_mixed_endian_el0(vcpu);
if (be)
make_el0_accessible(vm, MMIO_ADDR, vm->page_size);
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests) * (be ? 2 : 1));
/* Little-endian pass: one load and one result per iteration. */
for (i = 0; i < ARRAY_SIZE(tests); i++) {
const struct mmio_test *t = &tests[i];
vcpu_run(vcpu);
TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_MMIO);
handle_mmio(vcpu->run, t, false);
vcpu_run(vcpu);
expect_sync(vcpu, &uc, t);
ksft_test_result_pass("%s\n", t->name);
}
if (be) {
/* The EL0 stub issues all the loads, then reports the results. */
for (i = 0; i < ARRAY_SIZE(tests); i++) {
vcpu_run(vcpu);
TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_MMIO);
handle_mmio(vcpu->run, &tests[i], true);
}
for (i = 0; i < ARRAY_SIZE(tests); i++) {
vcpu_run(vcpu);
expect_sync(vcpu, &uc, &tests[i]);
ksft_test_result_pass("BE %s\n", tests[i].name);
}
}
vcpu_run(vcpu);
TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE, "Expected UCALL_DONE");
kvm_vm_free(vm);
ksft_finished();
}
|