-
-
Notifications
You must be signed in to change notification settings - Fork 151
fix(child_process): spawn via posix_spawn to avoid a macOS fork/dyld deadlock #7157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
proggeramlug
merged 2 commits into
PerryTS:main
from
jdalton:fix/macos-child-process-posix-spawn
Aug 1, 2026
+244
−23
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| **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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 25634
🌐 Web query:
Rust std::process::Command posix_spawn fork exec PATH env_clear execvp documentation💡 Result:
On Unix-like platforms, Rust's std::process::Command uses a combination of posix_spawn and the traditional fork/exec pattern to launch child processes [1][2][3]. 1. Strategy Selection: Rust preferentially uses posix_spawn for process creation because it can be more efficient and avoids some complexities associated with memory management after a fork (e.g., thread synchronization) [1][2][4]. However, it falls back to the fork/exec pattern if the configuration requires operations not easily supported by posix_spawn, such as pre-exec closures (via pre_exec), setting specific UIDs/GIDs, or when the environment's PATH has been modified (depending on the target) [3][5][6]. 2. PATH and Environment: If a program is not specified as an absolute path, the system must search for the executable. When using the traditional fork/exec path, Rust uses execvp, which automatically searches the PATH environment variable [7][8]. If env_clear or env_remove is used to remove the PATH variable without providing a replacement, execvp will typically fall back to an OS-defined default (often /bin:/usr/bin), rather than defaulting to the parent process's PATH [7][8]. 3. posix_spawn and Environment: posix_spawn generally does not perform PATH resolution in the same way as execvp (unless posix_spawnp is specifically used, which has inconsistent platform availability and behavior) [4]. Consequently, if Rust detects that the environment's PATH has been altered or that complex path-related logic is needed, it may disqualify the posix_spawn path in favor of fork/exec to ensure consistent, predictable executable resolution [3][4][5]. 4. env_clear: This method prevents the child process from inheriting any environment variables from the parent [7][9]. While this effectively clears the environment, users should be aware that if the PATH is not explicitly re-added, the system's ability to locate executables will rely on the default behavior of the underlying exec function (e.g., execvp's fallback to system defaults) rather than the parent's original environment [7][8].
Citations:
🌐 Web query:
POSIX execvp empty PATH element current directory semantics PATH missing💡 Result:
Under the POSIX standard, if the PATH environment variable is not present, the behavior of execvp regarding the search path is explicitly implementation-defined [1][2][3]. Consequently, there is no single "POSIX standard" behavior for what happens when PATH is missing; different systems handle this differently [4][5]. Regarding the interpretation of PATH elements: 1. Empty PATH Elements: POSIX specifies that a zero-length (empty) prefix in the PATH variable shall be treated as the current working directory [6]. This is commonly represented by a leading colon, a trailing colon, or two adjacent colons in the PATH string (e.g., PATH=:/bin or PATH=/bin::/usr/bin) [6]. 2. Missing PATH Variable: On many Linux systems (specifically those using glibc), if the PATH variable is absent, the system defaults to a search path that typically includes the current working directory followed by the directories defined by confstr(_CS_PATH) [4][7][6]. Historically, this often placed the current directory at the beginning of the search path [4][5]. However, this behavior can vary; for instance, some modern implementations have opted to drop the current directory from this default search path to mitigate security risks associated with potential Trojan horses [4][7]. In summary, while POSIX mandates that empty elements in a present PATH variable denote the current directory, it leaves the default behavior entirely to the implementation when the PATH variable itself is missing [1][2][3]. Users should consult the specific documentation for their operating system or C library (e.g., glibc) to determine the exact search order employed [4][7].
Citations:
Align background-spawn
PATHlookup withcp_effective_path.cp_effective_pathdocuments replacement-modeenvlookup as childPATHor theexecvpdefault only, withCP_DEFAULT_PATHas that default. This resolver falls back to the parent’sPATHwhen anenvobject has no"PATH", then uses an empty fallback whenPATHis absent from both, whichcp_resolve_program_pathinterprets as the current directory. Match the existing resolvedCommandpath: use the child’sPATHwhen theenvmap is present, otherwiseCP_DEFAULT_PATH.🤖 Prompt for AI Agents