xref: /illumos-gate/usr/src/test/libc-tests/tests/posix_spawn/posix_spawn_priv.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  * Privileged posix_spawn attribute tests. The scheduler tests require
18  * proc_priocntl and the RESETIDS test requires proc_setid.
19  */
20 
21 #include <err.h>
22 #include <errno.h>
23 #include <fcntl.h>
24 #include <libgen.h>
25 #include <limits.h>
26 #include <priv.h>
27 #include <pwd.h>
28 #include <sched.h>
29 #include <spawn.h>
30 #include <stdbool.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <unistd.h>
35 #include <wait.h>
36 #include <sys/debug.h>
37 #include <sys/stat.h>
38 #include <sys/sysmacros.h>
39 
40 #include "posix_spawn_common.h"
41 
42 typedef struct spawn_priv_test {
43 	const char	*spt_name;
44 	bool		(*spt_func)(struct spawn_priv_test *);
45 	const char	*spt_priv;
46 } spawn_priv_test_t;
47 
48 static char posix_spawn_child_path[PATH_MAX];
49 
50 /*
51  * SETSCHEDULER: set the child's scheduling policy to a real-time class with a
52  * specific priority. Verify the child sees the correct policy and priority.
53  */
54 static bool
setscheduler_test(spawn_priv_test_t * test)55 setscheduler_test(spawn_priv_test_t *test)
56 {
57 	posix_spawn_file_actions_t acts;
58 	posix_spawnattr_t attr;
59 	struct sched_param param;
60 	spawn_sched_result_t res;
61 	ssize_t n;
62 	bool bret = true;
63 	int orig_policy, new_policy, prio_min;
64 	char *argv[] = { posix_spawn_child_path, "sched", NULL };
65 	const char *desc = test->spt_name;
66 	int pipes[2];
67 
68 	/*
69 	 * Pick a real-time class that is not the policy for the current
70 	 * process.
71 	 */
72 	orig_policy = sched_getscheduler(0);
73 	new_policy = (orig_policy != SCHED_FIFO) ? SCHED_FIFO : SCHED_RR;
74 	prio_min = sched_get_priority_min(new_policy);
75 	if (prio_min == -1) {
76 		warn("TEST FAILED: %s: sched_get_priority_min(%d)",
77 		    desc, new_policy);
78 		return (false);
79 	}
80 
81 	posix_spawn_pipe_setup(&acts, pipes);
82 
83 	VERIFY0(posix_spawnattr_init(&attr));
84 	VERIFY0(posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDULER));
85 	VERIFY0(posix_spawnattr_setschedpolicy(&attr, new_policy));
86 	param.sched_priority = prio_min;
87 	VERIFY0(posix_spawnattr_setschedparam(&attr, &param));
88 
89 	if (!posix_spawn_run_child(desc, posix_spawn_child_path,
90 	    &acts, &attr, argv)) {
91 		bret = false;
92 		goto out;
93 	}
94 
95 	n = read(pipes[0], &res, sizeof (res));
96 	if (n != sizeof (res)) {
97 		warnx("TEST FAILED: %s: short read from pipe (%zd)", desc, n);
98 		bret = false;
99 		goto out;
100 	}
101 
102 	if (res.ssr_policy != new_policy) {
103 		warnx("TEST FAILED: %s: "
104 		    "child policy is %d, expected %d",
105 		    desc, res.ssr_policy, new_policy);
106 		bret = false;
107 	}
108 
109 	if (res.ssr_priority != prio_min) {
110 		warnx("TEST FAILED: %s: child priority is %d, expected %d",
111 		    desc, res.ssr_priority, prio_min);
112 		bret = false;
113 	}
114 
115 out:
116 	VERIFY0(posix_spawnattr_destroy(&attr));
117 	VERIFY0(posix_spawn_file_actions_destroy(&acts));
118 	VERIFY0(close(pipes[1]));
119 	VERIFY0(close(pipes[0]));
120 
121 	return (bret);
122 }
123 
124 /*
125  * SETSCHEDPARAM: set only the priority (not the policy). The child should
126  * inherit the parent's scheduling policy but with the specified priority.
127  * We first switch to a real-time scheduler so the child's policy can be
128  * distinguished from the system default.
129  */
130 static bool
setschedparam_test(spawn_priv_test_t * test)131 setschedparam_test(spawn_priv_test_t *test)
132 {
133 	const char *desc = test->spt_name;
134 	int pipes[2];
135 	posix_spawn_file_actions_t acts;
136 	posix_spawnattr_t attr;
137 	struct sched_param param, orig_param;
138 	spawn_sched_result_t res;
139 	ssize_t n;
140 	bool bret = true;
141 	int orig_policy, new_policy, parent_policy, prio_min;
142 	char *argv[] = { posix_spawn_child_path, "sched", NULL };
143 
144 	/*
145 	 * Save the original scheduling policy so we can restore it later.
146 	 * Don't assume the default is SCHED_OTHER; it may be FSS in a zone.
147 	 */
148 	orig_policy = sched_getscheduler(0);
149 	if (orig_policy == -1) {
150 		warn("TEST FAILED: %s: sched_getscheduler", desc);
151 		return (false);
152 	}
153 	if (sched_getparam(0, &orig_param) != 0) {
154 		warn("TEST FAILED: %s: sched_getparam", desc);
155 		return (false);
156 	}
157 
158 	/*
159 	 * Set ourselves to a real-time class so there is a meaningful priority
160 	 * range to work with. We pick one that is not the policy for the
161 	 * current process in case that is the system default - the child's
162 	 * inherited policy is then distinguishable from any system default.
163 	 */
164 	new_policy = (orig_policy != SCHED_FIFO) ? SCHED_FIFO : SCHED_RR;
165 
166 	prio_min = sched_get_priority_min(new_policy);
167 	if (prio_min == -1) {
168 		warn("TEST FAILED: %s: sched_get_priority_min(%d)",
169 		    desc, new_policy);
170 		return (false);
171 	}
172 
173 	param.sched_priority = prio_min;
174 	if (sched_setscheduler(0, new_policy, &param) == -1) {
175 		warn("TEST FAILED: %s: sched_setscheduler(%d) failed",
176 		    desc, new_policy);
177 		return (false);
178 	}
179 
180 	parent_policy = sched_getscheduler(0);
181 
182 	posix_spawn_pipe_setup(&acts, pipes);
183 
184 	VERIFY0(posix_spawnattr_init(&attr));
185 	VERIFY0(posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDPARAM));
186 	param.sched_priority = prio_min + 1;
187 	VERIFY0(posix_spawnattr_setschedparam(&attr, &param));
188 
189 	if (!posix_spawn_run_child(desc, posix_spawn_child_path,
190 	    &acts, &attr, argv)) {
191 		bret = false;
192 		goto out;
193 	}
194 
195 	n = read(pipes[0], &res, sizeof (res));
196 	if (n != sizeof (res)) {
197 		warnx("TEST FAILED: %s: short read from pipe (%zd)", desc, n);
198 		bret = false;
199 		goto out;
200 	}
201 
202 	if (res.ssr_policy != parent_policy) {
203 		warnx("TEST FAILED: %s: "
204 		    "child policy is %d, expected parent's policy (%d)",
205 		    desc, res.ssr_policy, parent_policy);
206 		bret = false;
207 	}
208 
209 	if (res.ssr_priority != prio_min + 1) {
210 		warnx("TEST FAILED: %s: child priority is %d, expected %d",
211 		    desc, res.ssr_priority, prio_min + 1);
212 		bret = false;
213 	}
214 
215 out:
216 	/*
217 	 * Restore the original scheduling policy.
218 	 */
219 	(void) sched_setscheduler(0, orig_policy, &orig_param);
220 
221 	VERIFY0(posix_spawnattr_destroy(&attr));
222 	VERIFY0(posix_spawn_file_actions_destroy(&acts));
223 	VERIFY0(close(pipes[1]));
224 	VERIFY0(close(pipes[0]));
225 
226 	return (bret);
227 }
228 
229 /*
230  * Look up the 'nobody' user and verify that our current uid is not already
231  * nobody, since RESETIDS tests depend on the distinction.
232  */
233 static uid_t
get_nobody_uid(const char * desc)234 get_nobody_uid(const char *desc)
235 {
236 	struct passwd *pwd;
237 
238 	errno = 0;
239 	if ((pwd = getpwnam("nobody")) == NULL) {
240 		err(EXIT_FAILURE,
241 		    "INTERNAL TEST FAILURE: could not find 'nobody' user");
242 	}
243 
244 	if (getuid() == pwd->pw_uid) {
245 		errx(EXIT_FAILURE,
246 		    "INTERNAL TEST FAILURE: %s: already running as nobody",
247 		    desc);
248 	}
249 
250 	return (pwd->pw_uid);
251 }
252 
253 /*
254  * RESETIDS: set euid to nobody, then spawn with RESETIDS. The child should see
255  * euid == uid. Requires proc_setid.
256  */
257 static bool
resetids_priv_test(spawn_priv_test_t * test)258 resetids_priv_test(spawn_priv_test_t *test)
259 {
260 	const char *desc = test->spt_name;
261 	int pipes[2];
262 	posix_spawn_file_actions_t acts;
263 	posix_spawnattr_t attr;
264 	spawn_id_result_t res;
265 	ssize_t n;
266 	bool bret = true;
267 	uid_t orig_euid = geteuid();
268 	uid_t nobody_uid;
269 	char *argv[] = { posix_spawn_child_path, "ids", NULL };
270 
271 	nobody_uid = get_nobody_uid(desc);
272 
273 	/*
274 	 * Set effective uid to that of 'nobody'. RESETIDS should restore euid
275 	 * to match uid.
276 	 */
277 	if (seteuid(nobody_uid) != 0) {
278 		warn("TEST FAILED: %s: seteuid(nobody) failed", desc);
279 		return (false);
280 	}
281 
282 	posix_spawn_pipe_setup(&acts, pipes);
283 
284 	VERIFY0(posix_spawnattr_init(&attr));
285 	VERIFY0(posix_spawnattr_setflags(&attr, POSIX_SPAWN_RESETIDS));
286 
287 	if (!posix_spawn_run_child(desc, posix_spawn_child_path,
288 	    &acts, &attr, argv)) {
289 		bret = false;
290 		goto out;
291 	}
292 
293 	n = read(pipes[0], &res, sizeof (res));
294 	if (n != sizeof (res)) {
295 		warnx("TEST FAILED: %s: short read from pipe (%zd)", desc, n);
296 		bret = false;
297 		goto out;
298 	}
299 
300 	if (res.sir_uid != res.sir_euid) {
301 		warnx("TEST FAILED: %s: uid %d != euid %d after RESETIDS",
302 		    desc, res.sir_uid, res.sir_euid);
303 		bret = false;
304 	}
305 
306 	if (res.sir_uid != getuid()) {
307 		warnx("TEST FAILED: %s: "
308 		    "child uid is %d, expected parent's uid %d",
309 		    desc, res.sir_uid, getuid());
310 		bret = false;
311 	}
312 
313 out:
314 	(void) seteuid(orig_euid);
315 
316 	VERIFY0(posix_spawnattr_destroy(&attr));
317 	VERIFY0(posix_spawn_file_actions_destroy(&acts));
318 	VERIFY0(close(pipes[1]));
319 	VERIFY0(close(pipes[0]));
320 
321 	return (bret);
322 }
323 
324 /*
325  * RESETIDS with a setuid binary. Create a temporary copy of the child helper
326  * owned by 'nobody' with the setuid bit set. The setuid bit is applied by
327  * exec(2) after RESETIDS processing, so the child's euid should be nobody
328  * in both cases. RESETIDS should not prevent legitimate setuid from working.
329  * We verify that uid remains the parent's real uid regardless.
330  */
331 static bool
resetids_suid_test(spawn_priv_test_t * test)332 resetids_suid_test(spawn_priv_test_t *test)
333 {
334 	const char *desc = test->spt_name;
335 	int pipes[2];
336 	posix_spawn_file_actions_t acts;
337 	posix_spawnattr_t attr;
338 	spawn_id_result_t res;
339 	ssize_t n;
340 	bool bret = true;
341 	uid_t nobody_uid;
342 	uid_t parent_uid = getuid();
343 	char *argv[] = { posix_spawn_child_path, "ids", NULL };
344 	char suid_path[PATH_MAX];
345 	char cmdbuf[PATH_MAX + 64];
346 
347 	nobody_uid = get_nobody_uid(desc);
348 
349 	/*
350 	 * Create a setuid copy of the child helper owned by nobody.
351 	 */
352 	(void) snprintf(suid_path, sizeof (suid_path),
353 	    "/tmp/posix_spawn_suid_child.%d", (int)getpid());
354 	(void) snprintf(cmdbuf, sizeof (cmdbuf),
355 	    "cp %s %s", posix_spawn_child_path, suid_path);
356 	if (system(cmdbuf) != 0) {
357 		warnx("TEST FAILED: %s: failed to copy child helper", desc);
358 		return (false);
359 	}
360 
361 	if (chown(suid_path, nobody_uid, (gid_t)-1) != 0) {
362 		warn("TEST FAILED: %s: chown failed", desc);
363 		(void) unlink(suid_path);
364 		return (false);
365 	}
366 
367 	if (chmod(suid_path, S_ISUID | 0555) != 0) {
368 		warn("TEST FAILED: %s: chmod failed", desc);
369 		(void) unlink(suid_path);
370 		return (false);
371 	}
372 
373 	/*
374 	 * First verify the control case: without RESETIDS, the setuid bit
375 	 * causes euid to become nobody.
376 	 */
377 	posix_spawn_pipe_setup(&acts, pipes);
378 
379 	argv[0] = suid_path;
380 
381 	if (!posix_spawn_run_child(desc, suid_path, &acts, NULL, argv)) {
382 		bret = false;
383 		VERIFY0(posix_spawn_file_actions_destroy(&acts));
384 		VERIFY0(close(pipes[1]));
385 		VERIFY0(close(pipes[0]));
386 		goto cleanup;
387 	}
388 
389 	n = read(pipes[0], &res, sizeof (res));
390 	if (n != sizeof (res)) {
391 		warnx("TEST FAILED: %s: control: short read from pipe (%zd)",
392 		    desc, n);
393 		bret = false;
394 	} else if (res.sir_euid != nobody_uid) {
395 		warnx("TEST FAILED: %s: control: euid is %d, "
396 		    "expected nobody (%d)",
397 		    desc, res.sir_euid, nobody_uid);
398 		bret = false;
399 	} else if (res.sir_uid != parent_uid) {
400 		warnx("TEST FAILED: %s: control: uid is %d, "
401 		    "expected parent uid (%d)",
402 		    desc, res.sir_uid, parent_uid);
403 		bret = false;
404 	}
405 
406 	VERIFY0(posix_spawn_file_actions_destroy(&acts));
407 	VERIFY0(close(pipes[1]));
408 	VERIFY0(close(pipes[0]));
409 
410 	/*
411 	 * With RESETIDS: the setuid bit still takes effect (euid = nobody)
412 	 * because exec applies suid after RESETIDS. The real uid should
413 	 * remain the parent's real uid.
414 	 */
415 	posix_spawn_pipe_setup(&acts, pipes);
416 
417 	VERIFY0(posix_spawnattr_init(&attr));
418 	VERIFY0(posix_spawnattr_setflags(&attr, POSIX_SPAWN_RESETIDS));
419 
420 	if (!posix_spawn_run_child(desc, suid_path, &acts, &attr, argv)) {
421 		bret = false;
422 		goto out;
423 	}
424 
425 	n = read(pipes[0], &res, sizeof (res));
426 	if (n != sizeof (res)) {
427 		warnx("TEST FAILED: %s: short read from pipe (%zd)", desc, n);
428 		bret = false;
429 		goto out;
430 	}
431 
432 	if (res.sir_euid != nobody_uid) {
433 		warnx("TEST FAILED: %s: "
434 		    "euid is %d, expected nobody (%d) (suid > RESETIDS)",
435 		    desc, res.sir_euid, nobody_uid);
436 		bret = false;
437 	}
438 
439 	if (res.sir_uid != parent_uid) {
440 		warnx("TEST FAILED: %s: "
441 		    "uid is %d, expected parent's uid %d",
442 		    desc, res.sir_uid, parent_uid);
443 		bret = false;
444 	}
445 
446 out:
447 	VERIFY0(posix_spawnattr_destroy(&attr));
448 	VERIFY0(posix_spawn_file_actions_destroy(&acts));
449 	VERIFY0(close(pipes[1]));
450 	VERIFY0(close(pipes[0]));
451 
452 cleanup:
453 	(void) unlink(suid_path);
454 
455 	return (bret);
456 }
457 
458 static spawn_priv_test_t tests[] = {
459 	{ .spt_name = "SETSCHEDULER: RT SCHED with min priority",
460 	    .spt_func = setscheduler_test, .spt_priv = PRIV_PROC_PRIOCNTL },
461 	{ .spt_name = "SETSCHEDPARAM: priority change under RT SCHED",
462 	    .spt_func = setschedparam_test, .spt_priv = PRIV_PROC_PRIOCNTL },
463 	{ .spt_name = "RESETIDS: euid reset after seteuid(nobody)",
464 	    .spt_func = resetids_priv_test, .spt_priv = PRIV_PROC_SETID },
465 	{ .spt_name = "RESETIDS: setuid binary retains suid euid",
466 	    .spt_func = resetids_suid_test, .spt_priv = PRIV_PROC_SETID },
467 };
468 
469 int
main(void)470 main(void)
471 {
472 	const char *helpers[] = { POSIX_SPAWN_CHILD_HELPERS };
473 	int ret = EXIT_SUCCESS;
474 
475 	for (size_t h = 0; h < ARRAY_SIZE(helpers); h++) {
476 		posix_spawn_find_helper(posix_spawn_child_path,
477 		    sizeof (posix_spawn_child_path), helpers[h]);
478 		(void) printf("--- child helper: %s ---\n", helpers[h]);
479 
480 		for (size_t i = 0; i < ARRAY_SIZE(tests); i++) {
481 			if (!priv_ineffect(tests[i].spt_priv)) {
482 				(void) printf("TEST FAILED: %s: "
483 				    "requires %s privilege\n",
484 				    tests[i].spt_name, tests[i].spt_priv);
485 				ret = EXIT_FAILURE;
486 			} else if (tests[i].spt_func(&tests[i])) {
487 				(void) printf("TEST PASSED: %s\n",
488 				    tests[i].spt_name);
489 			} else {
490 				ret = EXIT_FAILURE;
491 			}
492 		}
493 	}
494 
495 	if (ret == EXIT_SUCCESS)
496 		(void) printf("All tests passed successfully!\n");
497 
498 	return (ret);
499 }
500