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, ¶m));
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, ¶m) == -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, ¶m));
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