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
| 1090 | * because dup2 clears close-on-exec. |
| 1091 | * posix_spawnp keeps execvp's PATH semantics for a bare tool name. */ |
| 1092 | static int cbm_posix_spawn_apple(cbm_subprocess_t *process, int input, int output, pid_t *pid_out) { |
| 1093 | /* posix_spawn is the PRIMARY path on macOS; fork+exec is only the |
| 1094 | * exec-class fallback. Injection therefore has to live here too, or a test |
| 1095 | * on macOS exercises nothing. */ |
| 1096 | #ifdef CBM_ENABLE_TEST_SEAMS |
| 1097 | if (cbm_spawn_eagain_injected()) { |
| 1098 | return CBM_SPAWN_RETRY; |
| 1099 | } |
| 1100 | #endif |
| 1101 | posix_spawn_file_actions_t actions; |
| 1102 | posix_spawnattr_t attr; |
| 1103 | if (posix_spawn_file_actions_init(&actions) != 0) { |
| 1104 | return -1; |
| 1105 | } |
| 1106 | if (posix_spawnattr_init(&attr) != 0) { |
| 1107 | (void)posix_spawn_file_actions_destroy(&actions); |
| 1108 | return -1; |
| 1109 | } |
| 1110 | sigset_t empty_mask; |
| 1111 | sigset_t all_signals; |
| 1112 | sigemptyset(&empty_mask); |
| 1113 | sigfillset(&all_signals); |
| 1114 | short flags = (short)(POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK | |
| 1115 | POSIX_SPAWN_CLOEXEC_DEFAULT); |
| 1116 | bool configured = posix_spawnattr_setflags(&attr, flags) == 0 && |
| 1117 | posix_spawnattr_setpgroup(&attr, 0) == 0 && |
| 1118 | posix_spawnattr_setsigmask(&attr, &empty_mask) == 0 && |
| 1119 | posix_spawnattr_setsigdefault(&attr, &all_signals) == 0 && |
| 1120 | posix_spawn_file_actions_adddup2(&actions, input, STDIN_FILENO) == 0 && |
| 1121 | posix_spawn_file_actions_adddup2(&actions, output, STDOUT_FILENO) == 0 && |
| 1122 | posix_spawn_file_actions_adddup2(&actions, output, STDERR_FILENO) == 0; |
| 1123 | pid_t pid = -1; |
| 1124 | int rc = |
| 1125 | configured ? posix_spawnp(&pid, process->bin, &actions, &attr, process->argv, environ) : -1; |
| 1126 | (void)posix_spawn_file_actions_destroy(&actions); |
| 1127 | (void)posix_spawnattr_destroy(&attr); |
| 1128 | if (configured && rc == 0 && pid > 0) { |
| 1129 | *pid_out = pid; |
| 1130 | return 0; |
| 1131 | } |
| 1132 | /* posix_spawn reports an unusable binary itself, where fork+exec instead |
| 1133 | * produces a child that exits 127. Callers (and tests) rely on the latter: |
| 1134 | * "spawn_failed" means the SPAWN mechanism failed, not that the tool was |
| 1135 | * missing. Fall back to fork+exec for exec-class errors so macOS and Linux |
| 1136 | * classify a bogus binary identically; the ASan-fork hazard does not apply |
| 1137 | * here, since this child exits immediately. */ |
| 1138 | if (configured && (rc == ENOENT || rc == EACCES || rc == ENOEXEC || rc == EISDIR || |
| 1139 | rc == ELOOP || rc == ENAMETOOLONG || rc == ENOTDIR)) { |
| 1140 | return 1; |
| 1141 | } |
| 1142 | /* EAGAIN/ENOMEM are the kernel saying "not right now", not "never": the |
| 1143 | * process table or a per-user limit is momentarily full. Reporting |
| 1144 | * spawn_failed for that turns transient load into a user-visible error — |
| 1145 | * a git or LSP probe failing on a busy laptop for no reason the user can |
| 1146 | * see or act on. Retry briefly. Everything else stays a hard failure. */ |
| 1147 | if (configured && (rc == EAGAIN || rc == ENOMEM)) { |
| 1148 | return CBM_SPAWN_RETRY; |
| 1149 | } |
no test coverage detected