fix(exec/approvals): match executable realpath against allowlist patterns (#45595)#73543
fix(exec/approvals): match executable realpath against allowlist patterns (#45595)#73543johnstormentswe wants to merge 1 commit into
Conversation
Greptile SummaryThis PR fixes a path-matching asymmetry in Confidence Score: 5/5Safe to merge — the change is minimal, well-isolated, and comprehensively tested. Single-area diff touching one conditional expression; the equality guard correctly short-circuits the extra match call when no symlink indirection exists; all existing tests continue to pass unchanged; four new targeted tests cover the added branch exhaustively; no public API changes. No files require special attention. Reviews (1): Last reviewed commit: "fix(exec/approvals): match executable re..." | Re-trigger Greptile |
|
Codex review: needs real behavior proof before merge. Summary Reproducibility: yes. A high-confidence source reproduction is an Real behavior proof Next step before merge Security Review detailsBest possible solution: Land or replace this with a maintainer-approved executable identity fix that aligns matching, trust checks, persistence, audit metadata, and execution, with focused regression coverage and real setup proof. Do we have a high-confidence way to reproduce the issue? Yes. A high-confidence source reproduction is an Is this the best way to solve the issue? Unclear as a complete fix. The fallback is narrow and useful for real-path allowlist misses, but the final policy still needs to decide whether symlink-path approvals may continue to authorize a changed real executable and whether the linked issue should remain open for the other affected surfaces. Security concerns:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 48b4e5b3614f. |
8293123 to
4a9635f
Compare
4a9635f to
704edc5
Compare
|
Hi @steipete |
|
@clawsweeper authmerge |
|
Hi @clawsweeper Could you merge this PR? |
|
Thanks for the report and patch here. We landed a maintainer rewrite in #82825 as 524185a, keeping the same fix direction but broadening it across path-shaped allowlists, safe-bin trust, skill auto-allow, Allow Always persistence, and approval audit metadata, with symlink-retargeting regression coverage. Closing this PR as superseded by the landed fix. Thanks again for chasing this down. |
What
matchAllowlistinsrc/infra/exec-command-resolution.tscompares allowlist patterns againstresolution.resolvedPathonly — the PATH-resolved entry point — while the runtime later pins execution toresolution.resolvedRealPath(viafs.realpath). On any host where binaries are reached through a symlink (Homebrew, nix, asdf, etc.) the operator gets exactly one of two bad outcomes:/opt/homebrew/Cellar/ripgrep/14.1.1/bin/rg) → matching always misses because the resolution carries the symlink path/opt/homebrew/bin/rg. Every Homebrew rg invocation is unnecessarily blocked./opt/homebrew/bin/rg) → matching succeeds, but if the symlink target later changes (Homebrew bumps the Cellar path), the same approval keeps firing while a different real binary actually runs.The reporter at #45595 documented both directions and pointed at the exact mismatch.
Closes #45595.
Why this fix
Match each path-shaped allowlist pattern against both
resolvedPathand (when distinct)resolvedRealPath. The runtime always executes the realpath, so:The realpath check is short-circuited via a strict-equality guard so resolutions where
resolvedRealPath === resolvedPath(no symlink indirection) do not pay any extra work.The bare-
*wildcard branch is untouched. Basename-only matching (non-path-shaped patterns) is untouched. argPattern path is untouched. Only the path-shaped pattern check at line ~376 picks up the realpath fallback.Tests
Added
describe("symlink/realpath dual matching (#45595)")block in src/infra/exec-allowlist-matching.test.ts:/opt/homebrew/Cellar/ripgrep/14.1.1/bin/rg, resolution carries/opt/homebrew/bin/rgsymlink + that realpath, allowlist hits via the realpath branch. (The bug's primary case — was returningnullbefore this fix.)The existing 8 tests in the same file continue to pass unchanged because they all use resolutions without
resolvedRealPath— the new branch is a no-op for them.Notes
src/infra/exec-command-resolution.ts(one new local + one combined||) and a colocated test addition, plus a one-line CHANGELOG entry under## Unreleased### FixeswithThanks @juan-flores077.ExecutableResolution.resolvedRealPathalready existed; this fix just consults it where the matcher previously did not.Validation
Local CI is constrained on this machine; relying on CI for the canonical proof. I have:
matchAllowlistis the only matcher consulting allowlist patterns fortools.exec.allowlist(the only other path-shaped consumer ofExecutableResolution.resolvedPathisresolveAllowlistCandidatePathwhich produces a display path for diagnostics, not a match decision).exec-allowlist-matching.test.tsuse resolutions withoutresolvedRealPath, so the new branch is path-isolated and the existing assertions are unchanged.Happy to address Greptile/Codex review feedback or extend the match to also normalize
../.segments viapath.resolveif reviewers want defense in depth.Suggested CHANGELOG entry
This PR intentionally does not touch
CHANGELOG.mdto avoid hot-file rebase conflicts that have been closing rebased fix PRs. Maintainers can drop the following line under## Unreleased > ### Fixesat merge time:realpathagainsttools.exec.allowlistpatterns so operators on Homebrew/nix/asdf systems can allowlist either the symlink or the real-binary canonical path without one mismatching the other; the runtime always pins execution to the realpath, so approvals stay tied to the binary that actually runs. Fixes [Bug]: Exec approval checks use symlink path but execution uses real path #45595. Thanks @juan-flores077.