summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/kvm/x86/nested_tdp_fault_test.c
blob: fa95568f55ffc3bc152302a91646cdeed5c9f15b (plain)
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2025, Google, Inc.
 */

#include "test_util.h"
#include "kvm_util.h"
#include "processor.h"
#include "svm_util.h"
#include "vmx.h"

#define L2_GUEST_STACK_SIZE 64

enum test_type {
	TEST_FINAL_PAGE_UNMAPPED,	    /* Final data page not present */
	TEST_PT_PAGE_UNMAPPED,		    /* Page table page not present */
	TEST_FINAL_PAGE_WRITE_PROTECTED,    /* Final data page read-only */
	TEST_PT_PAGE_WRITE_PROTECTED,	    /* Page table page read-only */
};

static gva_t l2_test_page;
static void (*l2_entry)(void);

#define TEST_IO_PORT 0x80
#define TEST1_VADDR 0x8000000ULL
#define TEST2_VADDR 0x10000000ULL
#define TEST3_VADDR 0x18000000ULL
#define TEST4_VADDR 0x20000000ULL

/*
 * L2 executes OUTS reading from l2_test_page, triggering a nested page
 * fault on the read access.
 */
static void l2_guest_code_outs(void)
{
	asm volatile("outsb" ::"S"(l2_test_page), "d"(TEST_IO_PORT) : "memory");
	GUEST_FAIL("L2 should not reach here");
}

/*
 * L2 executes INS writing to l2_test_page, triggering a nested page
 * fault on the write access.
 */
static void l2_guest_code_ins(void)
{
	asm volatile("insb" ::"D"(l2_test_page), "d"(TEST_IO_PORT) : "memory");
	GUEST_FAIL("L2 should not reach here");
}

#define GUEST_ASSERT_EXIT_QUAL(ac_eq, ex_eq)		\
	__GUEST_ASSERT((ac_eq) == (ex_eq),		\
		       "Wanted EXIT_QUAL '0x%lx', got '0x%lx'", ex_eq, ac_eq)

static void l1_vmx_code(struct vmx_pages *vmx, u64 expected_fault_gpa,
			u64 test_type)
{
	unsigned long l2_guest_stack[L2_GUEST_STACK_SIZE];
	u64 exit_qual;

	GUEST_ASSERT(vmx->vmcs_gpa);
	GUEST_ASSERT(prepare_for_vmx_operation(vmx));
	GUEST_ASSERT(load_vmcs(vmx));

	prepare_vmcs(vmx, l2_entry, &l2_guest_stack[L2_GUEST_STACK_SIZE]);

	GUEST_ASSERT(!vmlaunch());

	/* Verify we got an EPT violation exit */
	__GUEST_ASSERT(vmreadz(VM_EXIT_REASON) == EXIT_REASON_EPT_VIOLATION,
		       "Expected EPT violation (0x%x), got 0x%lx",
		       EXIT_REASON_EPT_VIOLATION,
		       vmreadz(VM_EXIT_REASON));

	__GUEST_ASSERT(vmreadz(GUEST_PHYSICAL_ADDRESS) == expected_fault_gpa,
		       "Expected guest_physical_address = 0x%lx, got 0x%lx",
		       expected_fault_gpa,
		       vmreadz(GUEST_PHYSICAL_ADDRESS));

	exit_qual = vmreadz(EXIT_QUALIFICATION);

	/*
	 * Note, EPT page table accesses are always read+write, e.g. so that
	 * the CPU can do A/D updates at-will.
	 */
	switch (test_type) {
	case TEST_FINAL_PAGE_UNMAPPED:
		GUEST_ASSERT_EXIT_QUAL(exit_qual, EPT_VIOLATION_ACC_READ |
						  EPT_VIOLATION_GVA_IS_VALID |
						  EPT_VIOLATION_GVA_TRANSLATED);
		break;
	case TEST_PT_PAGE_UNMAPPED:
		GUEST_ASSERT_EXIT_QUAL(exit_qual, EPT_VIOLATION_ACC_READ |
						  EPT_VIOLATION_ACC_WRITE |
						  EPT_VIOLATION_GVA_IS_VALID);
		break;
	case TEST_FINAL_PAGE_WRITE_PROTECTED:
		GUEST_ASSERT_EXIT_QUAL(exit_qual, EPT_VIOLATION_ACC_WRITE |
						  EPT_VIOLATION_PROT_READ |
						  EPT_VIOLATION_PROT_EXEC |
						  EPT_VIOLATION_GVA_IS_VALID |
						  EPT_VIOLATION_GVA_TRANSLATED);
		break;
	case TEST_PT_PAGE_WRITE_PROTECTED:
		GUEST_ASSERT_EXIT_QUAL(exit_qual, EPT_VIOLATION_ACC_READ |
						  EPT_VIOLATION_ACC_WRITE |
						  EPT_VIOLATION_PROT_READ |
						  EPT_VIOLATION_PROT_EXEC |
						  EPT_VIOLATION_GVA_IS_VALID);
		break;
	}

	GUEST_DONE();
}

