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 * Various tests for posix_spawn(3C). Currently this mostly focuses on
18 * functionality added in POSIX 2024 which relates to SETSID and changing
19 * directories.
20 */
21
22 #include <err.h>
23 #include <stdlib.h>
24 #include <spawn.h>
25 #include <stdio.h>
26 #include <stdbool.h>
27 #include <unistd.h>
28 #include <sys/sysmacros.h>
29 #include <sys/debug.h>
30 #include <wait.h>
31 #include <string.h>
32 #include <fcntl.h>
33 #include <limits.h>
34 #include <errno.h>
35 #include <libgen.h>
36 #include <inttypes.h>
37
38 #include "posix_spawn_common.h"
39
40 /*
41 * This isn't const so we can refer to it in the argv arrays.
42 */
43 static char *spawn_pwd = "/usr/bin/pwd";
44 static char spawn_child[PATH_MAX];
45
46 /*
47 * This is an arbitrary fd that we believe will be okay to use in fchdir tests
48 * to overwrite.
49 */
50 #define SPAWN_FD 23
51
52 typedef struct spawn_dir_test {
53 const char *sdt_desc;
54 bool sdt_pass;
55 const char *sdt_pwd;
56 const char *sdt_dirs[16];
57 } spawn_dir_test_t;
58
59 static const spawn_dir_test_t spawn_dir_tests[] = {
60 {
61 .sdt_desc = "no chdir",
62 .sdt_pass = true,
63 .sdt_pwd = "/var/tmp"
64 }, {
65 .sdt_desc = "absolute path: /etc",
66 .sdt_pass = true,
67 .sdt_pwd = "/etc",
68 .sdt_dirs = { "/etc" }
69 }, {
70 .sdt_desc = "multiple absolute paths (1)",
71 .sdt_pass = true,
72 .sdt_pwd = "/dev/net",
73 .sdt_dirs = { "/etc", "/dev/net" }
74 }, {
75 .sdt_desc = "multiple absolute paths (2)",
76 .sdt_pass = true,
77 .sdt_pwd = "/var/svc",
78 .sdt_dirs = { "/proc/self", "/var/svc" }
79 }, {
80 .sdt_desc = "single relative path (1)",
81 .sdt_pass = true,
82 .sdt_pwd = "/var/tmp",
83 .sdt_dirs = { "." },
84 }, {
85 .sdt_desc = "single relative path (2)",
86 .sdt_pass = true,
87 .sdt_pwd = "/var",
88 .sdt_dirs = { ".." },
89 }, {
90 .sdt_desc = "multiple relative paths (1)",
91 .sdt_pass = true,
92 .sdt_pwd = "/usr/lib/dtrace",
93 .sdt_dirs = { "..", "..", "usr", "lib", "dtrace" },
94 }, {
95 .sdt_desc = "multiple relative paths (2)",
96 .sdt_pass = true,
97 .sdt_pwd = "/var/tmp",
98 .sdt_dirs = { "..", "tmp" },
99 }, {
100 .sdt_desc = "mixing absolute and relative paths (1)",
101 .sdt_pass = true,
102 .sdt_pwd = "/usr/lib/fm/fmd",
103 .sdt_dirs = { "..", "/usr/lib/fm", "fmd" },
104 }, {
105 .sdt_desc = "mixing absolute and relative paths (2)",
106 .sdt_pass = true,
107 .sdt_pwd = "/usr/bin",
108 .sdt_dirs = { "/usr/lib/64", "..", "..", "bin" },
109 }, {
110 .sdt_desc = "mixing absolute and relative paths (3)",
111 .sdt_pass = true,
112 .sdt_pwd = "/etc/svc/volatile",
113 .sdt_dirs = { "/usr/lib/64", "..", "..", "bin",
114 "/etc/svc/volatile" },
115 }, {
116 /*
117 * Note, these bad path tests will not be terribly meaningful
118 * for fchdir because the open will fail.
119 */
120 .sdt_desc = "bad path 1",
121 .sdt_pass = false,
122 .sdt_dirs = { "/#error//?*!@#$!asdf/please/don't/exist" }
123 }, {
124 .sdt_desc = "bad path 2",
125 .sdt_pass = false,
126 .sdt_dirs = { "/tmp", "\x001\x002\x003\x004\x003\x042" }
127 }
128 };
129
130 typedef struct spawn_flags_test {
131 const char *sft_desc;
132 int sft_ret;
133 short sft_flags;
134 } spawn_flags_test_t;
135
136 static const spawn_flags_test_t spawn_flags_tests[] = {
137 {
138 .sft_desc = "no flags",
139 .sft_ret = 0,
140 .sft_flags = 0
141 }, {
142 .sft_desc = "flag SETPGROUP",
143 .sft_ret = 0,
144 .sft_flags = POSIX_SPAWN_SETPGROUP
145 }, {
146 .sft_desc = "flag SETSID",
147 .sft_ret = 0,
148 .sft_flags = POSIX_SPAWN_SETSID
149 }, {
150 .sft_desc = "flags SETSID | SETPGROUP",
151 .sft_ret = EPERM,
152 .sft_flags = POSIX_SPAWN_SETSID | POSIX_SPAWN_SETPGROUP
153 }
154 };
155
156
157 static bool
posix_spawn_test_one_dir(const spawn_dir_test_t * test,int pipes[2],posix_spawn_file_actions_t * acts,const char * desc)158 posix_spawn_test_one_dir(const spawn_dir_test_t *test, int pipes[2],
159 posix_spawn_file_actions_t *acts, const char *desc)
160 {
161 int ret;
162 bool bret = false;
163 char *const argv[2] = { spawn_pwd, NULL };
164 char *const envp[1] = { NULL };
165 pid_t pid;
166 siginfo_t sig;
167 char pwd[PATH_MAX];
168 ssize_t pwd_len;
169
170 if ((ret = posix_spawn(&pid, spawn_pwd, acts, NULL, argv, envp)) != 0) {
171 if (!test->sdt_pass) {
172 (void) printf("TEST PASSED: %s (%s): posix_spawn "
173 "failed as expected\n", test->sdt_desc, desc);
174 bret = true;
175 goto out;
176 } else {
177 warnx("TEST FAILED: %s posix_spawn() failed with %s, "
178 "but expected success", test->sdt_desc,
179 strerrorname_np(ret));
180 goto out;
181 }
182 }
183
184 if (waitid(P_PID, pid, &sig, WEXITED) != 0) {
185 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: failed to wait on "
186 "pid %" _PRIdID ", but posix_spawn executed it",
187 test->sdt_desc, pid);
188 }
189
190 if (sig.si_code != CLD_EXITED) {
191 warnx("TEST FAILED: %s: child did not successfully exit: "
192 "foud si_code: %d", test->sdt_desc, sig.si_code);
193 goto out;
194 }
195
196 if (sig.si_status != 0) {
197 if (!test->sdt_pass) {
198 (void) printf("TEST PASSED: %s (%s): child process "
199 "failed", test->sdt_desc, desc);
200 bret = true;
201 goto out;
202 }
203
204 warnx("TEST FAILED: %s: child exited with status %d, expected "
205 "success", test->sdt_desc, sig.si_status);
206 goto out;
207 } else if (!test->sdt_pass) {
208 warnx("TEST FAILED: %s: child exited successfully, but "
209 "expected failure", test->sdt_desc);
210 goto out;
211 }
212
213 /*
214 * At this point we know that we have a pwd process that has
215 * successfully exited. We should be able to perform a non-blocking read
216 * from the pipe successfully and get its working directory. pwd(1)
217 * appends a new line. We remove it.
218 */
219 pwd[0] = 0;
220 pwd_len = read(pipes[0], pwd, sizeof (pwd));
221 if (pwd_len < 0) {
222 warn("TEST FAILED: %s: failed to read pwd from pipe",
223 test->sdt_desc);
224 goto out;
225 } else if (pwd_len == 0) {
226 warn("TEST FAILED: %s: got zero byte read from pipe?!",
227 test->sdt_desc);
228 goto out;
229 }
230 pwd[pwd_len - 1] = '\0';
231
232 if (strcmp(pwd, test->sdt_pwd) != 0) {
233 warnx("TEST FAILED: %s: found pwd '%s', expected '%s'",
234 test->sdt_desc, pwd, test->sdt_pwd);
235 goto out;
236 }
237
238 (void) printf("TEST PASSED: %s (%s)\n", test->sdt_desc, desc);
239
240 bret = true;
241 out:
242 return (bret);
243 }
244
245 static bool
posix_spawn_test_one_chdir(const spawn_dir_test_t * test)246 posix_spawn_test_one_chdir(const spawn_dir_test_t *test)
247 {
248 int ret, pipes[2];
249 bool bret = false;
250 posix_spawn_file_actions_t acts;
251
252 /*
253 * We set up a pipe to act as stdout so we can capture the output from
254 * pwd. While we could use /proc to try and do this, we prefer this
255 * mechanism.
256 */
257 posix_spawn_pipe_setup(&acts, pipes);
258
259 for (size_t i = 0; i < ARRAY_SIZE(test->sdt_dirs); i++) {
260 if (test->sdt_dirs[i] == NULL)
261 break;
262
263 ret = posix_spawn_file_actions_addchdir(&acts,
264 test->sdt_dirs[i]);
265 if (ret != 0) {
266 warnc(ret, "TEST FAILED: %s: adding path '%s' "
267 "(%zu) failed unexpectedly", test->sdt_desc,
268 test->sdt_dirs[i], i);
269 goto out;
270 }
271 }
272
273 bret = posix_spawn_test_one_dir(test, pipes, &acts, "chdir");
274 out:
275 VERIFY0(posix_spawn_file_actions_destroy(&acts));
276 VERIFY0(close(pipes[1]));
277 VERIFY0(close(pipes[0]));
278 return (bret);
279 }
280
281 static bool
posix_spawn_test_one_fchdir(const spawn_dir_test_t * test)282 posix_spawn_test_one_fchdir(const spawn_dir_test_t *test)
283 {
284 int ret, pipes[2];
285 bool bret = false;
286 posix_spawn_file_actions_t acts;
287
288 /*
289 * We set up a pipe to act as stdout so we can capture the output from
290 * pwd. While we could use /proc to try and do this, we prefer this
291 * mechanism.
292 */
293 posix_spawn_pipe_setup(&acts, pipes);
294
295 /*
296 * For the fchdir tests we go in a loop over these directories opening
297 * an fd, doing an fchdir to it, and then closing it.
298 */
299 for (size_t i = 0; i < ARRAY_SIZE(test->sdt_dirs); i++) {
300 if (test->sdt_dirs[i] == NULL)
301 break;
302
303 ret = posix_spawn_file_actions_addopen(&acts, SPAWN_FD,
304 test->sdt_dirs[i], O_RDONLY | O_DIRECTORY, 0);
305 if (ret != 0) {
306 warnc(ret, "TEST FAILED: %s: adding open action for "
307 "path '%s' (%zu) failed unexpectedly",
308 test->sdt_desc, test->sdt_dirs[i], i);
309 goto out;
310 }
311
312 ret = posix_spawn_file_actions_addfchdir(&acts, SPAWN_FD);
313 if (ret != 0) {
314 warnc(ret, "TEST FAILED: %s: adding fchdir action for "
315 "path '%s' (%zu) failed unexpectedly",
316 test->sdt_desc, test->sdt_dirs[i], i);
317 goto out;
318 }
319
320 ret = posix_spawn_file_actions_addclose(&acts, SPAWN_FD);
321 if (ret != 0) {
322 warnc(ret, "TEST FAILED: %s: adding close action for "
323 "path '%s' (%zu) failed unexpectedly",
324 test->sdt_desc, test->sdt_dirs[i], i);
325 goto out;
326 }
327 }
328
329 bret = posix_spawn_test_one_dir(test, pipes, &acts, "fchdir");
330 out:
331 VERIFY0(posix_spawn_file_actions_destroy(&acts));
332 VERIFY0(close(pipes[1]));
333 VERIFY0(close(pipes[0]));
334 return (bret);
335 }
336
337 /*
338 * Test a few different bad file actions.
339 */
340 static bool
posix_spawn_test_bad_actions(void)341 posix_spawn_test_bad_actions(void)
342 {
343 int ret;
344 bool bret = true;
345 posix_spawn_file_actions_t acts;
346
347 if ((ret = posix_spawn_file_actions_init(&acts)) != 0) {
348 errc(EXIT_FAILURE, ret, "INTERNAL TEST FAILURE: failed to "
349 "initialize posix_spawn file actions");
350 }
351
352 if ((ret = posix_spawn_file_actions_addfchdir(&acts, -23)) == 0) {
353 warnx("TEST FAILED: addfchdir() with bad fd: expected EBADF, "
354 "but returned successfully");
355 bret = false;
356 } else if (ret != EBADF) {
357 warnx("TEST FAILED: addfchdir with bad fd: failed with %s, "
358 "but expected EBADF", strerrorname_np(ret));
359 bret = false;
360 } else {
361 (void) printf("TEST PASSED: addfchdir() with bad fd: correctly "
362 "got EBADF\n");
363 }
364
365 if ((ret = posix_spawn_file_actions_addopen(&acts, -23, "/dev/null",
366 O_RDONLY, 0)) == 0) {
367 warnx("TEST FAILED: addopen() with bad fd: expected EBADF, "
368 "but returned successfully");
369 bret = false;
370 } else if (ret != EBADF) {
371 warnx("TEST FAILED: addopen with bad fd: failed with %s, "
372 "but expected EBADF", strerrorname_np(ret));
373 bret = false;
374 } else {
375 (void) printf("TEST PASSED: addopen() with bad fd: correctly "
376 "got EBADF\n");
377 }
378
379 if ((ret = posix_spawn_file_actions_addclose(&acts, -23)) == 0) {
380 warnx("TEST FAILED: addclose() with bad fd: expected EBADF, "
381 "but returned successfully");
382 bret = false;
383 } else if (ret != EBADF) {
384 warnx("TEST FAILED: addclose with bad fd: failed with %s, "
385 "but expected EBADF", strerrorname_np(ret));
386 bret = false;
387 } else {
388 (void) printf("TEST PASSED: addclose() with bad fd: correctly "
389 "got EBADF\n");
390 }
391
392 VERIFY0(posix_spawn_file_actions_destroy(&acts));
393 return (bret);
394 }
395
396 /*
397 * Verify that if we try to do an fchdir to an invalid fd that everything fails.
398 */
399 static bool
posix_spawn_test_bad_fchdir(void)400 posix_spawn_test_bad_fchdir(void)
401 {
402 int ret, pipes[2];
403 bool bret = false;
404 posix_spawn_file_actions_t acts;
405 spawn_dir_test_t test;
406
407 (void) memset(&test, 0, sizeof (test));
408 test.sdt_desc = "fchdir to closed fd";
409 test.sdt_pass = false;
410 test.sdt_pwd = "/nope";
411
412 /*
413 * We set up a pipe to act as stdout so we can capture the output from
414 * pwd. While we could use /proc to try and do this, we prefer this
415 * mechanism.
416 */
417 posix_spawn_pipe_setup(&acts, pipes);
418
419 ret = posix_spawn_file_actions_addclose(&acts, SPAWN_FD);
420 if (ret != 0) {
421 warnc(ret, "TEST FAILED: %s: adding close action failed "
422 "unexpectedly", test.sdt_desc);
423 goto out;
424 }
425
426 ret = posix_spawn_file_actions_addfchdir(&acts, SPAWN_FD);
427 if (ret != 0) {
428 warnc(ret, "TEST FAILED: %s: adding close action failed "
429 "unexpectedly", test.sdt_desc);
430 goto out;
431 }
432
433 bret = posix_spawn_test_one_dir(&test, pipes, &acts, "fchdir");
434 out:
435 VERIFY0(posix_spawn_file_actions_destroy(&acts));
436 VERIFY0(close(pipes[1]));
437 VERIFY0(close(pipes[0]));
438 return (bret);
439 }
440
441
442 static bool
posix_spawn_test_one_flags(const spawn_flags_test_t * test)443 posix_spawn_test_one_flags(const spawn_flags_test_t *test)
444 {
445 int ret, pipes[2];
446 bool bret = true;
447 char *const argv[3] = { spawn_child, "sidpgid", NULL };
448 char *const envp[1] = { NULL };
449 spawn_sidpgid_result_t res;
450 posix_spawn_file_actions_t acts;
451 posix_spawnattr_t attr;
452 short flags;
453 pid_t pid, exp_sid, exp_pgid;
454 siginfo_t sig;
455 ssize_t n;
456 const char *sid_desc, *pgid_desc;
457
458 posix_spawn_pipe_setup(&acts, pipes);
459
460 if ((ret = posix_spawnattr_init(&attr)) != 0) {
461 errc(EXIT_FAILURE, ret, "INTERNAL TEST FAILURE: failed to "
462 "initialize posix_spawn attributes");
463 }
464
465 VERIFY0(posix_spawnattr_getflags(&attr, &flags));
466 if (flags != 0) {
467 warnx("TEST FAILED: %s: initial flags are not zero, found 0x%x",
468 test->sft_desc, flags);
469 bret = false;
470 }
471 VERIFY0(posix_spawnattr_setflags(&attr, test->sft_flags));
472 VERIFY0(posix_spawnattr_getflags(&attr, &flags));
473 if (flags != test->sft_flags) {
474 warnx("TEST FAILED: %s: flags are don't match what we set: "
475 "found 0x%x, expected 0x%x", test->sft_desc, flags,
476 test->sft_flags);
477 bret = false;
478 }
479
480 ret = posix_spawn(&pid, spawn_child, &acts, &attr, argv, envp);
481 if (ret != test->sft_ret) {
482 if (test->sft_ret == 0) {
483 warnx("TEST FAILED: %s posix_spawn() failed with %s, "
484 "but expected success", test->sft_desc,
485 strerrorname_np(ret));
486 } else {
487 warnx("TEST FAILED: %s posix_spawn() failed with %s, "
488 "but expected %s", test->sft_desc,
489 strerrorname_np(ret),
490 strerrorname_np(test->sft_ret));
491 }
492 bret = false;
493 goto out;
494 }
495
496 if (test->sft_ret != 0) {
497 (void) printf("TEST PASSED: %s: posix_spawn() failed correctly "
498 "with %s\n", test->sft_desc, strerrorname_np(ret));
499 goto out;
500 }
501
502 if (waitid(P_PID, pid, &sig, WEXITED) != 0) {
503 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: failed to wait on "
504 "pid %" _PRIdID ", but posix_spawn executed it",
505 test->sft_desc, pid);
506 }
507
508 if (sig.si_code != CLD_EXITED) {
509 warnx("TEST FAILED: %s: child did not successfully exit: "
510 "foud si_code: %d", test->sft_desc, sig.si_code);
511 bret = false;
512 goto out;
513 }
514
515 if (sig.si_status != 0) {
516 warnx("TEST FAILED: %s: child exited with status %d, expected "
517 "success", test->sft_desc, sig.si_status);
518 bret = false;
519 goto out;
520 }
521
522 /*
523 * The child writes the results of getsid(2) and getpgid(2) as binary
524 * data to our pipe. We should be able to read all of this in one swoop.
525 */
526 n = read(pipes[0], &res, sizeof (res));
527 if (n != sizeof (res)) {
528 warnx("TEST FAILED: %s: short read from pipe (%zd)",
529 test->sft_desc, n);
530 bret = false;
531 goto out;
532 }
533
534 /*
535 * Now we need to check the various process group and session IDs. We
536 * expect the following values:
537 *
538 * If the SETSID flag was set then the session ID should match the
539 * child's pid. Otherwise it should match our value.
540 *
541 * If the SETSID or SETPGROUP flag was set then the process group ID
542 * should match the child's pid. Otherwise it should match our value.
543 */
544 if ((test->sft_flags & POSIX_SPAWN_SETSID) != 0) {
545 exp_sid = pid;
546 sid_desc = "child's ID";
547 } else {
548 exp_sid = getsid(0);
549 sid_desc = "test's SID";
550 }
551
552 if ((test->sft_flags & (POSIX_SPAWN_SETSID |
553 POSIX_SPAWN_SETPGROUP)) != 0) {
554 exp_pgid = pid;
555 pgid_desc = "child's ID";
556 } else {
557 exp_pgid = getpgid(0);
558 pgid_desc = "test's PGID";
559 }
560
561 if (res.sspr_sid != exp_sid) {
562 warnx("TEST FAILED: %s: session ID mismatch: expected 0x%"
563 _PRIxID " (%s), found 0x%" _PRIxID, test->sft_desc, exp_sid,
564 sid_desc, res.sspr_sid);
565 bret = false;
566 }
567
568 if (res.sspr_pgid != exp_pgid) {
569 warnx("TEST FAILED: %s: process group ID mismatch: expected "
570 "0x%" _PRIxID " (%s), found 0x%" _PRIxID, test->sft_desc,
571 exp_pgid, pgid_desc, res.sspr_pgid);
572 bret = false;
573 }
574
575 if (bret) {
576 (void) printf("TEST PASSED: %s\n", test->sft_desc);
577 }
578
579 out:
580 VERIFY0(posix_spawnattr_destroy(&attr));
581 VERIFY0(posix_spawn_file_actions_destroy(&acts));
582 VERIFY0(close(pipes[1]));
583 VERIFY0(close(pipes[0]));
584 return (bret);
585 }
586
587
588 int
main(void)589 main(void)
590 {
591 const char *helpers[] = { POSIX_SPAWN_CHILD_HELPERS };
592 int ret = EXIT_SUCCESS;
593
594 /*
595 * Because this test wants to rely on a known starting directory, we're
596 * going to chdir into /var/tmp at the start of this.
597 */
598 if (chdir("/var/tmp") != 0) {
599 err(EXIT_FAILURE, "INTERNAL TEST ERROR: failed to cd into "
600 "/var/tmp");
601 }
602
603 for (size_t i = 0; i < ARRAY_SIZE(spawn_dir_tests); i++) {
604 if (!posix_spawn_test_one_chdir(&spawn_dir_tests[i]))
605 ret = EXIT_FAILURE;
606
607 if (!posix_spawn_test_one_fchdir(&spawn_dir_tests[i]))
608 ret = EXIT_FAILURE;
609 }
610
611 if (!posix_spawn_test_bad_actions()) {
612 ret = EXIT_FAILURE;
613 }
614
615 if (!posix_spawn_test_bad_fchdir()) {
616 ret = EXIT_FAILURE;
617 }
618
619 for (size_t h = 0; h < ARRAY_SIZE(helpers); h++) {
620 posix_spawn_find_helper(spawn_child, sizeof (spawn_child),
621 helpers[h]);
622 (void) printf("--- child helper: %s ---\n", helpers[h]);
623
624 for (size_t i = 0; i < ARRAY_SIZE(spawn_flags_tests); i++) {
625 if (!posix_spawn_test_one_flags(&spawn_flags_tests[i]))
626 ret = EXIT_FAILURE;
627 }
628 }
629
630 if (ret == EXIT_SUCCESS) {
631 (void) printf("All tests passed successfully!\n");
632 }
633
634 return (ret);
635 }
636