@@ -97,4 +97,55 @@ describe("exec allowlist matching", () => {
9797 expect ( matchAllowlist ( [ { pattern } ] , resolution ) ?. pattern ) . toBe ( pattern ) ;
9898 }
9999 } ) ;
100+
101+ describe ( "symlink/realpath dual matching (#45595)" , ( ) => {
102+ const symlinkResolution = {
103+ rawExecutable : "rg" ,
104+ resolvedPath : "/opt/homebrew/bin/rg" ,
105+ resolvedRealPath : "/opt/homebrew/Cellar/ripgrep/14.1.1/bin/rg" ,
106+ executableName : "rg" ,
107+ } ;
108+
109+ it ( "matches when the allowlist entry pins the real-binary canonical path" , ( ) => {
110+ // Operator allowlists the real binary (the path execution actually pins
111+ // to via fs.realpath). The PATH-resolved symlink does not literally match
112+ // the pattern, but the realpath should still satisfy the entry — otherwise
113+ // every Homebrew/nix/asdf-style binary on the host is unnecessarily blocked.
114+ const match = matchAllowlist (
115+ [ { pattern : "/opt/homebrew/Cellar/ripgrep/14.1.1/bin/rg" } ] ,
116+ symlinkResolution ,
117+ ) ;
118+ expect ( match ?. pattern ) . toBe ( "/opt/homebrew/Cellar/ripgrep/14.1.1/bin/rg" ) ;
119+ } ) ;
120+
121+ it ( "still matches when the allowlist entry pins the symlink path" , ( ) => {
122+ // Backward compatibility: existing allowlists that pin the symlink keep
123+ // working (the resolvedPath branch fires first, so resolvedRealPath is
124+ // not even consulted).
125+ const match = matchAllowlist ( [ { pattern : "/opt/homebrew/bin/rg" } ] , symlinkResolution ) ;
126+ expect ( match ?. pattern ) . toBe ( "/opt/homebrew/bin/rg" ) ;
127+ } ) ;
128+
129+ it ( "does not match when neither path is in the allowlist" , ( ) => {
130+ const match = matchAllowlist (
131+ [ { pattern : "/usr/local/bin/something-else" } ] ,
132+ symlinkResolution ,
133+ ) ;
134+ expect ( match ) . toBeNull ( ) ;
135+ } ) ;
136+
137+ it ( "does not double-consult realpath when the symlink and realpath are identical" , ( ) => {
138+ // Resolution where realpath === resolvedPath (no symlink indirection).
139+ // Match should succeed via the resolvedPath branch and not loop on the
140+ // realpath branch (which is short-circuited via the strict-equality guard).
141+ const direct = {
142+ rawExecutable : "ls" ,
143+ resolvedPath : "/usr/bin/ls" ,
144+ resolvedRealPath : "/usr/bin/ls" ,
145+ executableName : "ls" ,
146+ } ;
147+ const match = matchAllowlist ( [ { pattern : "/usr/bin/ls" } ] , direct ) ;
148+ expect ( match ?. pattern ) . toBe ( "/usr/bin/ls" ) ;
149+ } ) ;
150+ } ) ;
100151} ) ;
0 commit comments