From f1a4cfd2437794a74ba2292468a0c476c914b27a Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Thu, 6 Aug 2026 23:23:36 +0200 Subject: [PATCH 1/3] fix: resolve symlinks in sh shims before computing basedir POSIX shells set $0 to the path of the invoked symlink, not the shim it points at. When a shim was reached through an external symlink (for example one placed on PATH), basedir resolved to the symlink's directory and the relative target path into node_modules broke. Follow the symlink chain with readlink before deriving basedir. Fixes pnpm/pnpm#13405 Co-Authored-By: Claude Fable 5 --- src/index.ts | 22 +++++- test/e2e.test.js | 33 +++++++++ test/test.js.snapshot | 165 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 203 insertions(+), 17 deletions(-) diff --git a/src/index.ts b/src/index.ts index 92e2a59..a568b1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -469,7 +469,16 @@ function generateShShim (src: string, to: string, opts: InternalOptions): string let progArgs = opts.progArgs ? `${opts.progArgs.join(` `)} ` : '' // #!/bin/sh - // basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + // # Resolve $0 through symlinks so basedir is the shim's real directory. + // link="$0" + // while [ -L "$link" ]; do + // target=$(readlink "$link") + // case "$target" in + // /*) link="$target" ;; + // *) link="$(dirname "$link")/$target" ;; + // esac + // done + // basedir=$(dirname "$(echo "$link" | sed -e 's,\\,/,g')") // basedir_win="$basedir" // exe="" // msys="" @@ -510,7 +519,16 @@ function generateShShim (src: string, to: string, opts: InternalOptions): string let sh = `\ #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" diff --git a/test/e2e.test.js b/test/e2e.test.js index 514fdbb..4c8abbb 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -108,3 +108,36 @@ describeOnPosix('sh shim uses POSIX runtime from PATH', () => { assert.doesNotMatch(r.stderr, /node\.exe/) }) }) + +describeOnPosix('sh shim invoked through a chain of external symlinks', () => { + // POSIX shells set $0 to the invoked symlink, not the shim it points at, + // so the shim must follow the chain before deriving basedir + // (https://github.com/pnpm/pnpm/issues/13405). + test('resolves basedir from the shim\'s real directory and runs the target', async () => { + const tempDir = tempy.directory() + const binDir = path.join(tempDir, 'node_modules', '.bin') + const targetDir = path.join(tempDir, 'node_modules', 'typescript', 'bin') + fs.mkdirSync(binDir, { recursive: true }) + fs.mkdirSync(targetDir, { recursive: true }) + + const target = path.join(targetDir, 'tsc') + fs.writeFileSync(target, '#!/bin/sh\necho "tsc-output"\n', 'utf8') + fs.chmodSync(target, 0o755) + + const shim = path.join(binDir, 'tsc') + await cmdShim(target, shim) + + const hop1 = path.join(tempDir, 'symlink_hop_1') + const hop2 = path.join(tempDir, 'symlink_hop_2') + fs.symlinkSync(shim, hop1) + fs.symlinkSync(hop1, hop2) + + const r = spawnSync(hop2, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + + assert.equal(r.status, 0, `shim exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) + assert.equal(r.stdout.trim(), 'tsc-output') + }) +}) diff --git a/test/test.js.snapshot b/test/test.js.snapshot index 0dfcfff..99673a1 100644 --- a/test/test.js.snapshot +++ b/test/test.js.snapshot @@ -1,7 +1,16 @@ exports[`batch script > shim files > bat.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -104,7 +113,16 @@ exit $ret exports[`custom node executable > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -165,7 +183,16 @@ exit $LASTEXITCODE exports[`env shebang > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -252,7 +279,16 @@ exit $ret exports[`env shebang with NODE_PATH > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -362,7 +398,16 @@ exit $ret exports[`env shebang with PATH extending > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -461,7 +506,16 @@ exit $ret exports[`env shebang with args > shim files > env.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -548,7 +602,16 @@ exit $ret exports[`env shebang with default args > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -635,7 +698,16 @@ exit $ret exports[`env shebang with no NODE_PATH > shim files > env.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -722,7 +794,16 @@ exit $ret exports[`explicit shebang > shim files > sh.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -809,7 +890,16 @@ exit $ret exports[`explicit shebang with args > shim files > sh.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -896,7 +986,16 @@ exit $ret exports[`explicit shebang with args, linking to another drive on Windows > shim files > sh.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -983,7 +1082,16 @@ exit $ret exports[`explicit shebang with prog args > shim files > sh.args.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -1070,7 +1178,16 @@ exit $ret exports[`no cmd file > shim files > exe.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -1124,7 +1241,16 @@ exit $LASTEXITCODE exports[`no shebang > shim files > exe.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" @@ -1185,7 +1311,16 @@ exit $LASTEXITCODE exports[`shebang with -S > shim files > from.env.s.shim 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" From 77feda88ceee3f8a4600bd0fc856f84e40c0d6ba Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Fri, 7 Aug 2026 11:09:49 +0200 Subject: [PATCH 2/3] test: update Windows-recorded e2e shim snapshot for symlink resolution The .exe-target sh shim snapshot is only exercised on Windows, so the Linux snapshot regeneration could not update it. Co-Authored-By: Claude Fable 5 --- test/e2e.test.js.snapshot | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/e2e.test.js.snapshot b/test/e2e.test.js.snapshot index 6490475..5cc2b15 100644 --- a/test/e2e.test.js.snapshot +++ b/test/e2e.test.js.snapshot @@ -1,7 +1,16 @@ exports[`create a command shim for a .exe file > shim files 1`] = ` #!/bin/sh -basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") +# Resolve $0 through symlinks so basedir is the shim's real directory. +link="$0" +while [ -L "$link" ]; do + target=$(readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="$(dirname "$link")/$target" ;; + esac +done +basedir=$(dirname "$(echo "$link" | sed -e 's,\\\\,/,g')") basedir_win="$basedir" exe="" msys="" From 7c7ba0465d8eca0faef930d2dbb2819f1a461c45 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Fri, 7 Aug 2026 11:29:48 +0200 Subject: [PATCH 3/3] fix: cap symlink hops at the kernel ELOOP limit and widen e2e coverage The cap makes a symlink cycle rewritten mid-execution terminate instead of looping forever. New e2e tests cover relative-target hops (the dirname-composition branch), directories with spaces, and a node-shebang bin invoked through a chain. Co-Authored-By: Claude Fable 5 --- src/index.ts | 10 ++++-- test/e2e.test.js | 71 +++++++++++++++++++++++++++++++----- test/e2e.test.js.snapshot | 5 ++- test/test.js.snapshot | 75 +++++++++++++++++++++++++++++++-------- 4 files changed, 135 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index a568b1e..a335b88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -470,8 +470,11 @@ function generateShShim (src: string, to: string, opts: InternalOptions): string // #!/bin/sh // # Resolve $0 through symlinks so basedir is the shim's real directory. + // # Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. // link="$0" - // while [ -L "$link" ]; do + // hops=0 + // while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + // hops=$((hops+1)) // target=$(readlink "$link") // case "$target" in // /*) link="$target" ;; @@ -520,8 +523,11 @@ function generateShShim (src: string, to: string, opts: InternalOptions): string let sh = `\ #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; diff --git a/test/e2e.test.js b/test/e2e.test.js index 4c8abbb..1fd97f4 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -113,8 +113,7 @@ describeOnPosix('sh shim invoked through a chain of external symlinks', () => { // POSIX shells set $0 to the invoked symlink, not the shim it points at, // so the shim must follow the chain before deriving basedir // (https://github.com/pnpm/pnpm/issues/13405). - test('resolves basedir from the shim\'s real directory and runs the target', async () => { - const tempDir = tempy.directory() + const makeShimmedTool = async (tempDir) => { const binDir = path.join(tempDir, 'node_modules', '.bin') const targetDir = path.join(tempDir, 'node_modules', 'typescript', 'bin') fs.mkdirSync(binDir, { recursive: true }) @@ -126,18 +125,74 @@ describeOnPosix('sh shim invoked through a chain of external symlinks', () => { const shim = path.join(binDir, 'tsc') await cmdShim(target, shim) + return shim + } + + const runShim = (cmd) => { + const r = spawnSync(cmd, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + assert.equal(r.status, 0, `shim exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) + return r.stdout.trim() + } + + test('follows a chain of absolute symlinks', async () => { + const tempDir = tempy.directory() + const shim = await makeShimmedTool(tempDir) const hop1 = path.join(tempDir, 'symlink_hop_1') const hop2 = path.join(tempDir, 'symlink_hop_2') fs.symlinkSync(shim, hop1) fs.symlinkSync(hop1, hop2) - const r = spawnSync(hop2, { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }) + assert.equal(runShim(hop2), 'tsc-output') + }) - assert.equal(r.status, 0, `shim exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) - assert.equal(r.stdout.trim(), 'tsc-output') + test('follows relative symlinks from another directory', async () => { + const tempDir = tempy.directory() + await makeShimmedTool(tempDir) + + // Both hops use relative targets, exercising the shim's + // dirname-composition branch rather than the absolute one. + const localBin = path.join(tempDir, 'usr', 'local', 'bin') + fs.mkdirSync(localBin, { recursive: true }) + const hop1 = path.join(tempDir, 'usr', 'tsc') + fs.symlinkSync(path.join('..', 'node_modules', '.bin', 'tsc'), hop1) + const hop2 = path.join(localBin, 'tsc') + fs.symlinkSync(path.join('..', '..', 'tsc'), hop2) + + assert.equal(runShim(hop2), 'tsc-output') + }) + + test('resolves symlinks when directories contain spaces', async () => { + const tempDir = path.join(tempy.directory(), 'dir with spaces') + fs.mkdirSync(tempDir, { recursive: true }) + await makeShimmedTool(tempDir) + + const hopDir = path.join(tempDir, 'link dir') + fs.mkdirSync(hopDir) + const hop = path.join(hopDir, 'tsc') + fs.symlinkSync(path.join('..', 'node_modules', '.bin', 'tsc'), hop) + + assert.equal(runShim(hop), 'tsc-output') + }) + + test('runs a node-shebang bin through a symlink chain', async () => { + const tempDir = tempy.directory() + const binDir = path.join(tempDir, 'node_modules', '.bin') + const pkgDir = path.join(tempDir, 'node_modules', 'tool') + fs.mkdirSync(binDir, { recursive: true }) + fs.mkdirSync(pkgDir, { recursive: true }) + + const target = path.join(pkgDir, 'cli.js') + fs.writeFileSync(target, '#!/usr/bin/env node\nconsole.log("NODE_BIN_OK")\n', 'utf8') + const shim = path.join(binDir, 'tool') + await cmdShim(target, shim, { createCmdFile: false }) + + const hop = path.join(tempDir, 'tool') + fs.symlinkSync(path.join('node_modules', '.bin', 'tool'), hop) + + assert.equal(runShim(hop), 'NODE_BIN_OK') }) }) diff --git a/test/e2e.test.js.snapshot b/test/e2e.test.js.snapshot index 5cc2b15..20db918 100644 --- a/test/e2e.test.js.snapshot +++ b/test/e2e.test.js.snapshot @@ -2,8 +2,11 @@ exports[`create a command shim for a .exe file > shim files 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; diff --git a/test/test.js.snapshot b/test/test.js.snapshot index 99673a1..9b29f19 100644 --- a/test/test.js.snapshot +++ b/test/test.js.snapshot @@ -2,8 +2,11 @@ exports[`batch script > shim files > bat.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -114,8 +117,11 @@ exports[`custom node executable > shim files > env.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -184,8 +190,11 @@ exports[`env shebang > shim files > env.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -280,8 +289,11 @@ exports[`env shebang with NODE_PATH > shim files > env.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -399,8 +411,11 @@ exports[`env shebang with PATH extending > shim files > env.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -507,8 +522,11 @@ exports[`env shebang with args > shim files > env.args.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -603,8 +621,11 @@ exports[`env shebang with default args > shim files > env.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -699,8 +720,11 @@ exports[`env shebang with no NODE_PATH > shim files > env.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -795,8 +819,11 @@ exports[`explicit shebang > shim files > sh.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -891,8 +918,11 @@ exports[`explicit shebang with args > shim files > sh.args.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -987,8 +1017,11 @@ exports[`explicit shebang with args, linking to another drive on Windows > shim #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -1083,8 +1116,11 @@ exports[`explicit shebang with prog args > shim files > sh.args.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -1179,8 +1215,11 @@ exports[`no cmd file > shim files > exe.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -1242,8 +1281,11 @@ exports[`no shebang > shim files > exe.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;; @@ -1312,8 +1354,11 @@ exports[`shebang with -S > shim files > from.env.s.shim 1`] = ` #!/bin/sh # Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. link="$0" -while [ -L "$link" ]; do +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) target=$(readlink "$link") case "$target" in /*) link="$target" ;;