1 /*
2 * This file and its contents are supplied under the terms of the
3 * Common Development and Distribution License ("CDDL"), version 1.0.
4 * You may only use this file in accordance with the terms of version
5 * 1.0 of the CDDL.
6 *
7 * A full copy of the text of the CDDL should have accompanied this
8 * source. A copy of the CDDL is also available via the Internet at
9 * http://www.illumos.org/license/CDDL.
10 */
11
12 /*
13 * Copyright 2024 Oxide Computer Company
14 */
15
16 #include <stdio.h>
17 #include <unistd.h>
18 #include <stdlib.h>
19 #include <strings.h>
20 #include <libgen.h>
21 #include <assert.h>
22 #include <errno.h>
23
24 #include <sys/types.h>
25 #include <sys/sysmacros.h>
26 #include <sys/debug.h>
27 #include <sys/mman.h>
28 #include <sys/vmm.h>
29 #include <sys/vmm_dev.h>
30 #include <vmmapi.h>
31
32 #include "common.h"
33 #include "in_guest.h"
34
35 #define PAGE_SZ 4096
36
37 #define DIRTY_BITMAP_SZ (MEM_TOTAL_SZ / (PAGE_SZ * 8))
38
39 static void
read_dirty_bitmap(struct vmctx * ctx,uint8_t * bitmap)40 read_dirty_bitmap(struct vmctx *ctx, uint8_t *bitmap)
41 {
42 struct vmm_dirty_tracker track = {
43 .vdt_start_gpa = 0,
44 .vdt_len = MEM_TOTAL_SZ,
45 .vdt_pfns = (void *)bitmap,
46 };
47 int err = ioctl(vm_get_device_fd(ctx), VM_TRACK_DIRTY_PAGES, &track);
48 if (err != 0) {
49 test_fail_errno(errno, "Could not get dirty page bitmap");
50 }
51 }
52
53 static uint8_t
popc8(uint8_t val)54 popc8(uint8_t val)
55 {
56 uint8_t cnt;
57
58 for (cnt = 0; val != 0; val &= (val - 1)) {
59 cnt++;
60 }
61 return (cnt);
62 }
63
64 static uint_t
count_dirty_pages(const uint8_t * bitmap)65 count_dirty_pages(const uint8_t *bitmap)
66 {
67 uint_t count = 0;
68 for (uint_t i = 0; i < DIRTY_BITMAP_SZ; i++) {
69 count += popc8(bitmap[i]);
70 }
71 return (count);
72 }
73
74 void
check_supported(const char * test_suite_name)75 check_supported(const char *test_suite_name)
76 {
77 char name[VM_MAX_NAMELEN];
78 int err;
79
80 name_test_vm(test_suite_name, name);
81
82 err = vm_create(name, VCF_TRACK_DIRTY);
83 if (err == 0) {
84 /*
85 * We created the VM successfully, so we know that dirty page
86 * tracking is supported.
87 */
88 err = destroy_instance(test_suite_name);
89 if (err != 0) {
90 (void) fprintf(stderr,
91 "Could not destroy VM: %s\n", strerror(errno));
92 (void) printf("FAIL %s\n", test_suite_name);
93 exit(EXIT_FAILURE);
94 }
95 } else if (errno == ENOTSUP) {
96 (void) printf(
97 "Skipping test: dirty page tracking not supported\n");
98 (void) printf("PASS %s\n", test_suite_name);
99 exit(EXIT_SUCCESS);
100 } else {
101 /*
102 * Ignore any other errors, they'll be caught by subsequent
103 * test routines.
104 */
105 }
106 }
107
108 void
test_dirty_tracking_disabled(const char * test_suite_name)109 test_dirty_tracking_disabled(const char *test_suite_name)
110 {
111 struct vmctx *ctx = NULL;
112 struct vcpu *vcpu;
113 int err;
114
115 uint8_t dirty_bitmap[DIRTY_BITMAP_SZ] = { 0 };
116 struct vmm_dirty_tracker track = {
117 .vdt_start_gpa = 0,
118 .vdt_len = MEM_TOTAL_SZ,
119 .vdt_pfns = (void *)dirty_bitmap,
120 };
121
122 /* Create VM without VCF_TRACK_DIRTY flag */
123 ctx = test_initialize_flags(test_suite_name, 0);
124
125 if ((vcpu = vm_vcpu_open(ctx, 0)) == NULL) {
126 test_fail_errno(errno, "Could not open vcpu0");
127 }
128
129 err = test_setup_vcpu(vcpu, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
130 if (err != 0) {
131 test_fail_errno(err, "Could not initialize vcpu0");
132 }
133
134 /* Try to query for dirty pages */
135 err = ioctl(vm_get_device_fd(ctx), VM_TRACK_DIRTY_PAGES, &track);
136 if (err == 0) {
137 test_fail_msg("VM_TRACK_DIRTY_PAGES succeeded unexpectedly\n");
138 } else if (errno != EPERM) {
139 test_fail_errno(errno,
140 "VM_TRACK_DIRTY_PAGES failed with unexpected error");
141 }
142
143 test_cleanup(false);
144 }
145
146 int
main(int argc,char * argv[])147 main(int argc, char *argv[])
148 {
149 const char *test_suite_name = basename(argv[0]);
150 struct vmctx *ctx = NULL;
151 struct vcpu *vcpu;
152 int err;
153
154 /* Skip test if CPU doesn't support HW A/D tracking */
155 check_supported(test_suite_name);
156
157 /* Test for expected error with dirty tracking disabled */
158 test_dirty_tracking_disabled(test_suite_name);
159
160 ctx = test_initialize_flags(test_suite_name, VCF_TRACK_DIRTY);
161
162 if ((vcpu = vm_vcpu_open(ctx, 0)) == NULL) {
163 test_fail_errno(errno, "Could not open vcpu0");
164 }
165 err = test_setup_vcpu(vcpu, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
166 if (err != 0) {
167 test_fail_errno(err, "Could not initialize vcpu0");
168 }
169
170 uint8_t dirty_bitmap[DIRTY_BITMAP_SZ] = { 0 };
171
172 /* Clear pages which were dirtied as part of initialization */
173 read_dirty_bitmap(ctx, dirty_bitmap);
174 if (count_dirty_pages(dirty_bitmap) == 0) {
175 test_fail_msg("no pages dirtied during setup\n");
176 }
177
178 /*
179 * With nothing running, and the old dirty bits cleared, the NPT should
180 * now be devoid of pages marked dirty.
181 */
182 read_dirty_bitmap(ctx, dirty_bitmap);
183 if (count_dirty_pages(dirty_bitmap) != 0) {
184 test_fail_msg("pages still dirty after clear\n");
185 }
186
187 /* Dirty a page through the segvmm mapping. */
188 uint8_t *dptr = vm_map_gpa(ctx, MEM_LOC_STACK, 1);
189 *dptr = 1;
190
191 /* Check that it was marked as such */
192 read_dirty_bitmap(ctx, dirty_bitmap);
193 if (count_dirty_pages(dirty_bitmap) != 1) {
194 test_fail_msg("direct access did not dirty page\n");
195 }
196 if (dirty_bitmap[MEM_LOC_STACK / (PAGE_SZ * 8)] == 0) {
197 test_fail_msg("unexpected page dirtied\n");
198 }
199
200
201 /* Dirty it again to check shootdown logic */
202 *dptr = 2;
203 if (count_dirty_pages(dirty_bitmap) != 1) {
204 test_fail_msg("subsequent direct access did not dirty page\n");
205 }
206
207 struct vm_entry ventry = { 0 };
208 struct vm_exit vexit = { 0 };
209 do {
210 const enum vm_exit_kind kind =
211 test_run_vcpu(vcpu, &ventry, &vexit);
212 switch (kind) {
213 case VEK_REENTR:
214 break;
215 case VEK_TEST_PASS:
216 /*
217 * By now, the guest should have dirtied that page
218 * directly via hardware-accelerated path.
219 */
220 read_dirty_bitmap(ctx, dirty_bitmap);
221 /*
222 * The guest will dirty more than the page it is
223 * explicitly writing to: it must mark its own page
224 * tables with accessed/dirty bits too.
225 */
226 if (count_dirty_pages(dirty_bitmap) <= 1) {
227 test_fail_msg(
228 "in-guest access did not dirty page\n");
229 }
230 if (dirty_bitmap[MEM_LOC_STACK / (PAGE_SZ * 8)] == 0) {
231 test_fail_msg("expected page not dirtied\n");
232 }
233 test_pass();
234 break;
235 default:
236 test_fail_vmexit(&vexit);
237 break;
238 }
239 } while (true);
240 }
241