#define GUEST_ASSERT_NPF_EC(ac_ec, ex_ec)		\
	__GUEST_ASSERT((ac_ec) == (ex_ec),		\
		       "Wanted NPF error code '0x%lx', got '0x%lx'", (u64)(ex_ec), ac_ec)


static void l1_svm_code(struct svm_test_data *svm, u64 expected_fault_gpa,
			 u64 test_type)
{
	unsigned long l2_guest_stack[L2_GUEST_STACK_SIZE];
	struct vmcb *vmcb = svm->vmcb;
	u64 exit_info_1;

	generic_svm_setup(svm, l2_entry,
			  &l2_guest_stack[L2_GUEST_STACK_SIZE]);

	run_guest(vmcb, svm->vmcb_gpa);

	/* Verify we got an NPF exit */
	__GUEST_ASSERT(vmcb->control.exit_code == SVM_EXIT_NPF,
		       "Expected NPF exit (0x%x), got 0x%lx", SVM_EXIT_NPF,
		       vmcb->control.exit_code);

	__GUEST_ASSERT(vmcb->control.exit_info_2 == expected_fault_gpa,
		       "Expected exit_info_2 = 0x%lx, got 0x%lx",
		       expected_fault_gpa,
		       vmcb->control.exit_info_2);

	exit_info_1 = vmcb->control.exit_info_1;

	/*
	 * Note, without GMET enabled, NPT walks are always user accesses.  And
	 * like EPT, page table accesses are always read+write.
	 */
	switch (test_type) {
	case TEST_FINAL_PAGE_UNMAPPED:
		GUEST_ASSERT_NPF_EC(exit_info_1, PFERR_USER_MASK |
						 PFERR_GUEST_FINAL_MASK);
		break;
	case TEST_PT_PAGE_UNMAPPED:
		GUEST_ASSERT_NPF_EC(exit_info_1, PFERR_WRITE_MASK |
						 PFERR_USER_MASK |
						 PFERR_GUEST_PAGE_MASK);
		break;
	case TEST_FINAL_PAGE_WRITE_PROTECTED:
		GUEST_ASSERT_NPF_EC(exit_info_1, PFERR_PRESENT_MASK |
						 PFERR_WRITE_MASK |
						 PFERR_USER_MASK |
						 PFERR_GUEST_FINAL_MASK);
		break;
	case TEST_PT_PAGE_WRITE_PROTECTED:
		GUEST_ASSERT_NPF_EC(exit_info_1, PFERR_PRESENT_MASK |
						 PFERR_WRITE_MASK |
						 PFERR_USER_MASK |
						 PFERR_GUEST_PAGE_MASK);
		break;
	}

	GUEST_DONE();
}

static void l1_guest_code(void *data, u64 expected_fault_gpa,
			  u64 test_type)
{
	if (this_cpu_has(X86_FEATURE_VMX))
		l1_vmx_code(data, expected_fault_gpa, test_type);
	else
		l1_svm_code(data, expected_fault_gpa, test_type);
}

/* Returns the GPA of the PT page that maps @vaddr. */
static u64 get_pt_gpa_for_vaddr(struct kvm_vm *vm, u64 vaddr)
{
	u64 *pte;

	pte = vm_get_pte(vm, vaddr);
	TEST_ASSERT(pte && (*pte & 0x1), "PTE not present for vaddr 0x%lx",
		    (unsigned long)vaddr);

	return addr_hva2gpa(vm, (void *)((u64)pte & ~0xFFFULL));
}

