xref: /illumos-gate/usr/src/test/os-tests/tests/xsave/spawn_fpu.c (revision ac2250cb76bb32944fd2c8a3ba2cd3f79747748d)
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 2026 Oxide Computer Company
14  */
15 
16 /*
17  * Verify that a process born via posix_spawn(3C) comes to life with a correctly
18  * initialised FPU.
19  *
20  * Unlike a fork(2) child, a spawned process is created as a bare kernel thread
21  * that never passes through forklwp()/fp_new_lwp(), so it inherits neither FPU
22  * register state nor the FPU thread context operations from its parent. Both
23  * are instead established from scratch by fp_exec() when the child execs its
24  * target, which is the same path the kernel uses to bring up init. This test
25  * confirms, from inside a spawned child, that:
26  *
27  *   o the initial x87 control word and MXCSR are the architectural defaults,
28  *     proving fp_exec()/fpinit() ran for the spawned LWP;
29  *   o the full vector register file survives context switches while sibling
30  *     threads hammer the FPU, making it very likely that the context
31  *     operations were installed and actually save and restore our state.
32  *
33  * A fork(2)+exec(2) child is run through the identical checks first as a
34  * control. The test needs xsave + AVX (it reuses the xsave test support
35  * library) and is gated on that hardware by xsu_hwtype in the runfile.
36  */
37 
38 #include <err.h>
39 #include <errno.h>
40 #include <ieeefp.h>
41 #include <spawn.h>
42 #include <stdint.h>
43 #include <stdio.h>
44 #include <stdlib.h>
45 #include <stdbool.h>
46 #include <string.h>
47 #include <thread.h>
48 #include <ucontext.h>
49 #include <unistd.h>
50 #include <sys/types.h>
51 #include <sys/fp.h>
52 #include <sys/processor.h>
53 #include <sys/procset.h>
54 #include <sys/time.h>
55 #include <sys/wait.h>
56 
57 #include "xsave_util.h"
58 
59 extern char **environ;
60 
61 /*
62  * How long to spend repeatedly reloading and verifying the vector register
63  * file while the sibling threads create FPU contention.
64  */
65 #define	SPAWN_FPU_RUNTIME_MS	500
66 
67 /*
68  * Number of FPU-hammering sibling threads to share the bound CPU with the
69  * thread under test. Pinning a handful to a single CPU gives reasonably
70  * reliable contention.
71  */
72 #define	SPAWN_FPU_NRUNNERS	4
73 
74 static uint_t failures;
75 
76 typedef struct {
77 	uint32_t	fra_hwsup;
78 	processorid_t	fra_cpu;
79 } fpu_runner_arg_t;
80 
81 /*
82  * Continually load the vector registers with a distinct pattern. Run by the
83  * sibling threads purely to dirty the physical registers so that, on a kernel
84  * that failed to install the spawned thread's FPU context operations, a context
85  * switch would leak this state into the thread under test.
86  */
87 static void *
fpu_runner(void * arg)88 fpu_runner(void *arg)
89 {
90 	const fpu_runner_arg_t *ra = arg;
91 	uint32_t seed = 0x80000000;
92 	xsu_fpu_t buf;
93 
94 	if (ra->fra_cpu != -1)
95 		(void) processor_bind(P_LWPID, P_MYID, ra->fra_cpu, NULL);
96 
97 	for (;;) {
98 		xsu_fill(&buf, ra->fra_hwsup, seed);
99 		xsu_setfpu(&buf, ra->fra_hwsup);
100 		seed += 0x1000;
101 	}
102 
103 	/* NOTREACHED */
104 	return (NULL);
105 }
106 
107 static processorid_t
bind_to_cpu(void)108 bind_to_cpu(void)
109 {
110 	long maxcpu = sysconf(_SC_CPUID_MAX);
111 
112 	for (processorid_t cpu = 0; cpu <= maxcpu; cpu++) {
113 		if (processor_bind(P_LWPID, P_MYID, cpu, NULL) == 0)
114 			return (cpu);
115 	}
116 
117 	return (-1);
118 }
119 
120 /*
121  * Snapshot the calling thread's initial FPU control word and MXCSR.
122  * getcontext() forces an FPU save, after which we read them from the saved
123  * state. Keeping this in its own function means getcontext(), which the
124  * compiler treats like setjmp(), does not force the caller's locals volatile.
125  */
126 static void
read_initial_fpu(uint32_t * cwp,uint32_t * mxcsrp)127 read_initial_fpu(uint32_t *cwp, uint32_t *mxcsrp)
128 {
129 	ucontext_t uc;
130 
131 	if (getcontext(&uc) != 0)
132 		err(EXIT_FAILURE, "getcontext failed");
133 
134 	/*
135 	 * The x87 control word is a named field on amd64. On i386 the
136 	 * fpchip_state is the opaque legacy save area, so we overlay the ILP32
137 	 * struct _fpstate to read it, as xsave_util.c does.
138 	 */
139 #ifdef __amd64
140 	*cwp = uc.uc_mcontext.fpregs.fp_reg_set.fpchip_state.cw & 0xffff;
141 #else
142 	struct _fpstate fps;
143 
144 	(void) memcpy(&fps, &uc.uc_mcontext.fpregs.fp_reg_set.fpchip_state,
145 	    sizeof (fps));
146 	*cwp = fps.cw & 0xffff;
147 #endif
148 	*mxcsrp = uc.uc_mcontext.fpregs.fp_reg_set.fpchip_state.mxcsr;
149 }
150 
151 static int
fpu_child(void)152 fpu_child(void)
153 {
154 	static fpu_runner_arg_t ra;
155 	uint32_t hwsup, cw, mxcsr;
156 	uint32_t seed = 1;
157 	long ncpu;
158 	uint_t nrun;
159 	hrtime_t end;
160 	int ret = EXIT_SUCCESS;
161 
162 	read_initial_fpu(&cw, &mxcsr);
163 
164 	hwsup = xsu_hwsupport();
165 
166 	if (cw != FPU_CW_INIT) {
167 		warnx("initial x87 control word is %#x, expected %#x", cw,
168 		    FPU_CW_INIT);
169 		ret = EXIT_FAILURE;
170 	}
171 	if (mxcsr != SSE_MXCSR_INIT) {
172 		warnx("initial MXCSR is %#x, expected %#x", mxcsr,
173 		    SSE_MXCSR_INIT);
174 		ret = EXIT_FAILURE;
175 	}
176 
177 	/*
178 	 * Force the thread under test to share a CPU with FPU-hammering
179 	 * siblings.
180 	 */
181 	ra.fra_hwsup = hwsup;
182 	ra.fra_cpu = bind_to_cpu();
183 	if (ra.fra_cpu != -1) {
184 		nrun = SPAWN_FPU_NRUNNERS;
185 	} else {
186 		ncpu = sysconf(_SC_NPROCESSORS_ONLN);
187 		if (ncpu < 1)
188 			ncpu = 1;
189 		nrun = 2 * (uint_t)ncpu;
190 	}
191 
192 	for (uint_t i = 0; i < nrun; i++) {
193 		thread_t tid;
194 		int e = thr_create(NULL, 0, fpu_runner, &ra, THR_DETACHED,
195 		    &tid);
196 		if (e != 0)
197 			errc(EXIT_FAILURE, e, "failed to create FPU runner");
198 	}
199 
200 	/*
201 	 * Load a known pattern and read it straight back. There is no
202 	 * FPU-clobbering call between the two so the pattern should
203 	 * survive even if we are preempted in the window.
204 	 */
205 	end = gethrtime() + (hrtime_t)SPAWN_FPU_RUNTIME_MS *
206 	    (NANOSEC / MILLISEC);
207 	do {
208 		xsu_fpu_t set, got;
209 
210 		xsu_fill(&set, hwsup, seed);
211 		xsu_setfpu(&set, hwsup);
212 
213 		/*
214 		 * Widen the window in which our vector state has to survive a
215 		 * context switch. This spin only touches integer registers, so
216 		 * it cannot disturb the FPU state we just loaded.
217 		 */
218 		for (volatile uint_t d = 0; d < 10000; d++)
219 			continue;
220 
221 		xsu_getfpu(&got, hwsup);
222 		if (!xsu_same(&set, &got, hwsup)) {
223 			warnx("vector register state was not preserved across "
224 			    "a context switch (seed %#x)", seed);
225 			ret = EXIT_FAILURE;
226 			break;
227 		}
228 		seed += 0x40;
229 	} while (gethrtime() < end);
230 
231 	return (ret);
232 }
233 
234 static void
run_child(const char * desc,bool use_spawn,const char * path)235 run_child(const char *desc, bool use_spawn, const char *path)
236 {
237 	char *argv[] = { (char *)path, "child", NULL };
238 	pid_t pid;
239 	int status;
240 
241 	if (use_spawn) {
242 		int e = posix_spawn(&pid, path, NULL, NULL, argv, environ);
243 		if (e != 0)
244 			errc(EXIT_FAILURE, e, "posix_spawn of %s failed", path);
245 	} else {
246 		pid = fork();
247 		if (pid == -1)
248 			err(EXIT_FAILURE, "fork failed");
249 		if (pid == 0) {
250 			(void) execv(path, argv);
251 			err(127, "execv of %s failed", path);
252 		}
253 	}
254 
255 	while (waitpid(pid, &status, 0) != pid) {
256 		if (errno != EINTR)
257 			err(EXIT_FAILURE, "waitpid failed");
258 	}
259 
260 	if (!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) {
261 		(void) fprintf(stderr, "TEST FAILED: %s: child status %#x\n",
262 		    desc, status);
263 		failures++;
264 		return;
265 	}
266 
267 	(void) printf("TEST PASSED: %s\n", desc);
268 }
269 
270 int
main(int argc,char * argv[])271 main(int argc, char *argv[])
272 {
273 	const char *path;
274 
275 	if (argc > 1 && strcmp(argv[1], "child") == 0)
276 		return (fpu_child());
277 
278 	path = getexecname();
279 	if (path == NULL)
280 		errx(EXIT_FAILURE, "could not determine own path");
281 
282 	run_child("fork+exec child FPU initialisation", false, path);
283 	run_child("posix_spawn child FPU initialisation", true, path);
284 
285 	if (failures != 0) {
286 		(void) fprintf(stderr, "%u test(s) failed\n", failures);
287 		return (EXIT_FAILURE);
288 	}
289 
290 	(void) printf("All tests passed\n");
291 	return (EXIT_SUCCESS);
292 }
293