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
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
79 do_freq_test(uint64_t guest_freq, uint8_t per_sec, uint8_t seconds,
80     const int vmfd, struct vmctx *ctx, 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(ctx, 0, &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
190 main(int argc, char *argv[])
191 {
192 	const char *test_suite_name = basename(argv[0]);
193 	struct vmctx *ctx = NULL;
194 	int err;
195 
196 	ctx = test_initialize(test_suite_name);
197 
198 	err = test_setup_vcpu(ctx, 0, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
199 	if (err != 0) {
200 		test_fail_errno(err, "Could not initialize vcpu0");
201 	}
202 
203 	const int vmfd = vm_get_device_fd(ctx);
204 	const bool is_svm = cpu_vendor_amd();
205 
206 	if (!is_svm) {
207 		test_fail_msg("intel not supported\n");
208 	}
209 
210 	/* Read time data to get baseline guest time values */
211 	struct vdi_time_info_v1 time_info;
212 	struct vm_data_xfer xfer = {
213 		.vdx_class = VDC_VMM_TIME,
214 		.vdx_version = 1,
215 		.vdx_len = sizeof (struct vdi_time_info_v1),
216 		.vdx_data = &time_info,
217 	};
218 	if (ioctl(vmfd, VM_DATA_READ, &xfer) != 0) {
219 		errx(EXIT_FAILURE, "VMM_DATA_READ of time info failed");
220 	}
221 	uint64_t host_freq = time_info.vt_guest_freq;
222 	uint64_t guest_freq = host_freq;
223 
224 	/* measure each test frequency 10x per sec, for 1 second */
225 	const uint8_t per_sec = 10;
226 	const uint8_t seconds = 1;
227 
228 	/* 2x host frequency */
229 	guest_freq = host_freq * 2;
230 	(void) printf("testing 2x host_freq: guest_freq=%lu, host_freq=%lu\n",
231 	    guest_freq, host_freq);
232 	do_freq_test(guest_freq, per_sec, seconds, vmfd, ctx, &time_info);
233 
234 	/* reset guest */
235 	test_cleanup(false);
236 	ctx = test_initialize(test_suite_name);
237 	err = test_setup_vcpu(ctx, 0, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
238 	if (err != 0) {
239 		test_fail_errno(err, "Could not initialize vcpu0");
240 	}
241 
242 
243 	/* 0.5x host frequency */
244 	guest_freq = host_freq / 2;
245 	(void) printf("testing 0.5x host_freq: guest_freq=%lu, host_freq=%lu\n",
246 	    guest_freq, host_freq);
247 	do_freq_test(guest_freq, per_sec, seconds, vmfd, ctx, &time_info);
248 
249 	/* reset guest */
250 	test_cleanup(false);
251 	ctx = test_initialize(test_suite_name);
252 	err = test_setup_vcpu(ctx, 0, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
253 	if (err != 0) {
254 		test_fail_errno(err, "Could not initialize vcpu0");
255 	}
256 
257 
258 	/* 1/3 host frequency */
259 	guest_freq = host_freq / 3;
260 	(void) printf("testing 1/3 host_freq: guest_freq=%lu, host_freq=%lu\n",
261 	    guest_freq, host_freq);
262 	do_freq_test(guest_freq, per_sec, seconds, vmfd, ctx, &time_info);
263 
264 	/* reset guest */
265 	test_cleanup(false);
266 	ctx = test_initialize(test_suite_name);
267 	err = test_setup_vcpu(ctx, 0, MEM_LOC_PAYLOAD, MEM_LOC_STACK);
268 	if (err != 0) {
269 		test_fail_errno(err, "Could not initialize vcpu0");
270 	}
271 
272 
273 	/* 1x host frequency */
274 	guest_freq = host_freq;
275 	(void) printf("testing 1x host_freq: guest_freq=%lu, host_freq=%lu\n",
276 	    guest_freq, host_freq);
277 	do_freq_test(guest_freq, per_sec, seconds, vmfd, ctx, &time_info);
278 
279 	test_pass();
280 }
281