Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion src/foundation/subprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#ifdef __APPLE__
#include <spawn.h>
extern char **environ;
#endif
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
Expand Down Expand Up @@ -877,6 +881,9 @@ static cbm_proc_poll_t cbm_subprocess_poll_win(cbm_subprocess_t *process, cbm_pr

#else /* POSIX */

/* Used by the fork+exec child. posix_spawn performs the same reset
* declaratively via SETSIGDEF + SETSIGMASK, but Apple still forks for the
* exec-failure fallback below, so this stays compiled everywhere. */
static void cbm_posix_reset_child_signals(void) {
struct sigaction action = {0};
action.sa_handler = SIG_DFL;
Expand All @@ -891,6 +898,9 @@ static void cbm_posix_reset_child_signals(void) {
(void)sigprocmask(SIG_SETMASK, &empty, NULL);
}

/* fork+exec child setup. On Apple this runs ONLY for the exec-failure
* fallback (see cbm_posix_spawn_apple), which preserves the documented
* "bogus binary => child exits 127" contract across platforms. */
static void cbm_posix_child_exec(cbm_subprocess_t *process, int input, int output, long max_fd) {
if (setpgid(0, 0) < 0) {
_exit(127);
Expand Down Expand Up @@ -935,6 +945,74 @@ static int cbm_posix_fd_at_least_three(int fd) {
return duplicate;
}

#ifdef __APPLE__
/* 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 test suite hit
* exactly this: `git init` inside a fixture failed once enough suites had run
* ahead of it, and the symptom was an unrelated-looking assertion. The same
* hazard is already documented in tests/test_daemon_runtime.c, which switched
* to posix_spawn for the same reason.
*
* posix_spawn never copies the parent address space, so the footprint is
* irrelevant. Every guarantee of the fork path is preserved:
* - own process group (SETPGROUP + setpgroup(0)) — the kill-tree contract
* - default signal dispositions and an empty mask (SETSIGDEF/SETSIGMASK)
* - stdin/stdout/stderr wired to the caller's fds (adddup2)
* - every OTHER descriptor closed: CLOEXEC_DEFAULT is Apple's equivalent of
* the child's close-everything loop, and the three dup2'd fds stay open
* because dup2 clears close-on-exec.
* posix_spawnp keeps execvp's PATH semantics for a bare tool name. */
static int cbm_posix_spawn_apple(cbm_subprocess_t *process, int input, int output, pid_t *pid_out) {
posix_spawn_file_actions_t actions;
posix_spawnattr_t attr;
if (posix_spawn_file_actions_init(&actions) != 0) {
return -1;
}
if (posix_spawnattr_init(&attr) != 0) {
(void)posix_spawn_file_actions_destroy(&actions);
return -1;
}
sigset_t empty_mask;
sigset_t all_signals;
sigemptyset(&empty_mask);
sigfillset(&all_signals);
short flags = (short)(POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK |
POSIX_SPAWN_CLOEXEC_DEFAULT);
bool configured = posix_spawnattr_setflags(&attr, flags) == 0 &&
posix_spawnattr_setpgroup(&attr, 0) == 0 &&
posix_spawnattr_setsigmask(&attr, &empty_mask) == 0 &&
posix_spawnattr_setsigdefault(&attr, &all_signals) == 0 &&
posix_spawn_file_actions_adddup2(&actions, input, STDIN_FILENO) == 0 &&
posix_spawn_file_actions_adddup2(&actions, output, STDOUT_FILENO) == 0 &&
posix_spawn_file_actions_adddup2(&actions, output, STDERR_FILENO) == 0;
pid_t pid = -1;
int rc =
configured ? posix_spawnp(&pid, process->bin, &actions, &attr, process->argv, environ) : -1;
(void)posix_spawn_file_actions_destroy(&actions);
(void)posix_spawnattr_destroy(&attr);
if (configured && rc == 0 && pid > 0) {
*pid_out = pid;
return 0;
}
/* posix_spawn reports an unusable binary itself, where fork+exec instead
* produces a child that exits 127. Callers (and tests) rely on the latter:
* "spawn_failed" means the SPAWN mechanism failed, not that the tool was
* missing. Fall back to fork+exec for exec-class errors so macOS and Linux
* classify a bogus binary identically; the ASan-fork hazard does not apply
* here, since this child exits immediately. */
if (configured && (rc == ENOENT || rc == EACCES || rc == ENOEXEC || rc == EISDIR ||
rc == ELOOP || rc == ENAMETOOLONG || rc == ENOTDIR)) {
return 1;
}
return -1;
}
#endif

static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
int input_flags = O_RDONLY;
#ifdef O_CLOEXEC
Expand Down Expand Up @@ -977,7 +1055,27 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
max_fd = 65536L;
}

pid_t pid = fork();
pid_t pid = -1;
#ifdef __APPLE__
int spawn_rc = cbm_posix_spawn_apple(process, input, output, &pid);
if (spawn_rc < 0) {
(void)close(input);
(void)close(output);
return -1;
}
if (spawn_rc > 0) { /* exec-class failure: reproduce the fork+exec 127 */
pid = fork();
if (pid < 0) {
(void)close(input);
(void)close(output);
return -1;
}
if (pid == 0) {
cbm_posix_child_exec(process, input, output, max_fd);
}
}
#else
pid = fork();
if (pid < 0) {
(void)close(input);
(void)close(output);
Expand All @@ -986,6 +1084,7 @@ static int cbm_subprocess_spawn_posix(cbm_subprocess_t *process) {
if (pid == 0) {
cbm_posix_child_exec(process, input, output, max_fd);
}
#endif
(void)close(input);
(void)close(output);

Expand Down
Loading