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 2023 Oxide Computer Company
14  */
15 
16 /*
17  * Test guest frequency control
18  *
19  * Note: requires `vmm_allow_state_writes` to be set, and only on AMD
20  */
21 
22 #include <unistd.h>
23 #include <stdlib.h>
24 #include <libgen.h>
25 #include <errno.h>
26 #include <err.h>
27 
28 #include <sys/time.h>
29 #include <sys/vmm_data.h>
30 #include <sys/vmm_dev.h>
31 #include <vmmapi.h>
32 
33 #include "common.h"
34 #include "in_guest.h"
35 
36 #define	PPM_MARGIN	200
37 
38 typedef struct tsc_reading {
39 	hrtime_t when;
40 	uint64_t tsc;
41 } tsc_reading_t;
42 
43 static bool
check_reading(tsc_reading_t r1,tsc_reading_t r2,uint64_t guest_freq,uint64_t min_ticks,uint64_t tick_margin,uint32_t ppm_margin)44 check_reading(tsc_reading_t r1, tsc_reading_t r2, uint64_t guest_freq,
45     uint64_t min_ticks, uint64_t tick_margin, uint32_t ppm_margin)
46 {
47 	hrtime_t time_delta = r2.when - r1.when;
48 	uint64_t tick_delta = r2.tsc - r1.tsc;
49 
50 	/* check that number of ticks seen is valid */
51 	if (tick_delta < min_ticks) {
52 		test_fail_msg("inadequate passage of guest TSC ticks %u < %u\n",
53 		    tick_delta, min_ticks);
54 	} else if ((tick_delta - min_ticks) > tick_margin) {
55 		(void) printf("%u ticks outside margin %u\n", tick_delta,
56 		    min_ticks + tick_margin);
57 		return (false);
58 	}
59 
60 	/* compute ppm error and validate */
61 	hrtime_t time_target = (tick_delta * NANOSEC) / guest_freq;
62 	hrtime_t offset;
63 	if (time_delta < time_target) {
64 		offset = time_target - time_delta;
65 	} else {
66 		offset = time_delta - time_target;
67 	}
68 	uint64_t ppm = (offset * 1000000) / time_target;
69 	(void) printf("%u ticks in %lu ns (error %lu ppm)\n",
70 	    tick_delta, time_delta, ppm);
71 	if (ppm > ppm_margin) {
72 		(void) printf("UNACCEPTABLE!\n");
73 		return (false);
74 	}
75 	return (true);
76 }
77 
78 void
do_freq_test(uint64_t guest_freq,uint8_t per_sec,uint8_t seconds,const int vmfd,struct vcpu * vcpu,struct vdi_time_info_v1 * src)79 do_freq_test(uint64_t guest_freq, uint8_t per_sec, uint8_t seconds,
80     const int vmfd, struct vcpu *vcpu, struct vdi_time_info_v1 *src)
81 {
82 	/* configure the guest to have the desired frequency */
83 	struct vdi_time_info_v1 time_info = {
84 		.vt_guest_freq = guest_freq,
85 		.vt_guest_tsc = src->vt_guest_tsc,
86 		.vt_boot_hrtime = src->vt_boot_hrtime,
87 		.vt_hrtime = src->vt_hrtime,
88 		.vt_hres_sec = src->vt_hres_sec,
89 		.vt_hres_ns = src->vt_hres_ns,
90 	};
91 	struct vm_data_xfer xfer = {
92 		.vdx_class = VDC_VMM_TIME,
93 		.vdx_version = 1,
94 		.vdx_len = sizeof (struct vdi_time_info_v1),
95 		.vdx_data = &time_info,
96 	};
97 	if (ioctl(vmfd, VM_DATA_WRITE, &xfer) != 0) {
98 		int error;
99 		error = errno;
100 		if (error == EPERM) {
101 			warn("VMM_DATA_WRITE got EPERM: is "
102 			    "vmm_allow_state_writes set?");
103 		}
104 		errx(EXIT_FAILURE, "VMM_DATA_WRITE of time info failed");
105 	}
106 
107 
108 	/*
109 	 * Run the test:
110 	 * - ask the guest to report the TSC every 1/per_sec seconds, in terms
111 	 *   of guest ticks
112 	 * - collect readings for `seconds` seconds, along with host hrtime
113 	 * - to avoid additional latency in the readings, process readings at
114 	 *   the end: check for error in the number of ticks reported and the
115 	 *   ppm based on host hrtime
116 	 */
117 	uint64_t guest_ticks = guest_freq / per_sec;
118 	const uint32_t nreadings = per_sec * seconds + 1;
119 	tsc_reading_t tsc_readings[nreadings];
120 
121 	/* 5 percent margin */
122 	const uint32_t tick_margin = guest_ticks / 20;
123 
124 	bool half_read = false;
125 	uint64_t cur_tsc;
126 	uint32_t count = 0;
127 
128 	struct vm_entry ventry = { 0 };
129 	struct vm_exit vexit = { 0 };
130 
131 	do {
132 		if (count >= nreadings) {
133 			/* test completed: check results */
134 			for (int i = 1; i < nreadings; i++) {
135 				if (!check_reading(tsc_readings[i-1],
136 				    tsc_readings[i], guest_freq, guest_ticks,
137 				    tick_margin, PPM_MARGIN)) {
138 					test_fail_msg("freq test failed");
139 				}
140 			}
141 			break;
142 		}
143 
144 		const enum vm_exit_kind kind =
145 		    test_run_vcpu(vcpu, &ventry, &vexit);
146 
147 		if (kind == VEK_REENTR) {
148 			continue;
149 		} else if (kind != VEK_UNHANDLED) {
150 			test_fail_vmexit(&vexit);
151 		}
152 
153 		uint32_t val;
154 		if (vexit_match_inout(&vexit, true, IOP_TEST_VALUE, 4,
155 		    &val)) {
156 			/* test setup: tell guest how often to report its tsc */
157 			ventry_fulfill_inout(&vexit, &ventry, guest_ticks);
158 
159 		} else if (vexit_match_inout(&vexit, false, IOP_TEST_VALUE, 4,
160 		    &val)) {
161 			/*
162 			 * Get reported guest TSC in two 32-bit parts, with the
163 			 * lower bits coming in first.
164 			 */
165 			if (!half_read) {
166 				/* lower bits */
167 				cur_tsc = val;
168 				half_read = true;
169 				ventry_fulfill_inout(&vexit, &ventry, 0);
170 			} else {
171 				/* upper bits */
172 				cur_tsc |= ((uint64_t)val << 32);
173 
174 				tsc_readings[count].when = gethrtime();
175 				tsc_readings[count].tsc = cur_tsc;
176 
177 				half_read = false;
178 				cur_tsc = 0;
179 				count++;
180 
181 				ventry_fulfill_inout(&vexit, &ventry, 0);
182 			}
183 		} else {
184 			test_fail_vmexit(&vexit);
185 		}
186 	} while (true);
187 }
188 
189 int
main(int argc,char * argv[])190 main(int argc, char *argv[])
191 {
192 	const char *test_suite_name = basename(argv[0]);
193 	struct vmctx *ctx = NULL;
194 	struct vcpu *vcpu;
195 	int err;
196 
197 	ctx = test_initialize(test_suite_name);
198 
199 	if ((vcpu = vm_vcpu_open(ctx, 0)) == NULL) {
200 		test_fail_errno(errno, "Could not open vcpu0");
201 	}
202 
203 	err = test_setup_vcpu(vcpu, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
204 	if (err != 0) {
205 		test_fail_errno(err, "Could not initialize vcpu0");
206 	}
207 
208 	const int vmfd = vm_get_device_fd(ctx);
209 	const bool is_svm = cpu_vendor_amd();
210 
211 	if (!is_svm) {
212 		test_fail_msg("intel not supported\n");
213 	}
214 
215 	/* Read time data to get baseline guest time values */
216 	struct vdi_time_info_v1 time_info;
217 	struct vm_data_xfer xfer = {
218 		.vdx_class = VDC_VMM_TIME,
219 		.vdx_version = 1,
220 		.vdx_len = sizeof (struct vdi_time_info_v1),
221 		.vdx_data = &time_info,
222 	};
223 	if (ioctl(vmfd, VM_DATA_READ, &xfer) != 0) {
224 		errx(EXIT_FAILURE, "VMM_DATA_READ of time info failed");
225 	}
226 	uint64_t host_freq = time_info.vt_guest_freq;
227 	uint64_t guest_freq = host_freq;
228 
229 	/* measure each test frequency 10x per sec, for 1 second */
230 	const uint8_t per_sec = 10;
231 	const uint8_t seconds = 1;
232 
233 	/* 2x host frequency */
234 	guest_freq = host_freq * 2;
235 	(void) printf("testing 2x host_freq: guest_freq=%lu, host_freq=%lu\n",
236 	    guest_freq, host_freq);
237 	do_freq_test(guest_freq, per_sec, seconds, vmfd, vcpu, &time_info);
238 
239 	/* reset guest */
240 	vm_vcpu_close(vcpu);
241 	test_cleanup(false);
242 	ctx = test_initialize(test_suite_name);
243 	if ((vcpu = vm_vcpu_open(ctx, 0)) == NULL) {
244 		test_fail_errno(errno, "Could not open vcpu0");
245 	}
246 	err = test_setup_vcpu(vcpu, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
247 	if (err != 0) {
248 		test_fail_errno(err, "Could not initialize vcpu0");
249 	}
250 
251 
252 	/* 0.5x host frequency */
253 	guest_freq = host_freq / 2;
254 	(void) printf("testing 0.5x host_freq: guest_freq=%lu, host_freq=%lu\n",
255 	    guest_freq, host_freq);
256 	do_freq_test(guest_freq, per_sec, seconds, vmfd, vcpu, &time_info);
257 
258 	/* reset guest */
259 	vm_vcpu_close(vcpu);
260 	test_cleanup(false);
261 	ctx = test_initialize(test_suite_name);
262 	if ((vcpu = vm_vcpu_open(ctx, 0)) == NULL) {
263 		test_fail_errno(errno, "Could not open vcpu0");
264 	}
265 	err = test_setup_vcpu(vcpu, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
266 	if (err != 0) {
267 		test_fail_errno(err, "Could not initialize vcpu0");
268 	}
269 
270 
271 	/* 1/3 host frequency */
272 	guest_freq = host_freq / 3;
273 	(void) printf("testing 1/3 host_freq: guest_freq=%lu, host_freq=%lu\n",
274 	    guest_freq, host_freq);
275 	do_freq_test(guest_freq, per_sec, seconds, vmfd, vcpu, &time_info);
276 
277 	/* reset guest */
278 	vm_vcpu_close(vcpu);
279 	test_cleanup(false);
280 	ctx = test_initialize(test_suite_name);
281 	if ((vcpu = vm_vcpu_open(ctx, 0)) == NULL) {
282 		test_fail_errno(errno, "Could not open vcpu0");
283 	}
284 	err = test_setup_vcpu(vcpu, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
285 	if (err != 0) {
286 		test_fail_errno(err, "Could not initialize vcpu0");
287 	}
288 
289 
290 	/* 1x host frequency */
291 	guest_freq = host_freq;
292 	(void) printf("testing 1x host_freq: guest_freq=%lu, host_freq=%lu\n",
293 	    guest_freq, host_freq);
294 	do_freq_test(guest_freq, per_sec, seconds, vmfd, vcpu, &time_info);
295 
296 	test_pass();
297 }
298