Skip to content

Vendored pnpm 9+ misses the aliased-importer refusal for a scoped npm alias (sl: npm:@scope/pkg@x), so it leaves a dangling quoted importer reference: scan says success, frozen installs fail, and VEX attests not_affected #957

Description

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

Summary

Vendored mode on a pnpm 9.0 lock is supposed to refuse a package that an npm: alias references (vendor_lock_entry_unsupported: "references … through an aliased importer version … that the pair surgery cannot rewrite — vendoring would leave a dangling reference pnpm rejects"). For an unscoped alias (lp: npm:left-pad@1.3.0) it does. For a scoped alias it doesn't. pnpm writes the importer's version quoted:

importers:
  .:
    dependencies:
      sl:
        specifier: npm:@isaacs/string-locale-compare@1.1.0
        version: '@isaacs/string-locale-compare@1.1.0'

The guard compares the raw (still quoted) value against the unquoted registry key, so it never matches. The vendor surgery then renames the packages: / snapshots: entries to '@isaacs/string-locale-compare@file:.socket/vendor/…' and leaves the importer's version: '@isaacs/string-locale-compare@1.1.0' pointing at an entry that no longer exists. That is exactly the dangling reference the refusal exists to prevent.

Impact

  • scan --mode vendored exits 0 with status: success and vendor reports applied.
  • Every pnpm install --frozen-lockfile then fails with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY ("Broken lockfile: no entry for '@isaacs/string-locale-compare@1.1.0' in pnpm-lock.yaml … probably caused by a badly resolved merge conflict").
  • Manifest-less vex attests not_affected for pkg:npm/%40isaacs/string-locale-compare@1.1.0, although nothing patched can be installed from this lock.
  • vendor --revert restores the lock byte for byte (pass).

Repro

The patch API is mocked locally (batch, by-package, view, package grant and the tarball route); the patch prepends a marker to index.js.

export PATH=/path/to/pnpm-12.8.1/bin:$PATH        # or 9.15.9
mkdir proj && cd proj
cat > package.json <<'EOF'
{ "name": "cell", "version": "0.0.0", "private": true,
  "dependencies": { "sl": "npm:@isaacs/string-locale-compare@1.1.0" } }
EOF
pnpm install
socket-patch scan --mode vendored --json --api-url $MOCK --org test-org --api-token tok
# -> exit 0, "status": "success", vendor event "applied"
grep -A2 '^      sl:' pnpm-lock.yaml        # importer version still '@isaacs/string-locale-compare@1.1.0'
grep -n "string-locale-compare@1.1.0':" pnpm-lock.yaml   # -> no packages/snapshots entry with that key any more
pnpm install --frozen-lockfile           # -> ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY
socket-patch vex --output vex.json ...   # -> not_affected

Control: {"lp": "npm:left-pad@1.3.0"} in the same project is refused correctly with vendor_lock_entry_unsupported ("an aliased importer version (left-pad@1.3.0)"), and the run reports partial_failure.

Expected vs actual

  • Expected: the refusal fires for scoped aliases too (or the surgery rewrites the alias importer). The pnpm 9.0 path already refuses unscoped aliases, and the pnpm 6.0 (pnpm 8) path refuses the scoped one (aliased root dependency (/@isaacs/string-locale-compare@1.1.0)). CLI_CONTRACT and docs/ecosystems.md describe lock shapes vendoring can't handle as loud refusals that write nothing, not as a success that breaks frozen installs.
  • Actual: success, a broken lock, and a VEX attestation.

Matrix (Linux, Node 22, main 9c43dfc)

pnpm (lock) scoped alias npm:@isaacs/string-locale-compare@1.1.0 unscoped alias npm:left-pad@1.3.0 (control)
8.15.9 (6.0) pass (refused: aliased root dependency) pass (refused)
9.15.9 (9.0) fail: success, frozen install broken, vex attests (2/2) pass (refused)
12.8.1 (9.0) fail: same (2/2) pass (refused)

pnpm 10 and 11 use the same 9.0 lock grammar and weren't run separately. macOS and Windows weren't probed (this is string matching, independent of OS).

First bad version

Not a regression: release 4.0.0 behaves the same on pnpm 12.8.1 (success, broken frozen install, not_affected).

Suspect code

crates/socket-patch-core/src/vendor/pnpm_lock.rs:1588 (index path) and :1643 (fallback scan):

if v == reg_key || v.starts_with(&key_peer_prefix) {
    return refuse("an aliased importer version", v);
}

v is the raw YAML value ('@isaacs/string-locale-compare@1.1.0', quoted because it starts with @), and reg_key is unquoted. The same comparison on snapshot body references (rest == reg_key, around :1576 / :1617) probably misses a scoped alias inside a transitive dependency's snapshot in the same way. Unquoting (unquote_value) before comparing, as the catalog branch right below already does for the specifier, should cover all four sites. importer_ref_candidates may also need to look up by the unquoted value.

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

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions