Fix binary name resolution against custom PATH on macOS - #55672
Merged
kubkon merged 5 commits intoJun 19, 2026
Conversation
G36maid
force-pushed
the
fix/posix-spawnp-path-resolution
branch
from
May 4, 2026 17:22
881ad73 to
90267fb
Compare
G36maid
marked this pull request as ready for review
May 4, 2026 17:30
G36maid
force-pushed
the
fix/posix-spawnp-path-resolution
branch
from
May 4, 2026 17:31
90267fb to
2caea8d
Compare
G36maid
marked this pull request as draft
May 4, 2026 17:39
On macOS, posix_spawnp resolves bare program names using the parent
process's environ (via getenv("PATH")), ignoring the PATH inside the
custom envp argument. This causes commands like external formatters or
LSP binaries that are only available in a project-specific PATH (e.g.
from Nix/direnv) to fail with ENOENT.
Before calling posix_spawnp, if the program name contains no slash and
a custom environment with a PATH entry is provided, resolve the program
to an absolute path using which::which_in against the child's PATH.
Update darwin.rs
G36maid
force-pushed
the
fix/posix-spawnp-path-resolution
branch
from
May 4, 2026 17:43
2caea8d to
598a7fb
Compare
G36maid
marked this pull request as ready for review
May 4, 2026 17:44
Contributor
dnlzro
suggested changes
May 10, 2026
dnlzro
left a comment
There was a problem hiding this comment.
Needed to add an import before it would build, but otherwise all good. Addresses the linked issue on my Mac.
dnlzro
approved these changes
May 11, 2026
# Conflicts: # crates/util/src/command/darwin.rs
kubkon
approved these changes
Jun 19, 2026
kubkon
left a comment
Member
There was a problem hiding this comment.
Looks great, thanks! I merged main and fixed a couple of tiny nits.
saranblock3
pushed a commit
to saranblock3/zed
that referenced
this pull request
Jun 23, 2026
…es#55672) Closes zed-industries#50536 ## Summary Addresses zed-industries#50536 - On macOS, `posix_spawnp` resolves programs against the parent process's `cwd` and `environ` (via `getcwd` and `getenv("PATH")`), ignoring the child's `current_dir` and `envp`. This causes two classes of failures when Zed spawns external commands via the custom `posix_spawnp`-based `Command`: - **Bare names** (e.g. `"black"`): resolved via the parent's `PATH`, not the child's — binaries only available in a project-specific PATH (Nix/direnv) are not found. - **Relative paths** (e.g. `"./script.sh"`, `"bin/test.sh"`): resolved against the parent's `cwd`, not `current_dir` — only works when Zed's cwd happens to match the project root. - Added program resolution in `spawn_posix_spawn` (`crates/util/src/command/darwin.rs`): before calling `posix_spawnp`, resolve the program to an absolute path — bare names via `which::which_in` against the child's PATH, relative paths via `Path::join(current_dir)`. Falls back to the original program if resolution fails. ## Root Cause Apple's Libc implementation of `posix_spawnp` resolves the program path using the parent process's context — `getcwd()` for relative paths and `getenv("PATH")` for bare names — rather than the `current_dir` (set via `posix_spawn_file_actions_addchdir_np`) or `envp` argument. The child's working directory and environment only take effect **after** the binary has already been located. This is a well-documented macOS behavior that Rust's own `std::process::Command` works around by bypassing `posix_spawn` when PATH is modified (see [rust-lang/rust#48624](rust-lang/rust#48624)). The regression was introduced when PR zed-industries#49090 switched macOS from `std::process::Command` (which uses fork+execvp, correctly using the child's cwd and PATH) to a custom posix_spawnp-based implementation (which does not). ## Testing - `test_bare_program_resolved_via_custom_path` — bare name resolves via child's custom PATH - `test_bare_program_with_custom_path_falls_back_when_not_found` — non-existent binary still errors - `test_bare_program_with_custom_env_no_path_key` — custom env without PATH key falls back gracefully - `test_relative_path_skips_resolution` — relative path resolves against `current_dir` instead of parent's cwd Note: The fix and tests are in `darwin.rs` which is macOS-only. The Linux path uses `smol::process::Command` (via fork+execve) and is unaffected. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed external formatters and language servers failing to launch on macOS when specified as a bare binary name or relative path and only available in the project's PATH (e.g. Nix, direnv) --------- Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
weihuoya
pushed a commit
to weihuoya/zed
that referenced
this pull request
Jun 25, 2026
…es#55672) Closes zed-industries#50536 ## Summary Addresses zed-industries#50536 - On macOS, `posix_spawnp` resolves programs against the parent process's `cwd` and `environ` (via `getcwd` and `getenv("PATH")`), ignoring the child's `current_dir` and `envp`. This causes two classes of failures when Zed spawns external commands via the custom `posix_spawnp`-based `Command`: - **Bare names** (e.g. `"black"`): resolved via the parent's `PATH`, not the child's — binaries only available in a project-specific PATH (Nix/direnv) are not found. - **Relative paths** (e.g. `"./script.sh"`, `"bin/test.sh"`): resolved against the parent's `cwd`, not `current_dir` — only works when Zed's cwd happens to match the project root. - Added program resolution in `spawn_posix_spawn` (`crates/util/src/command/darwin.rs`): before calling `posix_spawnp`, resolve the program to an absolute path — bare names via `which::which_in` against the child's PATH, relative paths via `Path::join(current_dir)`. Falls back to the original program if resolution fails. ## Root Cause Apple's Libc implementation of `posix_spawnp` resolves the program path using the parent process's context — `getcwd()` for relative paths and `getenv("PATH")` for bare names — rather than the `current_dir` (set via `posix_spawn_file_actions_addchdir_np`) or `envp` argument. The child's working directory and environment only take effect **after** the binary has already been located. This is a well-documented macOS behavior that Rust's own `std::process::Command` works around by bypassing `posix_spawn` when PATH is modified (see [rust-lang/rust#48624](rust-lang/rust#48624)). The regression was introduced when PR zed-industries#49090 switched macOS from `std::process::Command` (which uses fork+execvp, correctly using the child's cwd and PATH) to a custom posix_spawnp-based implementation (which does not). ## Testing - `test_bare_program_resolved_via_custom_path` — bare name resolves via child's custom PATH - `test_bare_program_with_custom_path_falls_back_when_not_found` — non-existent binary still errors - `test_bare_program_with_custom_env_no_path_key` — custom env without PATH key falls back gracefully - `test_relative_path_skips_resolution` — relative path resolves against `current_dir` instead of parent's cwd Note: The fix and tests are in `darwin.rs` which is macOS-only. The Linux path uses `smol::process::Command` (via fork+execve) and is unaffected. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed external formatters and language servers failing to launch on macOS when specified as a bare binary name or relative path and only available in the project's PATH (e.g. Nix, direnv) --------- Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
jolutz
pushed a commit
to jolutz/zed
that referenced
this pull request
Aug 8, 2026
…es#55672) Closes zed-industries#50536 ## Summary Addresses zed-industries#50536 - On macOS, `posix_spawnp` resolves programs against the parent process's `cwd` and `environ` (via `getcwd` and `getenv("PATH")`), ignoring the child's `current_dir` and `envp`. This causes two classes of failures when Zed spawns external commands via the custom `posix_spawnp`-based `Command`: - **Bare names** (e.g. `"black"`): resolved via the parent's `PATH`, not the child's — binaries only available in a project-specific PATH (Nix/direnv) are not found. - **Relative paths** (e.g. `"./script.sh"`, `"bin/test.sh"`): resolved against the parent's `cwd`, not `current_dir` — only works when Zed's cwd happens to match the project root. - Added program resolution in `spawn_posix_spawn` (`crates/util/src/command/darwin.rs`): before calling `posix_spawnp`, resolve the program to an absolute path — bare names via `which::which_in` against the child's PATH, relative paths via `Path::join(current_dir)`. Falls back to the original program if resolution fails. ## Root Cause Apple's Libc implementation of `posix_spawnp` resolves the program path using the parent process's context — `getcwd()` for relative paths and `getenv("PATH")` for bare names — rather than the `current_dir` (set via `posix_spawn_file_actions_addchdir_np`) or `envp` argument. The child's working directory and environment only take effect **after** the binary has already been located. This is a well-documented macOS behavior that Rust's own `std::process::Command` works around by bypassing `posix_spawn` when PATH is modified (see [rust-lang/rust#48624](rust-lang/rust#48624)). The regression was introduced when PR zed-industries#49090 switched macOS from `std::process::Command` (which uses fork+execvp, correctly using the child's cwd and PATH) to a custom posix_spawnp-based implementation (which does not). ## Testing - `test_bare_program_resolved_via_custom_path` — bare name resolves via child's custom PATH - `test_bare_program_with_custom_path_falls_back_when_not_found` — non-existent binary still errors - `test_bare_program_with_custom_env_no_path_key` — custom env without PATH key falls back gracefully - `test_relative_path_skips_resolution` — relative path resolves against `current_dir` instead of parent's cwd Note: The fix and tests are in `darwin.rs` which is macOS-only. The Linux path uses `smol::process::Command` (via fork+execve) and is unaffected. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed external formatters and language servers failing to launch on macOS when specified as a bare binary name or relative path and only available in the project's PATH (e.g. Nix, direnv) --------- Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
This was referenced Aug 29, 2026
vitallium
added a commit
to zed-extensions/ruby
that referenced
this pull request
Aug 29, 2026
Zed now resolves bare executables against the child `PATH` on macOS before `posix_spawnp`. See zed-industries/zed#55672, so extension-side ruby/gem/bundle probes no longer need a compile-time workaround. Refs zed-industries/zed#57170
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #50536
Summary
Addresses #50536
posix_spawnpresolves programs against the parent process'scwdandenviron(viagetcwdandgetenv("PATH")), ignoring the child'scurrent_dirandenvp. This causes two classes of failures when Zed spawns external commands via the customposix_spawnp-basedCommand:"black"): resolved via the parent'sPATH, not the child's — binaries only available in a project-specific PATH (Nix/direnv) are not found."./script.sh","bin/test.sh"): resolved against the parent'scwd, notcurrent_dir— only works when Zed's cwd happens to match the project root.spawn_posix_spawn(crates/util/src/command/darwin.rs): before callingposix_spawnp, resolve the program to an absolute path — bare names viawhich::which_inagainst the child's PATH, relative paths viaPath::join(current_dir). Falls back to the original program if resolution fails.Root Cause
Apple's Libc implementation of
posix_spawnpresolves the program path using the parent process's context —getcwd()for relative paths andgetenv("PATH")for bare names — rather than thecurrent_dir(set viaposix_spawn_file_actions_addchdir_np) orenvpargument. The child's working directory and environment only take effect after the binary has already been located. This is a well-documented macOS behavior that Rust's ownstd::process::Commandworks around by bypassingposix_spawnwhen PATH is modified (see rust-lang/rust#48624).The regression was introduced when PR #49090 switched macOS from
std::process::Command(which uses fork+execvp, correctly using the child's cwd and PATH) to a custom posix_spawnp-based implementation (which does not).Testing
test_bare_program_resolved_via_custom_path— bare name resolves via child's custom PATHtest_bare_program_with_custom_path_falls_back_when_not_found— non-existent binary still errorstest_bare_program_with_custom_env_no_path_key— custom env without PATH key falls back gracefullytest_relative_path_skips_resolution— relative path resolves againstcurrent_dirinstead of parent's cwdNote: The fix and tests are in
darwin.rswhich is macOS-only. The Linux path usessmol::process::Command(via fork+execve) and is unaffected.Self-Review Checklist:
Release Notes: