Describe the bug
pants check (and any goal that needs find_binary via the nodejs subsystem) crashes with IntrinsicError: Operation not permitted (os error 1) when one or more $PATH entries return EPERM (or EACCES) on stat/symlink_metadata. The pattern is identical to #21990 / #22519: a system-binary lookup fans path_metadata_request over every $PATH entry; the Rust VFS only maps NotFound (and, post-#23327, NotADirectory) to Ok(None); any other io::Error propagates and aborts the goal — including PermissionDenied, which is what happens when running pants inside a process sandbox that blocks read on some $PATH directories.
Engine traceback:
in `check` goal
IntrinsicError: Operation not permitted (os error 1)
fix, fmt, lint, hadolint, yamllint, and ruff pass; only goals that reach the nodejs shim path fan-out (e.g. check with pyright) crash.
Pants version
2.31.0 (current main / 2.31.x HEAD has the same unhandled branch — see code refs below).
OS
macOS (Darwin 25.5.0), running under a userspace process sandbox (nono) that denies read on a handful of $PATH directories.
Root cause
Pants' local find_binary uses an enumerate-all path_metadata_request fan-out across every $PATH entry. The Rust VFS only swallows ErrorKind::NotFound and ErrorKind::NotADirectory; any other io::Error — including PermissionDenied (EPERM/EACCES) — becomes an IntrinsicError that aborts the goal before the requested binary is ever resolved.
Code path (verified against main @ commit 5dd30b2f70):
src/python/pants/backend/javascript/subsystems/nodejs.py:416-420 — get_nodejs_process_tools_shims fans find_binary over (sh, bash, mkdir, rm, touch, which, sed, dirname, uname, …) via concurrently().
src/python/pants/core/util_rules/system_binaries.py:731-740 — local env takes _find_candidate_paths_via_path_metadata_lookups.
src/python/pants/core/util_rules/system_binaries.py:606-614 — concurrently(path_metadata_request(...) for path in search_path). No error tolerance.
src/rust/engine/src/nodes/path_metadata.rs:33-42 — throw(format!(\"{e}\")) on any VFS error.
src/rust/fs/src/posixfs.rs (lines ~285-290 on main) — only ErrorKind::NotFound and ErrorKind::NotADirectory map to Ok(None); everything else (including PermissionDenied) falls through Err(err) => Err(err).
#23327 (merged, in 2.31.1rc0) added the ENOTDIR branch but did not touch PermissionDenied. #22519 (still open) is almost certainly the same underlying bug, manifesting as os error 13 (EACCES) on a Docker-binary lookup rather than os error 1 (EPERM) on a nodejs-shim lookup.
Reproduction
Minimal repro using chmod (no sandbox required):
mkdir -p /tmp/eperm-path
chmod 000 /tmp/eperm-path
PATH=\"/tmp/eperm-path:\$PATH\" pants --no-pantsd check ::
The goal fails with IntrinsicError: Permission denied (os error 13) even though /tmp/eperm-path contains nothing pants needs.
Original observation (sandboxed): $PATH contained three entries the sandbox denied read on (~/.local/share/nvim/mason/bin, ~/.scripts, ~/.volta/bin). Stripping them from $PATH before invoking pants makes pants --no-pantsd check libs/<...>:: succeed (pyright runs cleanly on 37 files).
Suggested fix
Treat PermissionDenied the same way NotFound and NotADirectory are already treated in PosixFS::path_metadata:
Err(err) if err.kind() == ErrorKind::NotFound => Ok(None),
Err(err) if err.kind() == ErrorKind::NotADirectory => Ok(None),
Err(err) if err.kind() == ErrorKind::PermissionDenied => Ok(None),
Err(err) => Err(err),
Rationale: an unreadable $PATH entry is, from the perspective of the binary search, equivalent to a non-existent one — the caller cannot list it, cannot stat its contents, and therefore cannot find a binary in it. Aborting the entire goal for one unreadable entry is strictly worse than skipping it. The find_binary flow already silently tolerates $PATH entries that don't exist or aren't directories; tolerating ones it can't read is the same logical category. Worth considering whether to also handle this at a higher level (e.g. tolerate per-entry errors in _find_candidate_paths_via_path_metadata_lookups rather than relying on the VFS to mask them), but the minimal Rust-side fix is sufficient to unblock sandboxed environments.
Workarounds
- Strip the offending entries from
\$PATH before invoking pants.
- Pin
[nodejs] search_path (and equivalent options for other binary lookups) in pants.toml to an explicit list of readable directories instead of relying on <PATH>.
- Grant the sandbox read access to the relevant
\$PATH directories.
Related
Describe the bug
pants check(and any goal that needsfind_binaryvia the nodejs subsystem) crashes withIntrinsicError: Operation not permitted (os error 1)when one or more$PATHentries returnEPERM(orEACCES) onstat/symlink_metadata. The pattern is identical to #21990 / #22519: a system-binary lookup fanspath_metadata_requestover every$PATHentry; the Rust VFS only mapsNotFound(and, post-#23327,NotADirectory) toOk(None); any otherio::Errorpropagates and aborts the goal — includingPermissionDenied, which is what happens when running pants inside a process sandbox that blocks read on some$PATHdirectories.fix,fmt,lint, hadolint, yamllint, and ruff pass; only goals that reach the nodejs shim path fan-out (e.g.checkwith pyright) crash.Pants version
2.31.0 (current
main/2.31.xHEAD has the same unhandled branch — see code refs below).OS
macOS (Darwin 25.5.0), running under a userspace process sandbox (
nono) that denies read on a handful of$PATHdirectories.Root cause
Pants' local
find_binaryuses an enumerate-allpath_metadata_requestfan-out across every$PATHentry. The Rust VFS only swallowsErrorKind::NotFoundandErrorKind::NotADirectory; any otherio::Error— includingPermissionDenied(EPERM/EACCES) — becomes anIntrinsicErrorthat aborts the goal before the requested binary is ever resolved.Code path (verified against
main@ commit5dd30b2f70):src/python/pants/backend/javascript/subsystems/nodejs.py:416-420—get_nodejs_process_tools_shimsfansfind_binaryover (sh, bash, mkdir, rm, touch, which, sed, dirname, uname, …) viaconcurrently().src/python/pants/core/util_rules/system_binaries.py:731-740— local env takes_find_candidate_paths_via_path_metadata_lookups.src/python/pants/core/util_rules/system_binaries.py:606-614—concurrently(path_metadata_request(...) for path in search_path). No error tolerance.src/rust/engine/src/nodes/path_metadata.rs:33-42—throw(format!(\"{e}\"))on any VFS error.src/rust/fs/src/posixfs.rs(lines ~285-290 onmain) — onlyErrorKind::NotFoundandErrorKind::NotADirectorymap toOk(None); everything else (includingPermissionDenied) falls throughErr(err) => Err(err).#23327 (merged, in 2.31.1rc0) added the
ENOTDIRbranch but did not touchPermissionDenied. #22519 (still open) is almost certainly the same underlying bug, manifesting asos error 13(EACCES) on a Docker-binary lookup rather thanos error 1(EPERM) on a nodejs-shim lookup.Reproduction
Minimal repro using
chmod(no sandbox required):The goal fails with
IntrinsicError: Permission denied (os error 13)even though/tmp/eperm-pathcontains nothing pants needs.Original observation (sandboxed):
$PATHcontained three entries the sandbox denied read on (~/.local/share/nvim/mason/bin,~/.scripts,~/.volta/bin). Stripping them from$PATHbefore invoking pants makespants --no-pantsd check libs/<...>::succeed (pyright runs cleanly on 37 files).Suggested fix
Treat
PermissionDeniedthe same wayNotFoundandNotADirectoryare already treated inPosixFS::path_metadata:Rationale: an unreadable
$PATHentry is, from the perspective of the binary search, equivalent to a non-existent one — the caller cannot list it, cannot stat its contents, and therefore cannot find a binary in it. Aborting the entire goal for one unreadable entry is strictly worse than skipping it. Thefind_binaryflow already silently tolerates$PATHentries that don't exist or aren't directories; tolerating ones it can't read is the same logical category. Worth considering whether to also handle this at a higher level (e.g. tolerate per-entry errors in_find_candidate_paths_via_path_metadata_lookupsrather than relying on the VFS to mask them), but the minimal Rust-side fix is sufficient to unblock sandboxed environments.Workarounds
\$PATHbefore invoking pants.[nodejs] search_path(and equivalent options for other binary lookups) inpants.tomlto an explicit list of readable directories instead of relying on<PATH>.\$PATHdirectories.Related
EACCES/ os error 13).system_binary#23327 — same code path, fixed theENOTDIRshape; this issue is the unaddressedPermissionDeniedshape.