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 * Tests for the raw spawn(2) system call interface. These concentrate
18 * mainly on checking that the kernel fully validates the marshalled
19 * spawn_param_t/spawn_args_t structures that libc constructs.
20 * We also test that a spawned child is robust against being killed in
21 * the window between creation and exec.
22 *
23 * Functional coverage of posix_spawn(3C) itself (attributes, file actions,
24 * PATH handling, etc.) lives in libc-tests/tests/posix_spawn.
25 */
26
27 #include <errno.h>
28 #include <signal.h>
29 #include <spawn.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <unistd.h>
34 #include <sys/syscall.h>
35 #include <sys/spawn_impl.h>
36 #include <sys/types.h>
37 #include <sys/wait.h>
38
39 static uint_t failures = 0;
40
41 #define TFAIL(name, fmt, ...) do { \
42 (void) fprintf(stderr, "TEST FAILED: %s: " fmt "\n", \
43 (name), ##__VA_ARGS__); \
44 failures++; \
45 } while (0)
46
47 #define TPASS(name) (void) printf("TEST PASSED: %s\n", (name))
48
49 /*
50 * The spawn_args_t and spawn_param_t structures end in a variable-length
51 * data[] region. These tests build small instances on the stack and reserve a
52 * fixed amount of room for that region.
53 */
54 #define SA_DATA 16
55 #define SP_DATA 64
56
57 static pid_t
raw_spawn(const char * path,const void * sp,uint32_t spsize,const void * sa,uint32_t sasize)58 raw_spawn(const char *path, const void *sp, uint32_t spsize, const void *sa,
59 uint32_t sasize)
60 {
61 return (syscall(SYS_spawn, path, sp, spsize, sa, sasize));
62 }
63
64 /*
65 * Build a minimal valid spawn_args_t in the provided buffer:
66 * argv = { "x" }, empty environment.
67 */
68 static uint32_t
valid_args(spawn_args_t * sa)69 valid_args(spawn_args_t *sa)
70 {
71 uint32_t sasize = sizeof (*sa) + 2;
72
73 (void) memset(sa, 0, sasize);
74 sa->sa_size = sasize;
75 sa->sa_datalen = 2;
76 sa->sa_arg_cnt = 1;
77 sa->sa_env_off = 2;
78 sa->sa_data[0] = 'x';
79 sa->sa_data[1] = '\0';
80
81 return (sasize);
82 }
83
84 static const char *
errname(int err)85 errname(int err)
86 {
87 const char *name = strerrorname_np(err);
88
89 return (name != NULL ? name : "?");
90 }
91
92 static void
expect_err(const char * name,int wanted,const char * path,const void * sp,uint32_t spsize,const void * sa,uint32_t sasize)93 expect_err(const char *name, int wanted, const char *path, const void *sp,
94 uint32_t spsize, const void *sa, uint32_t sasize)
95 {
96 pid_t pid = raw_spawn(path, sp, spsize, sa, sasize);
97
98 if (pid != -1) {
99 TFAIL(name, "spawn unexpectedly succeeded (pid %d)",
100 (int)pid);
101 (void) waitpid(pid, NULL, 0);
102 return;
103 }
104 if (errno != wanted) {
105 TFAIL(name, "got %s (%d), wanted %s (%d)", errname(errno),
106 errno, errname(wanted), wanted);
107 return;
108 }
109 TPASS(name);
110 }
111
112 /*
113 * The positive case: a well-formed minimal request - a valid argument vector,
114 * no spawn parameters and no file actions - succeeds, and the child
115 * (/usr/bin/true) exits 0.
116 */
117 static void
t_valid(void)118 t_valid(void)
119 {
120 union {
121 spawn_args_t sa;
122 char pad[sizeof (spawn_args_t) + SA_DATA];
123 } au;
124 uint32_t sasize = valid_args(&au.sa);
125 int status;
126 pid_t pid;
127
128 pid = raw_spawn("/usr/bin/true", NULL, 0, &au.sa, sasize);
129 if (pid == -1) {
130 TFAIL("valid", "spawn failed: %s", strerror(errno));
131 return;
132 }
133 if (waitpid(pid, &status, 0) != pid) {
134 TFAIL("valid", "waitpid: %s", strerror(errno));
135 return;
136 }
137 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
138 TFAIL("valid", "status %#x", status);
139 return;
140 }
141 TPASS("valid, well-formed, minimal spawn request");
142 }
143
144 static void
t_args_fuzz(void)145 t_args_fuzz(void)
146 {
147 union {
148 spawn_args_t sa;
149 char pad[sizeof (spawn_args_t) + SA_DATA];
150 } au;
151 spawn_args_t *sa = &au.sa;
152 uint32_t sasize;
153
154 expect_err("null-path", EINVAL, NULL, NULL, 0, &au.sa,
155 valid_args(&au.sa));
156 expect_err("null-args", EINVAL, "/usr/bin/true", NULL, 0, NULL, 0);
157
158 sasize = valid_args(sa);
159 sa->sa_size = sasize + 8;
160 expect_err("args-size-mismatch", EINVAL, "/usr/bin/true", NULL, 0,
161 sa, sasize);
162
163 sasize = valid_args(sa);
164 sa->sa_datalen = 1;
165 expect_err("args-datalen-mismatch", EINVAL, "/usr/bin/true", NULL, 0,
166 sa, sasize);
167
168 sasize = valid_args(sa);
169 sa->sa_arg_cnt = 100;
170 expect_err("args-cnt-overrun", EINVAL, "/usr/bin/true", NULL, 0,
171 sa, sasize);
172
173 sasize = valid_args(sa);
174 sa->sa_data[1] = 'y'; /* string no longer NUL-terminated */
175 expect_err("args-unterminated", EINVAL, "/usr/bin/true", NULL, 0,
176 sa, sasize);
177
178 sasize = valid_args(sa);
179 sa->sa_env_off = 100;
180 expect_err("args-env-off", EINVAL, "/usr/bin/true", NULL, 0,
181 sa, sasize);
182
183 sasize = valid_args(sa);
184 sa->sa_env_cnt = 7;
185 expect_err("args-env-cnt", EINVAL, "/usr/bin/true", NULL, 0,
186 sa, sasize);
187
188 sasize = valid_args(sa);
189 expect_err("args-e2big", E2BIG, "/usr/bin/true", NULL, 0,
190 sa, 0x400000);
191
192 /* Truncated buffer: shorter than the header itself. */
193 sasize = valid_args(sa);
194 expect_err("args-short", EINVAL, "/usr/bin/true", NULL, 0,
195 sa, sizeof (*sa) - 4);
196 }
197
198 static void
t_param_fuzz(void)199 t_param_fuzz(void)
200 {
201 union {
202 spawn_args_t sa;
203 char pad[sizeof (spawn_args_t) + SA_DATA];
204 } au;
205 union {
206 spawn_param_t sp;
207 char pad[sizeof (spawn_param_t) + SP_DATA];
208 } pu;
209 spawn_param_t *sp = &pu.sp;
210 uint32_t sasize = valid_args(&au.sa);
211 uint32_t spsize = sizeof (*sp) + SP_DATA;
212 kfile_attr_t *kfa;
213
214 /* Size field disagreeing with the system call argument. */
215 (void) memset(&pu, 0, sizeof (pu));
216 sp->sp_size = spsize + 4;
217 sp->sp_datalen = SP_DATA;
218 expect_err("param-size-mismatch", EINVAL, "/usr/bin/true",
219 sp, spsize, &au.sa, sasize);
220
221 /* Attribute region out of bounds. */
222 (void) memset(&pu, 0, sizeof (pu));
223 sp->sp_size = spsize;
224 sp->sp_datalen = SP_DATA;
225 sp->sp_attr_off = 60;
226 sp->sp_attr_len = sizeof (spawn_attr_t);
227 expect_err("param-attr-off", EINVAL, "/usr/bin/true",
228 sp, spsize, &au.sa, sasize);
229
230 /* Attribute length that is not sizeof (spawn_attr_t). */
231 (void) memset(&pu, 0, sizeof (pu));
232 sp->sp_size = spsize;
233 sp->sp_datalen = SP_DATA;
234 sp->sp_attr_len = 8;
235 expect_err("param-attr-len", EINVAL, "/usr/bin/true",
236 sp, spsize, &au.sa, sasize);
237
238 /* Undefined attribute flags. */
239 (void) memset(&pu, 0, sizeof (pu));
240 sp->sp_size = spsize;
241 sp->sp_datalen = SP_DATA;
242 sp->sp_attr_off = 0;
243 sp->sp_attr_len = sizeof (spawn_attr_t);
244 ((spawn_attr_t *)&sp->sp_data[0])->sa_psflags = ~0;
245 expect_err("param-attr-flags", EINVAL, "/usr/bin/true",
246 sp, spsize, &au.sa, sasize);
247
248 /* File action record with a zero length. */
249 (void) memset(&pu, 0, sizeof (pu));
250 sp->sp_size = spsize;
251 sp->sp_datalen = SP_DATA;
252 sp->sp_fattr_cnt = 2;
253 expect_err("param-fattr-len", EINVAL, "/usr/bin/true",
254 sp, spsize, &au.sa, sasize);
255
256 /* File action record with a bad type. */
257 (void) memset(&pu, 0, sizeof (pu));
258 sp->sp_size = spsize;
259 sp->sp_datalen = SP_DATA;
260 sp->sp_fattr_cnt = 1;
261 kfa = (kfile_attr_t *)&sp->sp_data[0];
262 kfa->kfa_len = sizeof (*kfa);
263 kfa->kfa_type = 666;
264 expect_err("param-fattr-type", EINVAL, "/usr/bin/true",
265 sp, spsize, &au.sa, sasize);
266
267 /* FA_CHDIR with an unterminated path. */
268 (void) memset(&pu, 0, sizeof (pu));
269 sp->sp_size = spsize;
270 sp->sp_datalen = SP_DATA;
271 sp->sp_fattr_cnt = 1;
272 kfa = (kfile_attr_t *)&sp->sp_data[0];
273 kfa->kfa_type = FA_CHDIR;
274 kfa->kfa_pathsize = 4;
275 kfa->kfa_len = sizeof (*kfa) + 4;
276 (void) memcpy(kfa->kfa_path, "/tmp", 4); /* no NUL */
277 expect_err("param-chdir-nul", EINVAL, "/usr/bin/true",
278 sp, spsize, &au.sa, sasize);
279
280 /* Unterminated shell and search path strings. */
281 (void) memset(&pu, 0, sizeof (pu));
282 sp->sp_size = spsize;
283 sp->sp_datalen = SP_DATA;
284 sp->sp_shell_off = 0;
285 sp->sp_shell_len = 4;
286 (void) memcpy(sp->sp_data, "/bin", 4);
287 expect_err("param-shell-nul", EINVAL, "/usr/bin/true",
288 sp, spsize, &au.sa, sasize);
289
290 (void) memset(&pu, 0, sizeof (pu));
291 sp->sp_size = spsize;
292 sp->sp_datalen = SP_DATA;
293 sp->sp_path_off = 62;
294 sp->sp_path_len = 8;
295 expect_err("param-path-off", EINVAL, "/usr/bin/true",
296 sp, spsize, &au.sa, sasize);
297
298 expect_err("param-e2big", E2BIG, "/usr/bin/true",
299 sp, 0x400000, &au.sa, sasize);
300
301 expect_err("param-short", EINVAL, "/usr/bin/true",
302 sp, sizeof (*sp) - 4, &au.sa, sasize);
303 }
304
305 /*
306 * Build a spawn_param_t carrying an attribute region with
307 * POSIX_SPAWN_SETSCHEDPARAM set and a scheduling region with the given
308 * operation.
309 */
310 static kspawn_sched_t *
valid_sched(spawn_param_t * sp,uint32_t spsize,int op)311 valid_sched(spawn_param_t *sp, uint32_t spsize, int op)
312 {
313 spawn_attr_t *spa;
314 kspawn_sched_t *ks;
315
316 (void) memset(sp, 0, spsize);
317 sp->sp_size = spsize;
318 sp->sp_datalen = spsize - sizeof (*sp);
319 sp->sp_attr_off = 0;
320 sp->sp_attr_len = sizeof (spawn_attr_t);
321 spa = (spawn_attr_t *)&sp->sp_data[0];
322 spa->sa_psflags = POSIX_SPAWN_SETSCHEDPARAM;
323 sp->sp_sched_off = sizeof (spawn_attr_t);
324 sp->sp_sched_len = sizeof (kspawn_sched_t);
325 ks = (kspawn_sched_t *)&sp->sp_data[sp->sp_sched_off];
326 ks->ksched_op = op;
327
328 return (ks);
329 }
330
331 static void
t_sched_fuzz(void)332 t_sched_fuzz(void)
333 {
334 union {
335 spawn_args_t sa;
336 char pad[sizeof (spawn_args_t) + SA_DATA];
337 } au;
338 union {
339 spawn_param_t sp;
340 char pad[sizeof (spawn_param_t) + 128];
341 } pu;
342 spawn_param_t *sp = &pu.sp;
343 uint32_t sasize = valid_args(&au.sa);
344 uint32_t spsize = sizeof (*sp) + 128;
345 kspawn_sched_t *ks;
346
347 /* Scheduling flag set but no scheduling region. */
348 (void) valid_sched(sp, spsize, KSCHED_PARMS);
349 sp->sp_sched_off = sp->sp_sched_len = 0;
350 expect_err("sched-missing", EINVAL, "/usr/bin/true",
351 sp, spsize, &au.sa, sasize);
352
353 /* Scheduling region present without a scheduling flag. */
354 (void) valid_sched(sp, spsize, KSCHED_PARMS);
355 ((spawn_attr_t *)&sp->sp_data[0])->sa_psflags = 0;
356 expect_err("sched-no-flags", EINVAL, "/usr/bin/true",
357 sp, spsize, &au.sa, sasize);
358
359 /* Scheduling region with the wrong length. */
360 (void) valid_sched(sp, spsize, KSCHED_PARMS);
361 sp->sp_sched_len = 8;
362 expect_err("sched-len", EINVAL, "/usr/bin/true",
363 sp, spsize, &au.sa, sasize);
364
365 /* Scheduling region out of bounds. */
366 (void) valid_sched(sp, spsize, KSCHED_PARMS);
367 sp->sp_sched_off = sp->sp_datalen - 4;
368 expect_err("sched-off", EINVAL, "/usr/bin/true",
369 sp, spsize, &au.sa, sasize);
370
371 /* Invalid operation. */
372 (void) valid_sched(sp, spsize, 0);
373 expect_err("sched-op", EINVAL, "/usr/bin/true",
374 sp, spsize, &au.sa, sasize);
375
376 /* A priority change must be a set operation. */
377 ks = valid_sched(sp, spsize, KSCHED_PRIO);
378 ks->ksched_prio.pc_op = PC_GETPRIO;
379 ks->ksched_prio.pc_cid = 1;
380 expect_err("sched-prio-op", EINVAL, "/usr/bin/true",
381 sp, spsize, &au.sa, sasize);
382
383 /*
384 * Class IDs out of range. These are not caught until the child
385 * applies the attributes to itself, exercising the error handshake.
386 */
387 ks = valid_sched(sp, spsize, KSCHED_PRIO);
388 ks->ksched_prio.pc_op = PC_SETPRIO;
389 ks->ksched_prio.pc_cid = 999;
390 expect_err("sched-prio-cid", EINVAL, "/usr/bin/true",
391 sp, spsize, &au.sa, sasize);
392
393 ks = valid_sched(sp, spsize, KSCHED_PARMS);
394 ks->ksched_parms.pc_cid = 999;
395 expect_err("sched-parms-cid", EINVAL, "/usr/bin/true",
396 sp, spsize, &au.sa, sasize);
397 }
398
399 /*
400 * A child killed immediately after spawn(2) returns must be waitable and
401 * must never leave the parent stuck. We try a few times to give us a fighting
402 * chance of catching it.
403 */
404 static void
t_kill_race(void)405 t_kill_race(void)
406 {
407 union {
408 spawn_args_t sa;
409 char pad[sizeof (spawn_args_t) + SA_DATA];
410 } au;
411 uint32_t sasize = valid_args(&au.sa);
412
413 /*
414 * Spawn a child and immediately kill it, over and over, to exercise
415 * the window between creation and exec. valid_args() supplies only
416 * argv[0], so sleep runs with no duration. That does not matter,
417 * because we SIGKILL it at once - all we need is a live PID to race
418 * against.
419 */
420 for (int i = 0; i < 100; i++) {
421 int status;
422 pid_t pid;
423
424 pid = raw_spawn("/usr/bin/sleep", NULL, 0, &au.sa, sasize);
425 if (pid == -1) {
426 TFAIL("kill-race", "spawn failed: %s",
427 strerror(errno));
428 return;
429 }
430 (void) kill(pid, SIGKILL);
431 if (waitpid(pid, &status, 0) != pid) {
432 TFAIL("kill-race", "waitpid: %s", strerror(errno));
433 return;
434 }
435 if (!WIFSIGNALED(status) &&
436 !(WIFEXITED(status) && WEXITSTATUS(status) != 0)) {
437 TFAIL("kill-race", "iteration %d: status %#x",
438 i, status);
439 return;
440 }
441 }
442 TPASS("kill-race");
443 }
444
445 int
main(void)446 main(void)
447 {
448 t_valid();
449 t_args_fuzz();
450 t_param_fuzz();
451 t_sched_fuzz();
452 t_kill_race();
453
454 if (failures == 0) {
455 (void) printf("All tests passed\n");
456 return (EXIT_SUCCESS);
457 }
458
459 (void) fprintf(stderr, "%u test(s) failed\n", failures);
460 return (EXIT_FAILURE);
461 }
462