xref: /illumos-gate/usr/src/test/libc-tests/tests/posix_spawn/posix_spawn_path.c (revision 93d6c51de00648a982a50fbecc433b5482953fc7)
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