You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(child_process): spawn via posix_spawn to avoid a macOS fork/dyld deadlock (#7157)
`std::process::Command` only uses `posix_spawn` when the program is given as a
path and no `pre_exec`/uid/gid closures are set; a bare command name combined
with an `env` option (which calls `env_clear()`) drops it onto the `fork`+`exec`
fallback. On macOS a `fork` from Perry's multithreaded runtime (async reactor +
GC/worker threads) can deadlock the child post-`exec` in dyld
(`RemoteNotificationResponder::blockOnSynchronousEvent`): the child inherits
locks/Mach state from parent threads that no longer exist after `fork`. The
reader/waiter threads then block forever in `read()`/`wait4()` and the main loop
idles in `js_wait_for_event`.
Resolve a bare command name to its absolute path in the child's effective PATH
before building the `Command`, so std stays on the `posix_spawn` fast path even
when an `env`/`cwd` option is present; `argv[0]` is preserved via `arg0`. The
`exec`/`execSync`/promisify(exec) shell now uses the absolute `/bin/sh` (Node's
shell), and `spawn`/`spawnSync`/`execFile`/`execFileSync`/spawn_background all
resolve their program. Verified with a dyld interposer: `exec`/`spawnSync` with
an `env` option go from `fork()` to `posix_spawn` while stdout/exit-code capture
and argv are unchanged.
`detached` (setsid), `fork()`'s IPC dup2, and uid/gid necessarily keep std's
fork path — `posix_spawn` can't express them via std — and are documented as
such; they are outside the reported exec/spawn impact.
Co-authored-by: Ralph Küpper <ralph@skelpo.com>
**macOS `child_process` fork/dyld deadlock:** `exec`/`spawn`/`execFile` (and their `Sync` forms) could deadlock a child on macOS. `std::process::Command` falls back from `posix_spawn` to `fork`+`exec` whenever a bare command name is combined with an `env` option (`env_clear()` sets `env_saw_path()`), and `fork` from Perry's multithreaded runtime (async reactor + GC/worker threads) leaves the child holding locks/Mach state from parent threads that no longer exist — so a fast child like `sh -c "echo hi"` hangs post-`exec` in dyld (`RemoteNotificationResponder::blockOnSynchronousEvent`), the reader/waiter threads block in `read()`/`wait4()`, and the main loop idles in `js_wait_for_event`. Perry now resolves a bare command name to its absolute path in the child's effective PATH before building the `Command`, keeping std on the `posix_spawn` fast path (`argv[0]` preserved via `arg0`); the `exec` shell uses the absolute `/bin/sh`. Verified with a dyld interposer: `env`-carrying `exec`/`spawnSync` go from `fork()` to `posix_spawn` with output/exit-code/argv capture unchanged. `detached` (setsid), `fork()`'s IPC `dup2`, and uid/gid necessarily keep std's fork path (not expressible through std's `posix_spawn`) and are documented inline. Linux behavior is unchanged.
0 commit comments