static void run_test(enum test_type type)
{
	gpa_t expected_fault_gpa;
	gva_t nested_gva;

	struct kvm_vcpu *vcpu;
	struct kvm_vm *vm;
	struct ucall uc;

	vm = vm_create_with_one_vcpu(&vcpu, l1_guest_code);
	vm_enable_tdp(vm);

	if (kvm_cpu_has(X86_FEATURE_VMX))
		vcpu_alloc_vmx(vm, &nested_gva);
	else
		vcpu_alloc_svm(vm, &nested_gva);

	switch (type) {
	case TEST_FINAL_PAGE_UNMAPPED:
		/*
		 * Unmap the final data page from NPT/EPT. The guest page
		 * table walk succeeds, but the final GPA->HPA translation
		 * fails. L2 reads from the page via OUTS.
		 */
		l2_entry = l2_guest_code_outs;
		l2_test_page = vm_alloc(vm, vm->page_size, TEST1_VADDR);
		expected_fault_gpa = addr_gva2gpa(vm, l2_test_page);
		break;
	case TEST_PT_PAGE_UNMAPPED:
		/*
		 * Unmap a page table page from NPT/EPT. The hardware page
		 * table walk fails when translating the PT page's GPA
		 * through NPT/EPT. L2 reads from the page via OUTS.
		 */
		l2_entry = l2_guest_code_outs;
		l2_test_page = vm_alloc(vm, vm->page_size, TEST2_VADDR);
		expected_fault_gpa = get_pt_gpa_for_vaddr(vm, l2_test_page);
		break;
	case TEST_FINAL_PAGE_WRITE_PROTECTED:
		/*
		 * Write-protect the final data page in NPT/EPT.  The page
		 * is present and readable, but not writable.  L2 writes to
		 * the page via INS, triggering a protection violation.
		 */
		l2_entry = l2_guest_code_ins;
		l2_test_page = vm_alloc(vm, vm->page_size, TEST3_VADDR);
		expected_fault_gpa = addr_gva2gpa(vm, l2_test_page);
		break;
	case TEST_PT_PAGE_WRITE_PROTECTED:
		/*
		 * Write-protect a page table page in NPT/EPT.  The page is
		 * present and readable, but not writable.  The guest page
		 * table walk needs write access to set A/D bits, so it
		 * triggers a protection violation on the PT page.
		 * L2 reads from the page via OUTS.
		 */
		l2_entry = l2_guest_code_outs;
		l2_test_page = vm_alloc(vm, vm->page_size, TEST4_VADDR);
		expected_fault_gpa = get_pt_gpa_for_vaddr(vm, l2_test_page);
		break;
	}

	tdp_identity_map_default_memslots(vm);

	if (type == TEST_FINAL_PAGE_WRITE_PROTECTED ||
	    type == TEST_PT_PAGE_WRITE_PROTECTED)
		*tdp_get_pte(vm, expected_fault_gpa) &= ~PTE_WRITABLE_MASK(&vm->stage2_mmu);
	else
		*tdp_get_pte(vm, expected_fault_gpa) &= ~(PTE_PRESENT_MASK(&vm->stage2_mmu) |
							   PTE_READABLE_MASK(&vm->stage2_mmu) |
							   PTE_WRITABLE_MASK(&vm->stage2_mmu) |
							   PTE_EXECUTABLE_MASK(&vm->stage2_mmu));

	sync_global_to_guest(vm, l2_entry);
	sync_global_to_guest(vm, l2_test_page);
	vcpu_args_set(vcpu, 3, nested_gva, expected_fault_gpa, (u64)type);

	/*
	 * For the INS-based write test, KVM emulates the instruction and
	 * first reads from the I/O port, which exits to userspace.
	 * Re-enter the guest so emulation can proceed to the memory
	 * write, where the nested page fault is triggered.
	 */
	for (;;) {
		vcpu_run(vcpu);

		if (vcpu->run->exit_reason == KVM_EXIT_IO &&
		    vcpu->run->io.port == TEST_IO_PORT &&
		    vcpu->run->io.direction == KVM_EXIT_IO_IN) {
			continue;
		}
		break;
	}

	switch (get_ucall(vcpu, &uc)) {
	case UCALL_DONE:
		break;
	case UCALL_ABORT:
		REPORT_GUEST_ASSERT(uc);
	default:
		TEST_FAIL("Unexpected exit reason: %d", vcpu->run->exit_reason);
	}

	kvm_vm_free(vm);
}

int main(int argc, char *argv[])
{
	TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_VMX) || kvm_cpu_has(X86_FEATURE_SVM));
	TEST_REQUIRE(kvm_cpu_has_tdp());

	run_test(TEST_FINAL_PAGE_UNMAPPED);
	run_test(TEST_PT_PAGE_UNMAPPED);
	run_test(TEST_FINAL_PAGE_WRITE_PROTECTED);
	run_test(TEST_PT_PAGE_WRITE_PROTECTED);

	return 0;
}