Self-checks
Subject of the issue
The pnpm standalone-executable release tarballs published to GitHub Releases (pnpm-{darwin,linux}-{x64,arm64}.tar.gz for v11.0.0 and v11.0.1) contain four broken absolute symlinks under dist/node_modules/.bin/ whose targets point at the GitHub Actions runner's filesystem path, not anywhere on the user's machine:
$ curl -sL https://github.com/pnpm/pnpm/releases/download/v11.0.1/pnpm-darwin-arm64.tar.gz | tar -tvz | grep '^l'
lrwxrwxrwx 0 runner 1001 0 Apr 29 17:07 dist/node_modules/.bin/node-gyp -> /home/runner/work/pnpm/pnpm/pnpm/dist/node_modules/node-gyp/bin/node-gyp.js
lrwxrwxrwx 0 runner 1001 0 Apr 29 17:07 dist/node_modules/.bin/node-which -> /home/runner/work/pnpm/pnpm/pnpm/dist/node_modules/which/bin/which.js
lrwxrwxrwx 0 runner 1001 0 Apr 29 17:07 dist/node_modules/.bin/nopt -> /home/runner/work/pnpm/pnpm/pnpm/dist/node_modules/nopt/bin/nopt.js
lrwxrwxrwx 0 runner 1001 0 Apr 29 17:07 dist/node_modules/.bin/semver -> /home/runner/work/pnpm/pnpm/pnpm/dist/node_modules/semver/bin/semver.js
Lenient extractors (system tar, install.sh, npm i -g @pnpm/exe) silently extract these as dangling symlinks; nothing at runtime ever traverses them, so the bug is invisible during normal use. Strict extractors that perform path-traversal validation (e.g. hermit) refuse to extract, breaking installation:
fatal:hermit: .../dist/node_modules/.bin/node-gyp: illegal symlink target
"/home/runner/work/pnpm/pnpm/pnpm/dist/node_modules/node-gyp/bin/node-gyp.js"
(resolves to /home/runner/work/... which is outside ...)
Expected behaviour
The symlinks under dist/node_modules/.bin/ should be relative (e.g. ../node-gyp/bin/node-gyp.js), so they remain valid at any extraction location and don't leak the build host's filesystem layout.
Actual behaviour
They are absolute paths into the GitHub Actions runner's working directory (/home/runner/work/pnpm/pnpm/pnpm/...), which:
- Are dangling on every user's machine (a supply-chain smell)
- Cause strict tarball extractors to refuse the archive
- Leak build-host paths into a public artifact
Root cause
pnpm/artifacts/exe/scripts/build-artifacts.ts#L48:
fs.cpSync(distSrc, distDest, { recursive: true })
Node's fs.cpSync defaults to verbatimSymlinks: false, which resolves relative symlinks into absolute paths at the source filesystem location during the copy. This is a well-known behaviour, see nodejs/node#41693 and the Node.js docs:
verbatimSymlinks <boolean> When true, path resolution for symlinks will be skipped. Default: false.
So a relative symlink in the source pnpm checkout (dist/node_modules/.bin/node-gyp -> ../node-gyp/bin/node-gyp.js) becomes absolute (-> /home/runner/work/pnpm/pnpm/pnpm/dist/node_modules/node-gyp/bin/node-gyp.js) in the destination directory that gets archived.
Steps to reproduce
curl -sL https://github.com/pnpm/pnpm/releases/download/v11.0.1/pnpm-darwin-arm64.tar.gz \
| tar -tvz | grep '^l'
(Same result for pnpm-darwin-x64, pnpm-linux-x64, pnpm-linux-arm64 archives in both v11.0.0 and v11.0.1.)
Spot-check a v10.x release for comparison: v10.x ships bare binaries (no tarball, no dist/ directory in the artifact), so this regression is specific to v11+.
Suggested fix
One-line change in pnpm/artifacts/exe/scripts/build-artifacts.ts:
- fs.cpSync(distSrc, distDest, { recursive: true })
+ fs.cpSync(distSrc, distDest, { recursive: true, verbatimSymlinks: true })
Would also recommend adding a regression check to the release workflow that walks the packaged dist/ and fails if any symlink target is absolute or resolves outside the archive root.
I'd be happy to send a PR if that's useful.
Environment
- pnpm version: 11.0.0, 11.0.1 (release tarballs)
- Node.js: N/A (issue is in build artifacts, not runtime)
- Tested extraction with: hermit (rejects), system
tar (extracts as dangling)
🤖 Issue drafted with Amp assistance after reproducing the failure mode and tracing the root cause.
Self-checks
verbatimSymlinks, absolute symlinks, or/home/runnerin tarballsSubject of the issue
The
pnpmstandalone-executable release tarballs published to GitHub Releases (pnpm-{darwin,linux}-{x64,arm64}.tar.gzfor v11.0.0 and v11.0.1) contain four broken absolute symlinks underdist/node_modules/.bin/whose targets point at the GitHub Actions runner's filesystem path, not anywhere on the user's machine:Lenient extractors (system
tar,install.sh,npm i -g @pnpm/exe) silently extract these as dangling symlinks; nothing at runtime ever traverses them, so the bug is invisible during normal use. Strict extractors that perform path-traversal validation (e.g. hermit) refuse to extract, breaking installation:Expected behaviour
The symlinks under
dist/node_modules/.bin/should be relative (e.g.../node-gyp/bin/node-gyp.js), so they remain valid at any extraction location and don't leak the build host's filesystem layout.Actual behaviour
They are absolute paths into the GitHub Actions runner's working directory (
/home/runner/work/pnpm/pnpm/pnpm/...), which:Root cause
pnpm/artifacts/exe/scripts/build-artifacts.ts#L48:Node's
fs.cpSyncdefaults toverbatimSymlinks: false, which resolves relative symlinks into absolute paths at the source filesystem location during the copy. This is a well-known behaviour, see nodejs/node#41693 and the Node.js docs:So a relative symlink in the source pnpm checkout (
dist/node_modules/.bin/node-gyp -> ../node-gyp/bin/node-gyp.js) becomes absolute (-> /home/runner/work/pnpm/pnpm/pnpm/dist/node_modules/node-gyp/bin/node-gyp.js) in the destination directory that gets archived.Steps to reproduce
(Same result for
pnpm-darwin-x64,pnpm-linux-x64,pnpm-linux-arm64archives in both v11.0.0 and v11.0.1.)Spot-check a v10.x release for comparison: v10.x ships bare binaries (no tarball, no
dist/directory in the artifact), so this regression is specific to v11+.Suggested fix
One-line change in
pnpm/artifacts/exe/scripts/build-artifacts.ts:Would also recommend adding a regression check to the release workflow that walks the packaged
dist/and fails if any symlink target is absolute or resolves outside the archive root.I'd be happy to send a PR if that's useful.
Environment
tar(extracts as dangling)🤖 Issue drafted with Amp assistance after reproducing the failure mode and tracing the root cause.