Skip to content

Vendored pnpm 7/8 (lock 5.4 / 6.0) writes an unquoted name: @scope/pkg for a scoped package, so every frozen install fails with ERR_PNPM_BROKEN_LOCKFILE after a successful scan #956

Description

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

Summary

When vendored mode rewires a scoped package (@scope/name) in a pnpm 7 (lockfileVersion 5.4) or pnpm 8 (lockfileVersion 6.0) pnpm-lock.yaml, the legacy lock writer re-emits the package entry's name: field without quoting it:

packages:

  file:.socket/vendor/npm/<uuid>/@isaacs/string-locale-compare-1.1.0.tgz:
    resolution: {integrity: sha512-…, tarball: file:.socket/vendor/npm/<uuid>/@isaacs/string-locale-compare-1.1.0.tgz}
    name: @isaacs/string-locale-compare      # <- `@` is a reserved YAML indicator; pnpm writes '@isaacs/string-locale-compare'
    version: 1.1.0
    dev: false

@ can't start a plain YAML scalar, so the lock no longer parses. scan --mode vendored (and vendor) report success and vex attests not_affected, but pnpm rejects the lock:

 ERR_PNPM_BROKEN_LOCKFILE  The lockfile at ".../pnpm-lock.yaml" is broken: bad indentation of a mapping entry (19:11)
 19 |     name: @isaacs/string-locale-compare
----------------^

pnpm 9+ (lock 9.0) isn't affected, because that writer emits no name: field. Unscoped packages (left-pad) are fine on every version.

Impact

  • Every pnpm install --frozen-lockfile fails on pnpm 7.33.7 and 8.15.9 once a scoped package is vendored, both in place and in a fresh checkout (--frozen-lockfile is the CI default). So vendored mode on pnpm 7/8 is unusable for any scoped package, and most real-world patch targets are scoped.
  • A plain pnpm install prints WARN Ignoring broken lockfile, re-resolves the whole tree from package.json (here through the pnpm.overrides wiring, so the patched bytes do land), and rewrites the lock. That throws away every other lock pin in the project.
  • Lock-only vex keeps attesting not_affected from a lock that pnpm itself refuses to read.
  • vendor --revert still restores pnpm-lock.yaml byte for byte (pass).

Repro

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

export PATH=/path/to/pnpm-8.15.9/bin:$PATH        # or 7.33.7
mkdir proj && cd proj
cat > package.json <<'EOF'
{ "name": "cell", "version": "0.0.0", "private": true,
  "dependencies": { "@isaacs/string-locale-compare": "1.1.0" } }
EOF
pnpm install                                     # lockfileVersion '6.0' (5.4 on pnpm 7)
socket-patch scan --mode vendored --json --api-url $MOCK --org test-org --api-token tok
# -> "status": "success"
grep -n 'name: @' pnpm-lock.yaml                  # -> 19:    name: @isaacs/string-locale-compare
pnpm install --frozen-lockfile                    # -> ERR_PNPM_BROKEN_LOCKFILE ... bad indentation of a mapping entry
socket-patch vex --output vex.json ...            # -> not_affected

Expected vs actual

  • Expected: docs/ecosystems.md lists pnpm legacy v5.4/v6.0 (pnpm 7/8) as a supported vendored lockfile flavour. The only documented caveat is the absolute file: specifier: a frozen install in the same checkout works, and a moved checkout needs one pnpm install --offline --no-frozen-lockfile (vendor_pnpm_legacy_absolute_specifier). The rewritten lock should stay valid YAML that pnpm can load, as it is for unscoped packages.
  • Actual: for a scoped package the lock is invalid YAML. The frozen install fails in place too, and the documented --offline --no-frozen-lockfile recovery doesn't help either: it discards the lock and re-resolves.

Matrix (Linux, Node 22, main 9c43dfc)

pnpm (lock) scoped @isaacs/string-locale-compare unscoped left-pad (control)
7.33.7 (5.4) fail: broken lock, 2/2 runs pass
8.15.9 (6.0) fail: broken lock, 2/2 runs pass
9.15.9 (9.0) pass (frozen install patched) pass
10.34.5 (9.0) pass pass
11.28.3 (9.0) pass pass
12.8.1 (9.0) pass pass

macOS and Windows weren't probed. The writer is OS-independent string formatting, so I expect them to behave the same.

First bad version

Not a regression from the last release: 4.0.0 writes the same unquoted name: (pnpm 8.15.9, reproduced). 3.3.0 has no pnpm vendored mode.

Suspect code

crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs:730

new_lines.push(format!("    name: {}", ctx.name));

It should go through the same YAML scalar quoting that pnpm uses for scoped names (the 9.0 writer in pnpm_lock.rs already has yaml_key for this): pnpm writes name: '@isaacs/string-locale-compare'. The legacy hermetic fixtures (pnpm7_lock_v54_hermetic_…, pnpm8_lock_v60_hermetic_… in crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs) only use left-pad, so a scoped case would catch this.

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