Skip to content

Vendored yarn classic: after a git merge resolves .socket/vendor/state.json to one side, vendor --check and rollback exit 0 while the other side's package stays wired, and rollback deletes the ledger, stranding it #991

Description

[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).

Summary

This is a common team flow. Two branches each run scan --mode vendored for a different package. Git merges yarn.lock cleanly (the blocks are different), but .socket/vendor/state.json conflicts (AA, both sides add an entry under entries). The obvious resolution is git checkout --ours or --theirs. It leaves the other branch's package wired in yarn.lock to its committed tarball, with no ledger entry. socket-patch handles that state inconsistently:

  • vendor --check exits 0 ("committed artifact and wiring verified") and audits only the remaining ledger entry. It also exits 0 with no events once the ledger is gone entirely, while yarn.lock still resolves into .socket/vendor/.
  • rollback exits 0: "Reverted vendoring for pkg:npm/left-pad@1.3.0", with no event or warning for the unrecorded wiring. It then deletes state.json, so is-number stays wired and its tarball stays committed, with no ledger at all.
  • After that, a second rollback fails: "the vendor ledger is missing — restore .socket/vendor/state.json from version control". So does scan --mode vendored ("the lockfile references this patch without its vendor ledger entry; restore .socket/vendor/state.json from version control"). No commit in history has a ledger with the is-number entry next to the merged lock, so that remedy doesn't apply. The only exits are hand-editing state.json or git checkout -- yarn.lock.
  • remove pkg:npm/is-number@7.0.0 exits 1 not_found. vendor --revert exits 0 and skips it with vendor_orphan_still_wired.

repair already detects this exact shape: exit 1, vendor_ledger_missing, "a lockfile references .socket/vendor/npm// but the vendor ledger has no entry for it". So does scan. Only the CI gate (vendor --check) and rollback miss it.

Impact

  • vendor --check is the documented CI gate, and it stays green on a project whose ledger no longer covers a wired package. The next rollback reports success but leaves that package patched and committed. It also destroys the ledger that the remaining entries' originals lived in, and every later rollback or re-vendor then fails with an unusable remedy.
  • Users read "rollback succeeded" as "nothing is vendored any more". yarn's frozen install keeps installing the patched is-number from the orphaned tarball.

Repro (yarn 1.22.22; local mock patch API serving left-pad@1.3.0 and is-number@7.0.0)

git init -q app && cd app
echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","is-number":"7.0.0"}}' > package.json
yarn install && echo node_modules > .gitignore && git add -A && git commit -qm base
git checkout -qb a && socket-patch scan --mode vendored --yes --package pkg:npm/left-pad@1.3.0 && git add -A && git commit -qm a
git checkout -q main && git checkout -qb b && socket-patch scan --mode vendored --yes --package pkg:npm/is-number@7.0.0 && git add -A && git commit -qm b
git checkout -q a && git merge --no-edit b            # CONFLICT (add/add) in .socket/vendor/state.json; yarn.lock auto-merges
git checkout --ours .socket/vendor/state.json && git add -A && git commit -qm merge
socket-patch vendor --check; echo $?                   # 0  — "pkg:npm/left-pad@1.3.0: committed artifact and wiring verified"
socket-patch repair; echo $?                           # 1  — vendor_ledger_missing: "a lockfile references .socket/vendor/npm/2222…/ but the vendor ledger … has no entry"
socket-patch rollback; echo $?                         # 0  — "Reverted vendoring for pkg:npm/left-pad@1.3.0"
ls .socket/vendor/state.json                           # gone
grep -A2 '^is-number' yarn.lock                        # resolved "file:./.socket/vendor/npm/2222…/is-number-7.0.0.tgz#…"
socket-patch vendor --check; echo $?                   # 0  — no output at all
socket-patch rollback; echo $?                         # 1  — "vendor ledger is missing — restore .socket/vendor/state.json from version control"
rm -rf node_modules && yarn install --frozen-lockfile  # installs the patched is-number from the orphaned tarball

A manual JSON union of both sides' entries works end to end: vendor --check verifies both, and rollback is byte-exact to the base lock. So the bug is the handling of the one-sided resolution, not the merge itself.

Expected vs actual

  • CLI_CONTRACT "Vendored JVM support (v5)", the vendor --check paragraph: "Missing ledger entries fail with vendor_ledger_missing." Actual: a lockfile reference with no ledger entry passes vendor --check (exit 0). For JVM, run_check already refuses "artifacts exist without a vendor ledger" (vendor.rs:983-993); npm has no equivalent.
  • CLI_CONTRACT rollback "State discovery": "A project whose lockfiles still reference .socket/vendor/ artifacts but whose vendor ledger is missing errors asking for .socket/vendor/state.json to be restored". Actual: with a partial ledger, rollback exits 0, skips the unrecorded reference silently, and deletes the ledger, which creates exactly the state the contract refuses.
  • Expected: vendor --check fails (vendor_ledger_missing, naming the referenced uuid/path, as repair does). rollback either refuses before writing or warns and keeps the ledger. The remedy text should cover the merge case (merge both sides' entries).

OS × version

OS yarn result
Linux 1.7.0 reproduces (×2)
Linux 1.10.1 reproduces (×2)
Linux 1.22.22 reproduces (×2)

This is ledger and lock-scan logic with no OS-specific paths, so macOS and Windows weren't probed. It is likely npm-family-wide (the check loop is ecosystem-generic), but it was only measured with yarn classic.

First bad: not a regression. v4.0.0 has no vendor --check, and its rollback skips vendored entries ("use remove or vendor --revert"). Tested on main 9c43dfc.

Suspect code

  • crates/socket-patch-cli/src/commands/vendor.rs:974 run_check: iterates only state.entries and manifest keys, and never calls vendored_backend::repair::scan_vendor_references (which repair uses at vendored_backend/repair.rs:585 to emit vendor_ledger_missing for uncovered references).
  • crates/socket-patch-cli/src/commands/rollback.rs:~1100: the scan_vendor_references guard runs only when the ledger is absent, not for references the present ledger doesn't cover.

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