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 the raw spawn(2) system call interface. These concentrate 18 * mainly on checking that the kernel fully validates the marshalled 19 * spawn_param_t/spawn_args_t structures that libc constructs. 20 * We also test that a spawned child is robust against being killed in 21 * the window between creation and exec. 22 * 23 * Functional coverage of posix_spawn(3C) itself (attributes, file actions, 24 * PATH handling, etc.) lives in libc-tests/tests/posix_spawn. 25 */ 26 27 #include <errno.h> 28 #include <signal.h> 29 #include <spawn.h> 30 #include <stdio.h> 31 #include <stdlib.h> 32 #include <string.h> 33 #include <unistd.h> 34 #include <sys/syscall.h> 35 #include <sys/spawn_impl.h> 36 #include <sys/types.h> 37 #include <sys/wait.h> 38 39 static uint_t failures = 0; 40 41 #define TFAIL(name, fmt, ...) do { \ 42 (void) fprintf(stderr, "TEST FAILED: %s: " fmt "\n", \ 43 (name), ##__VA_ARGS__); \ 44 failures++; \ 45 } while (0) 46 47 #define TPASS(name) (void) printf("TEST PASSED: %s\n", (name)) 48 49 /* 50 * The spawn_args_t and spawn_param_t structures end in a variable-length 51 * data[] region. These tests build small instances on the stack and reserve a 52 * fixed amount of room for that region. 53 */ 54 #define SA_DATA 16 55 #define SP_DATA 64 56 57 static pid_t 58 raw_spawn(const char *path, const void *sp, uint32_t spsize, const void *sa, 59 uint32_t sasize) 60 { 61 return (syscall(SYS_spawn, path, sp, spsize, sa, sasize)); 62 } 63 64 /* 65 * Build a minimal valid spawn_args_t in the provided buffer: 66 * argv = { "x" }, empty environment. 67 */ 68 static uint32_t 69 valid_args(spawn_args_t *sa) 70 { 71 uint32_t sasize = sizeof (*sa) + 2; 72 73 (void) memset(sa, 0, sasize); 74 sa->sa_size = sasize; 75 sa->sa_datalen = 2; 76 sa->sa_arg_cnt = 1; 77 sa->sa_env_off = 2; 78 sa->sa_data[0] = 'x'; 79 sa->sa_data[1] = '\0'; 80 81 return (sasize); 82 } 83 84 static const char * 85 errname(int err) 86 { 87 const char *name = strerrorname_np(err); 88 89 return (name != NULL ? name : "?"); 90 } 91 92 static void 93 expect_err(const char *name, int wanted, const char *path, const void *sp, 94 uint32_t spsize, const void *sa, uint32_t sasize) 95 { 96 pid_t pid = raw_spawn(path, sp, spsize, sa, sasize); 97 98 if (pid != -1) { 99 TFAIL(name, "spawn unexpectedly succeeded (pid %d)", 100 (int)pid); 101 (void) waitpid(pid, NULL, 0); 102 return; 103 } 104 if (errno != wanted) { 105 TFAIL(name, "got %s (%d), wanted %s (%d)", errname(errno), 106 errno, errname(wanted), wanted); 107 return; 108 } 109 TPASS(name); 110 } 111 112 /* 113 * The positive case: a well-formed minimal request - a valid argument vector, 114 * no spawn parameters and no file actions - succeeds, and the child 115 * (/usr/bin/true) exits 0. 116 */ 117 static void 118 t_valid(void) 119 { 120 union { 121 spawn_args_t sa; 122 char pad[sizeof (spawn_args_t) + SA_DATA]; 123 } au; 124 uint32_t sasize = valid_args(&au.sa); 125 int status; 126 pid_t pid; 127 128 pid = raw_spawn("/usr/bin/true", NULL, 0, &au.sa, sasize); 129 if (pid == -1) { 130 TFAIL("valid", "spawn failed: %s", strerror(errno)); 131 return; 132 } 133 if (waitpid(pid, &status, 0) != pid) { 134 TFAIL("valid", "waitpid: %s", strerror(errno)); 135 return; 136 } 137 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { 138 TFAIL("valid", "status %#x", status); 139 return; 140 } 141 TPASS("valid, well-formed, minimal spawn request"); 142 } 143 144 static void 145 t_args_fuzz(void) 146 { 147 union { 148 spawn_args_t sa; 149 char pad[sizeof (spawn_args_t) + SA_DATA]; 150 } au; 151 spawn_args_t *sa = &au.sa; 152 uint32_t sasize; 153 154 expect_err("null-path", EINVAL, NULL, NULL, 0, &au.sa, 155 valid_args(&au.sa)); 156 expect_err("null-args", EINVAL, "/usr/bin/true", NULL, 0, NULL, 0); 157 158 sasize = valid_args(sa); 159 sa->sa_size = sasize + 8; 160 expect_err("args-size-mismatch", EINVAL, "/usr/bin/true", NULL, 0, 161 sa, sasize); 162 163 sasize = valid_args(sa); 164 sa->sa_datalen = 1; 165 expect_err("args-datalen-mismatch", EINVAL, "/usr/bin/true", NULL, 0, 166 sa, sasize); 167 168 sasize = valid_args(sa); 169 sa->sa_arg_cnt = 100; 170 expect_err("args-cnt-overrun", EINVAL, "/usr/bin/true", NULL, 0, 171 sa, sasize); 172 173 sasize = valid_args(sa); 174 sa->sa_data[1] = 'y'; /* string no longer NUL-terminated */ 175 expect_err("args-unterminated", EINVAL, "/usr/bin/true", NULL, 0, 176 sa, sasize); 177 178 sasize = valid_args(sa); 179 sa->sa_env_off = 100; 180 expect_err("args-env-off", EINVAL, "/usr/bin/true", NULL, 0, 181 sa, sasize); 182 183 sasize = valid_args(sa); 184 sa->sa_env_cnt = 7; 185 expect_err("args-env-cnt", EINVAL, "/usr/bin/true", NULL, 0, 186 sa, sasize); 187 188 sasize = valid_args(sa); 189 expect_err("args-e2big", E2BIG, "/usr/bin/true", NULL, 0, 190 sa, 0x400000); 191 192 /* Truncated buffer: shorter than the header itself. */ 193 sasize = valid_args(sa); 194 expect_err("args-short", EINVAL, "/usr/bin/true", NULL, 0, 195 sa, sizeof (*sa) - 4); 196 } 197 198 static void 199 t_param_fuzz(void) 200 { 201 union { 202 spawn_args_t sa; 203 char pad[sizeof (spawn_args_t) + SA_DATA]; 204 } au; 205 union { 206 spawn_param_t sp; 207 char pad[sizeof (spawn_param_t) + SP_DATA]; 208 } pu; 209 spawn_param_t *sp = &pu.sp; 210 uint32_t sasize = valid_args(&au.sa); 211 uint32_t spsize = sizeof (*sp) + SP_DATA; 212 kfile_attr_t *kfa; 213 214 /* Size field disagreeing with the system call argument. */ 215 (void) memset(&pu, 0, sizeof (pu)); 216 sp->sp_size = spsize + 4; 217 sp->sp_datalen = SP_DATA; 218 expect_err("param-size-mismatch", EINVAL, "/usr/bin/true", 219 sp, spsize, &au.sa, sasize); 220 221 /* Attribute region out of bounds. */ 222 (void) memset(&pu, 0, sizeof (pu)); 223 sp->sp_size = spsize; 224 sp->sp_datalen = SP_DATA; 225 sp->sp_attr_off = 60; 226 sp->sp_attr_len = sizeof (spawn_attr_t); 227 expect_err("param-attr-off", EINVAL, "/usr/bin/true", 228 sp, spsize, &au.sa, sasize); 229 230 /* Attribute length that is not sizeof (spawn_attr_t). */ 231 (void) memset(&pu, 0, sizeof (pu)); 232 sp->sp_size = spsize; 233 sp->sp_datalen = SP_DATA; 234 sp->sp_attr_len = 8; 235 expect_err("param-attr-len", EINVAL, "/usr/bin/true", 236 sp, spsize, &au.sa, sasize); 237 238 /* Undefined attribute flags. */ 239 (void) memset(&pu, 0, sizeof (pu)); 240 sp->sp_size = spsize; 241 sp->sp_datalen = SP_DATA; 242 sp->sp_attr_off = 0; 243 sp->sp_attr_len = sizeof (spawn_attr_t); 244 ((spawn_attr_t *)&sp->sp_data[0])->sa_psflags = ~0; 245 expect_err("param-attr-flags", EINVAL, "/usr/bin/true", 246 sp, spsize, &au.sa, sasize); 247 248 /* File action record with a zero length. */ 249 (void) memset(&pu, 0, sizeof (pu)); 250 sp->sp_size = spsize; 251 sp->sp_datalen = SP_DATA; 252 sp->sp_fattr_cnt = 2; 253 expect_err("param-fattr-len", EINVAL, "/usr/bin/true", 254 sp, spsize, &au.sa, sasize); 255 256 /* File action record with a bad type. */ 257 (void) memset(&pu, 0, sizeof (pu)); 258 sp->sp_size = spsize; 259 sp->sp_datalen = SP_DATA; 260 sp->sp_fattr_cnt = 1; 261 kfa = (kfile_attr_t *)&sp->sp_data[0]; 262 kfa->kfa_len = sizeof (*kfa); 263 kfa->kfa_type = 666; 264 expect_err("param-fattr-type", EINVAL, "/usr/bin/true", 265 sp, spsize, &au.sa, sasize); 266 267 /* FA_CHDIR with an unterminated path. */ 268 (void) memset(&pu, 0, sizeof (pu)); 269 sp->sp_size = spsize; 270 sp->sp_datalen = SP_DATA; 271 sp->sp_fattr_cnt = 1; 272 kfa = (kfile_attr_t *)&sp->sp_data[0]; 273 kfa->kfa_type = FA_CHDIR; 274 kfa->kfa_pathsize = 4; 275 kfa->kfa_len = sizeof (*kfa) + 4; 276 (void) memcpy(kfa->kfa_path, "/tmp", 4); /* no NUL */ 277 expect_err("param-chdir-nul", EINVAL, "/usr/bin/true", 278 sp, spsize, &au.sa, sasize); 279 280 /* Unterminated shell and search path strings. */ 281 (void) memset(&pu, 0, sizeof (pu)); 282 sp->sp_size = spsize; 283 sp->sp_datalen = SP_DATA; 284 sp->sp_shell_off = 0; 285 sp->sp_shell_len = 4; 286 (void) memcpy(sp->sp_data, "/bin", 4); 287 expect_err("param-shell-nul", EINVAL, "/usr/bin/true", 288 sp, spsize, &au.sa, sasize); 289 290 (void) memset(&pu, 0, sizeof (pu)); 291 sp->sp_size = spsize; 292 sp->sp_datalen = SP_DATA; 293 sp->sp_path_off = 62; 294 sp->sp_path_len = 8; 295 expect_err("param-path-off", EINVAL, "/usr/bin/true", 296 sp, spsize, &au.sa, sasize); 297 298 expect_err("param-e2big", E2BIG, "/usr/bin/true", 299 sp, 0x400000, &au.sa, sasize); 300 301 expect_err("param-short", EINVAL, "/usr/bin/true", 302 sp, sizeof (*sp) - 4, &au.sa, sasize); 303 } 304 305 /* 306 * Build a spawn_param_t carrying an attribute region with 307 * POSIX_SPAWN_SETSCHEDPARAM set and a scheduling region with the given 308 * operation. 309 */ 310 static kspawn_sched_t * 311 valid_sched(spawn_param_t *sp, uint32_t spsize, int op) 312 { 313 spawn_attr_t *spa; 314 kspawn_sched_t *ks; 315 316 (void) memset(sp, 0, spsize); 317 sp->sp_size = spsize; 318 sp->sp_datalen = spsize - sizeof (*sp); 319 sp->sp_attr_off = 0; 320 sp->sp_attr_len = sizeof (spawn_attr_t); 321 spa = (spawn_attr_t *)&sp->sp_data[0]; 322 spa->sa_psflags = POSIX_SPAWN_SETSCHEDPARAM; 323 sp->sp_sched_off = sizeof (spawn_attr_t); 324 sp->sp_sched_len = sizeof (kspawn_sched_t); 325 ks = (kspawn_sched_t *)&sp->sp_data[sp->sp_sched_off]; 326 ks->ksched_op = op; 327 328 return (ks); 329 } 330 331 static void 332 t_sched_fuzz(void) 333 { 334 union { 335 spawn_args_t sa; 336 char pad[sizeof (spawn_args_t) + SA_DATA]; 337 } au; 338 union { 339 spawn_param_t sp; 340 char pad[sizeof (spawn_param_t) + 128]; 341 } pu; 342 spawn_param_t *sp = &pu.sp; 343 uint32_t sasize = valid_args(&au.sa); 344 uint32_t spsize = sizeof (*sp) + 128; 345 kspawn_sched_t *ks; 346 347 /* Scheduling flag set but no scheduling region. */ 348 (void) valid_sched(sp, spsize, KSCHED_PARMS); 349 sp->sp_sched_off = sp->sp_sched_len = 0; 350 expect_err("sched-missing", EINVAL, "/usr/bin/true", 351 sp, spsize, &au.sa, sasize); 352 353 /* Scheduling region present without a scheduling flag. */ 354 (void) valid_sched(sp, spsize, KSCHED_PARMS); 355 ((spawn_attr_t *)&sp->sp_data[0])->sa_psflags = 0; 356 expect_err("sched-no-flags", EINVAL, "/usr/bin/true", 357 sp, spsize, &au.sa, sasize); 358 359 /* Scheduling region with the wrong length. */ 360 (void) valid_sched(sp, spsize, KSCHED_PARMS); 361 sp->sp_sched_len = 8; 362 expect_err("sched-len", EINVAL, "/usr/bin/true", 363 sp, spsize, &au.sa, sasize); 364 365 /* Scheduling region out of bounds. */ 366 (void) valid_sched(sp, spsize, KSCHED_PARMS); 367 sp->sp_sched_off = sp->sp_datalen - 4; 368 expect_err("sched-off", EINVAL, "/usr/bin/true", 369 sp, spsize, &au.sa, sasize); 370 371 /* Invalid operation. */ 372 (void) valid_sched(sp, spsize, 0); 373 expect_err("sched-op", EINVAL, "/usr/bin/true", 374 sp, spsize, &au.sa, sasize); 375 376 /* A priority change must be a set operation. */ 377 ks = valid_sched(sp, spsize, KSCHED_PRIO); 378 ks->ksched_prio.pc_op = PC_GETPRIO; 379 ks->ksched_prio.pc_cid = 1; 380 expect_err("sched-prio-op", EINVAL, "/usr/bin/true", 381 sp, spsize, &au.sa, sasize); 382 383 /* 384 * Class IDs out of range. These are not caught until the child 385 * applies the attributes to itself, exercising the error handshake. 386 */ 387 ks = valid_sched(sp, spsize, KSCHED_PRIO); 388 ks->ksched_prio.pc_op = PC_SETPRIO; 389 ks->ksched_prio.pc_cid = 999; 390 expect_err("sched-prio-cid", EINVAL, "/usr/bin/true", 391 sp, spsize, &au.sa, sasize); 392 393 ks = valid_sched(sp, spsize, KSCHED_PARMS); 394 ks->ksched_parms.pc_cid = 999; 395 expect_err("sched-parms-cid", EINVAL, "/usr/bin/true", 396 sp, spsize, &au.sa, sasize); 397 } 398 399 /* 400 * A child killed immediately after spawn(2) returns must be waitable and 401 * must never leave the parent stuck. We try a few times to give us a fighting 402 * chance of catching it. 403 */ 404 static void 405 t_kill_race(void) 406 { 407 union { 408 spawn_args_t sa; 409 char pad[sizeof (spawn_args_t) + SA_DATA]; 410 } au; 411 uint32_t sasize = valid_args(&au.sa); 412 413 /* 414 * Spawn a child and immediately kill it, over and over, to exercise 415 * the window between creation and exec. valid_args() supplies only 416 * argv[0], so sleep runs with no duration. That does not matter, 417 * because we SIGKILL it at once - all we need is a live PID to race 418 * against. 419 */ 420 for (int i = 0; i < 100; i++) { 421 int status; 422 pid_t pid; 423 424 pid = raw_spawn("/usr/bin/sleep", NULL, 0, &au.sa, sasize); 425 if (pid == -1) { 426 TFAIL("kill-race", "spawn failed: %s", 427 strerror(errno)); 428 return; 429 } 430 (void) kill(pid, SIGKILL); 431 if (waitpid(pid, &status, 0) != pid) { 432 TFAIL("kill-race", "waitpid: %s", strerror(errno)); 433 return; 434 } 435 if (!WIFSIGNALED(status) && 436 !(WIFEXITED(status) && WEXITSTATUS(status) != 0)) { 437 TFAIL("kill-race", "iteration %d: status %#x", 438 i, status); 439 return; 440 } 441 } 442 TPASS("kill-race"); 443 } 444 445 int 446 main(void) 447 { 448 t_valid(); 449 t_args_fuzz(); 450 t_param_fuzz(); 451 t_sched_fuzz(); 452 t_kill_race(); 453 454 if (failures == 0) { 455 (void) printf("All tests passed\n"); 456 return (EXIT_SUCCESS); 457 } 458 459 (void) fprintf(stderr, "%u test(s) failed\n", failures); 460 return (EXIT_FAILURE); 461 } 462