Skip to content

Vendored pnpm 7/8 writes the absolute file: specifier unquoted, so a project path containing # or : breaks every frozen install while vendor, vendor --check and vex report success #754

Description

[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).

Summary

On pnpm 7 and 8 locks (lockfile 5.4 / 6.0), vendored mode rewrites the root dependency's specifier to the machine-absolute file:<project root>/.socket/vendor/...tgz spelling that pnpm itself records. It splices that value into pnpm-lock.yaml as a plain YAML scalar with no quoting. When the project path contains a YAML indicator sequence, the lock no longer says what socket-patch meant:

  • # (for example ~/src/My Project #2/): YAML reads everything after # as a comment. pnpm sees the specifier file:/…/My Project and refuses with ERR_PNPM_OUTDATED_LOCKFILE.
  • : (for example colon: x): the line is no longer valid YAML. pnpm fails with ERR_PNPM_BROKEN_LOCKFILE … bad indentation of a mapping entry.

pnpm writes the same value single-quoted when it generates the lock itself. In the same directory, pnpm install from the vendored package.json produces specifier: 'file:/…/hash #x/.socket/vendor/…/left-pad-1.3.0.tgz'.

Impact

  • scan --mode vendored / vendor report success, and the checkout is then uninstallable with --frozen-lockfile (the CI default) at the same path, not only in a moved checkout. That contradicts the vendor_pnpm_legacy_absolute_specifier caveat, which promises that the lock works at the recorded path.
  • vendor --check reports vendor_check_ok ("committed artifact and wiring verified") on the broken lock.
  • vex attests not_affected for a project that can't be installed from its lock.
  • vendor --revert restores the original correctly. Only the forward write is wrong.

Repro (Linux, pnpm 8.15.9; 7.33.7 is the same)

D="/tmp/w/hash #x"; mkdir -p "$D" && cd "$D"
printf '{"name":"proj","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}\n' > package.json
pnpm install
socket-patch scan --mode vendored --json --yes      # status: success (any left-pad@1.3.0 patch; a mock API was used)
grep specifier pnpm-lock.yaml
#   specifier: file:/tmp/w/hash #x/.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz   <- unquoted
rm -rf node_modules
pnpm install --frozen-lockfile --offline
#  ERR_PNPM_OUTDATED_LOCKFILE ... specifiers in the lockfile ({"left-pad":"file:/tmp/w/hash"}) don't match specs in package.json
#  (the lock value is truncated at " #")
socket-patch vendor --check --json                   # status: success, vendor_check_ok
socket-patch vex --output v.json                     # not_affected

On a 5.4 lock (pnpm 7.33.7), the line written is specifiers: → left-pad: file:/…/hash #x/.socket/vendor/…tgz, which fails the same way.

Expected vs actual

  • Expected: the vendored lock round-trips through pnpm's own YAML reader to the exact specifier pnpm would record. docs/ecosystems.md and the vendor_pnpm_legacy_absolute_specifier warning say that a frozen install works in a checkout at the recorded path. CLI_CONTRACT: a reported success must leave a project that installs patched.
  • Actual: the value is written unquoted, so YAML truncates or rejects it. vendor --check and vex don't notice.

Matrix (Linux, main 045d7ec; each cell = vendored scan, then a fresh --frozen-lockfile --offline install)

project dir name pnpm 7.33.7 (5.4) pnpm 8.15.9 (6.0)
plain dir pass pass
ünïcode pass pass
a'quote, [br], x#y pass pass
hash #x fail ERR_PNPM_OUTDATED_LOCKFILE fail ERR_PNPM_OUTDATED_LOCKFILE
colon: x fail ERR_PNPM_BROKEN_LOCKFILE fail ERR_PNPM_BROKEN_LOCKFILE

Reproduced twice on each failing cell. pnpm ≥ 9 vendored locks use only relative specifiers, so they aren't affected. Windows paths (C:/Users/x/My Project #2) would hit the same splice, but I haven't tested that on a runner.

First bad release: release 4.0.0 (npm) writes the same unquoted line and fails identically. 3.3.0 has no vendored mode. So it isn't a regression.

Suspect code

  • crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs:593 (v5.4 specifiers: line): format!(" {}: {}", yaml_key_like(key, repr), ctx.abs_spec)
  • crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs:654 (v6.0 nested specifier:): format!(" specifier: {}", ctx.abs_spec)
  • abs_spec is built at pnpm_lock_legacy.rs:358 from the canonical root without YAML escaping. The in-sync comparisons at :578 / :630 would also need to compare the decoded value. vendor --check evidently validates the same raw string, so it agrees with the broken write.

Probe runs: none (Linux reproduction only).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions