macOS: spawn instead of fork+exec. * * fork() duplicates the parent's whole address space bookkeeping, and an * ASan-instrumented parent carries an enormous shadow mapping. Past a * footprint threshold the child is killed (jetsam) BEFORE exec replaces the * image, so the call fails with the child already gone (ESRCH on reap) — a * spawn failure that looks like the launched tool crashing. The
| 1043 | * because dup2 clears close-on-exec. |
| 1044 | * posix_spawnp keeps execvp's PATH semantics for a bare tool name. */ |
| 1045 | static int cbm_posix_spawn_apple(cbm_subprocess_t *process, int input, int output, pid_t *pid_out) { |
| 1046 | /* posix_spawn is the PRIMARY path on macOS; fork+exec is only the |
| 1047 | * exec-class fallback. Injection therefore has to live here too, or a test |
| 1048 | * on macOS exercises nothing. */ |
| 1049 | #ifdef CBM_ENABLE_TEST_SEAMS |
| 1050 | if (cbm_spawn_eagain_injected()) { |
| 1051 | return CBM_SPAWN_RETRY; |
| 1052 | } |
| 1053 | #endif |
| 1054 | posix_spawn_file_actions_t actions; |
| 1055 | posix_spawnattr_t attr; |
| 1056 | if (posix_spawn_file_actions_init(&actions) != 0) { |
| 1057 | return -1; |
| 1058 | } |
| 1059 | if (posix_spawnattr_init(&attr) != 0) { |
| 1060 | (void)posix_spawn_file_actions_destroy(&actions); |
| 1061 | return -1; |
| 1062 | } |
| 1063 | sigset_t empty_mask; |
| 1064 | sigset_t all_signals; |
| 1065 | sigemptyset(&empty_mask); |
| 1066 | sigfillset(&all_signals); |
| 1067 | short flags = (short)(POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK | |
| 1068 | POSIX_SPAWN_CLOEXEC_DEFAULT); |
| 1069 | bool configured = posix_spawnattr_setflags(&attr, flags) == 0 && |
| 1070 | posix_spawnattr_setpgroup(&attr, 0) == 0 && |
| 1071 | posix_spawnattr_setsigmask(&attr, &empty_mask) == 0 && |
| 1072 | posix_spawnattr_setsigdefault(&attr, &all_signals) == 0 && |
| 1073 | posix_spawn_file_actions_adddup2(&actions, input, STDIN_FILENO) == 0 && |
| 1074 | posix_spawn_file_actions_adddup2(&actions, output, STDOUT_FILENO) == 0 && |
| 1075 | posix_spawn_file_actions_adddup2(&actions, output, STDERR_FILENO) == 0; |
| 1076 | pid_t pid = -1; |
| 1077 | int rc = |
| 1078 | configured ? posix_spawnp(&pid, process->bin, &actions, &attr, process->argv, environ) : -1; |
| 1079 | (void)posix_spawn_file_actions_destroy(&actions); |
| 1080 | (void)posix_spawnattr_destroy(&attr); |
| 1081 | if (configured && rc == 0 && pid > 0) { |
| 1082 | *pid_out = pid; |
| 1083 | return 0; |
| 1084 | } |
| 1085 | /* posix_spawn reports an unusable binary itself, where fork+exec instead |
| 1086 | * produces a child that exits 127. Callers (and tests) rely on the latter: |
| 1087 | * "spawn_failed" means the SPAWN mechanism failed, not that the tool was |
| 1088 | * missing. Fall back to fork+exec for exec-class errors so macOS and Linux |
| 1089 | * classify a bogus binary identically; the ASan-fork hazard does not apply |
| 1090 | * here, since this child exits immediately. */ |
| 1091 | if (configured && (rc == ENOENT || rc == EACCES || rc == ENOEXEC || rc == EISDIR || |
| 1092 | rc == ELOOP || rc == ENAMETOOLONG || rc == ENOTDIR)) { |
| 1093 | return 1; |
| 1094 | } |
| 1095 | /* EAGAIN/ENOMEM are the kernel saying "not right now", not "never": the |
| 1096 | * process table or a per-user limit is momentarily full. Reporting |
| 1097 | * spawn_failed for that turns transient load into a user-visible error — |
| 1098 | * a git or LSP probe failing on a busy laptop for no reason the user can |
| 1099 | * see or act on. Retry briefly. Everything else stays a hard failure. */ |
| 1100 | if (configured && (rc == EAGAIN || rc == ENOMEM)) { |
| 1101 | return CBM_SPAWN_RETRY; |
| 1102 | } |
no test coverage detected