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