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 posix_spawnp(3C) PATH resolution and ENOEXEC shell fallback.
18 * Each test forks to isolate environment changes from the parent, then uses
19 * posix_spawnp in the child.
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 <sys/fork.h>
31 #include <wait.h>
32 #include <string.h>
33 #include <fcntl.h>
34 #include <limits.h>
35 #include <errno.h>
36 #include <libgen.h>
37
38 #include "posix_spawn_common.h"
39
40 extern char **environ;
41
42 /*
43 * Directory containing the no-shebang script, used as PATH for spawnp tests.
44 */
45 static char posix_spawn_noshebang_dir[PATH_MAX];
46
47 typedef struct spawn_path_test spawn_path_test_t;
48
49 struct spawn_path_test {
50 const char *spt_desc;
51 bool (*spt_func)(spawn_path_test_t *);
52 const char *spt_file; /* file arg to posix_spawnp */
53 const char *spt_path; /* PATH to set, or NULL to unset */
54 bool spt_pass; /* expect success? */
55 int spt_err; /* expected errno if !spt_pass */
56 };
57
58 /*
59 * Run a single posix_spawnp PATH resolution test inside a forked child
60 * process to isolate environment changes.
61 */
62 static bool
path_resolve_test(spawn_path_test_t * test)63 path_resolve_test(spawn_path_test_t *test)
64 {
65 pid_t fork_pid;
66 siginfo_t sig;
67
68 fork_pid = forkx(FORK_NOSIGCHLD | FORK_WAITPID);
69 if (fork_pid == -1) {
70 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: fork",
71 test->spt_desc);
72 }
73
74 if (fork_pid == 0) {
75 char *argv[] = { (char *)test->spt_file, NULL };
76 siginfo_t child_sig;
77 pid_t pid;
78 int ret;
79
80 /* Child: set up PATH and attempt posix_spawnp */
81 if (test->spt_path != NULL) {
82 if (setenv("PATH", test->spt_path, 1) != 0)
83 _exit(99);
84 } else {
85 if (unsetenv("PATH") != 0)
86 _exit(99);
87 }
88
89 ret = posix_spawnp(&pid, test->spt_file, NULL, NULL,
90 argv, environ);
91
92 if (ret != 0) {
93 if (!test->spt_pass && ret == test->spt_err)
94 _exit(0);
95 /*
96 * Encode the errno in the exit status for
97 * diagnostics. Use values > 100 to distinguish
98 * from normal exits.
99 */
100 _exit(100 + ret);
101 }
102
103 /* posix_spawn succeeded. Wait for the spawned process */
104 if (waitid(P_PID, pid, &child_sig, WEXITED) != 0)
105 _exit(98);
106 if (child_sig.si_code != CLD_EXITED || child_sig.si_status != 0)
107 _exit(97);
108
109 /* Expected failure but got success. */
110 if (!test->spt_pass)
111 _exit(96);
112
113 _exit(0);
114 }
115
116 /* Parent: wait for the child */
117 if (waitid(P_PID, fork_pid, &sig, WEXITED) != 0) {
118 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: waitid",
119 test->spt_desc);
120 }
121
122 if (sig.si_code != CLD_EXITED) {
123 warnx("TEST FAILED: %s: "
124 "fork child did not exit normally: si_code: %d",
125 test->spt_desc, sig.si_code);
126 return (false);
127 }
128
129 if (sig.si_status != 0) {
130 if (sig.si_status == 96) {
131 warnx("TEST FAILED: %s: "
132 "expected failure but posix_spawnp succeeded",
133 test->spt_desc);
134 } else if (sig.si_status > 100) {
135 warnx("TEST FAILED: %s: "
136 "posix_spawnp failed with %s, expected %s",
137 test->spt_desc,
138 strerrorname_np(sig.si_status - 100),
139 test->spt_pass ? "success" :
140 strerrorname_np(test->spt_err));
141 } else {
142 warnx("TEST FAILED: %s: "
143 "fork child exited with status %d",
144 test->spt_desc, sig.si_status);
145 }
146 return (false);
147 }
148
149 return (true);
150 }
151
152 /*
153 * Test ENOEXEC shell fallback: posix_spawnp a script without a #! line.
154 * The implementation should fall back to executing it via /bin/sh.
155 */
156 static bool
enoexec_fallback_test(spawn_path_test_t * test)157 enoexec_fallback_test(spawn_path_test_t *test)
158 {
159 const char *desc = test->spt_desc;
160 pid_t fork_pid;
161 siginfo_t sig;
162
163 fork_pid = forkx(FORK_NOSIGCHLD | FORK_WAITPID);
164 if (fork_pid == -1)
165 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: fork", desc);
166
167 if (fork_pid == 0) {
168 char *argv[] = { "posix_spawn_noshebang", NULL };
169 siginfo_t child_sig;
170 pid_t pid;
171 int ret;
172
173 if (setenv("PATH", posix_spawn_noshebang_dir, 1) != 0)
174 _exit(99);
175
176 ret = posix_spawnp(&pid, "posix_spawn_noshebang", NULL, NULL,
177 argv, environ);
178 if (ret != 0)
179 _exit(100 + ret);
180
181 if (waitid(P_PID, pid, &child_sig, WEXITED) != 0)
182 _exit(98);
183 if (child_sig.si_code != CLD_EXITED ||
184 child_sig.si_status != 0)
185 _exit(97);
186
187 _exit(0);
188 }
189
190 if (waitid(P_PID, fork_pid, &sig, WEXITED) != 0)
191 err(EXIT_FAILURE, "INTERNAL TEST ERROR: %s: waitid", desc);
192
193 if (sig.si_code != CLD_EXITED) {
194 warnx("TEST FAILED: %s: "
195 "fork child did not exit normally: si_code: %d",
196 desc, sig.si_code);
197 return (false);
198 }
199
200 if (sig.si_status != 0) {
201 if (sig.si_status > 100) {
202 warnx("TEST FAILED: %s: posix_spawnp failed with %s",
203 desc, strerrorname_np(sig.si_status - 100));
204 } else {
205 warnx("TEST FAILED: %s: "
206 "fork child exited with status %d",
207 desc, sig.si_status);
208 }
209 return (false);
210 }
211
212 return (true);
213 }
214
215 static spawn_path_test_t tests[] = {
216 { .spt_desc = "find true via PATH=/usr/bin",
217 .spt_func = path_resolve_test,
218 .spt_file = "true", .spt_path = "/usr/bin",
219 .spt_pass = true },
220 { .spt_desc = "find true via second PATH component",
221 .spt_func = path_resolve_test,
222 .spt_file = "true", .spt_path = "/usr/lib:/usr/bin",
223 .spt_pass = true },
224 { .spt_desc = "fail with PATH=/nonexistent",
225 .spt_func = path_resolve_test,
226 .spt_file = "true", .spt_path = "/nonexistent",
227 .spt_pass = false, .spt_err = ENOENT },
228 { .spt_desc = "absolute path ignores PATH",
229 .spt_func = path_resolve_test,
230 .spt_file = "/usr/bin/true", .spt_path = "/nonexistent",
231 .spt_pass = true },
232 { .spt_desc = "empty file returns EACCES",
233 .spt_func = path_resolve_test,
234 .spt_file = "", .spt_path = "/usr/bin",
235 .spt_pass = false, .spt_err = EACCES },
236 { .spt_desc = "NULL PATH uses default path",
237 .spt_func = path_resolve_test,
238 .spt_file = "true",
239 .spt_pass = true },
240 { .spt_desc = "ENOEXEC: shell fallback for no-shebang script",
241 .spt_func = enoexec_fallback_test },
242 };
243
244 int
main(void)245 main(void)
246 {
247 int ret = EXIT_SUCCESS;
248 char path[PATH_MAX];
249
250 posix_spawn_find_helper(path, sizeof (path), "posix_spawn_noshebang");
251 (void) strlcpy(posix_spawn_noshebang_dir, dirname(path),
252 sizeof (posix_spawn_noshebang_dir));
253
254 for (size_t i = 0; i < ARRAY_SIZE(tests); i++) {
255 if (tests[i].spt_func(&tests[i]))
256 (void) printf("TEST PASSED: %s\n", tests[i].spt_desc);
257 else
258 ret = EXIT_FAILURE;
259 }
260
261 if (ret == EXIT_SUCCESS)
262 (void) printf("All tests passed successfully!\n");
263
264 return (ret);
265 }
266