[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.
[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'sname:field without quoting it:@can't start a plain YAML scalar, so the lock no longer parses.scan --mode vendored(andvendor) reportsuccessandvexattestsnot_affected, but pnpm rejects the lock: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
pnpm install --frozen-lockfilefails on pnpm 7.33.7 and 8.15.9 once a scoped package is vendored, both in place and in a fresh checkout (--frozen-lockfileis the CI default). So vendored mode on pnpm 7/8 is unusable for any scoped package, and most real-world patch targets are scoped.pnpm installprintsWARN Ignoring broken lockfile, re-resolves the whole tree frompackage.json(here through thepnpm.overrideswiring, so the patched bytes do land), and rewrites the lock. That throws away every other lock pin in the project.vexkeeps attestingnot_affectedfrom a lock that pnpm itself refuses to read.vendor --revertstill restorespnpm-lock.yamlbyte 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.Expected vs actual
pnpm legacy v5.4/v6.0 (pnpm 7/8)as a supported vendored lockfile flavour. The only documented caveat is the absolutefile:specifier: a frozen install in the same checkout works, and a moved checkout needs onepnpm 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.--offline --no-frozen-lockfilerecovery doesn't help either: it discards the lock and re-resolves.Matrix (Linux, Node 22, main
9c43dfc)@isaacs/string-locale-compareleft-pad(control)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:730It should go through the same YAML scalar quoting that pnpm uses for scoped names (the 9.0 writer in
pnpm_lock.rsalready hasyaml_keyfor this): pnpm writesname: '@isaacs/string-locale-compare'. The legacy hermetic fixtures (pnpm7_lock_v54_hermetic_…,pnpm8_lock_v60_hermetic_…incrates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs) only useleft-pad, so a scoped case would catch this.