Skip to content

Vendored yarn classic writes and deletes through a symlinked .socket/vendor/npm dir, so rollback in one project deletes another project's vendored tarballs and breaks its frozen install #664

Description

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

Summary

If .socket/vendor/npm is a symlink (for example, two non-workspace projects in a monorepo that share one vendor store), scan --mode vendored writes the <uuid>/ artifact dirs through the link. rollback / vendor --revert then delete through the link too (remove_tree_and_prune at crates/socket-patch-core/src/vendor/yarn_classic_lock.rs:548). When a second project has vendored the same patch into the same shared store, rolling back the first project deletes the second project's tarballs. The second project's yarn.lock still points at file:./.socket/vendor/npm/<uuid>/…tgz, so its next frozen or offline install fails. The rollback exits success with no warning.

The code already says this layout isn't ours. sweep_vendor_dirs (crates/socket-patch-core/src/vendor/path.rs ~357–364 and the note at ~892) is deliberately symlink-strict: "staging creates these dirs itself and never writes symlinks, so a symlinked eco dir cannot be ours — read_dir would follow it and the sweep would enumerate (and let callers delete through) its target." The vendor write path and the ledger-driven revert don't apply that rule, so the delete-through the sweep guards against still happens.

Impact

  • Silent data loss in a different project: a project that is correctly vendored loses its committed artifacts, and the rollback that did it reports success.
  • That project's yarn install --frozen-lockfile [--offline] then fails (Tarball is not in network and can not be located in cache). Its vendor --check reports artifact verification failed: Missing.

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

mkdir -p mono/shared && cd mono
for X in A B; do
  mkdir $X && (cd $X
    echo '{"name":"'$X'","version":"1.0.0","private":true,"dependencies":{"is-number":"7.0.0","left-pad":"1.3.0"}}' > package.json
    yarn install
    mkdir -p .socket/vendor && ln -s ../../../shared .socket/vendor/npm
    socket-patch scan --mode vendored --vendor-source service --api-url $API --org test-org --api-token x --json --yes)   # success
done
find shared -type f | wc -l          # 4  (two uuid dirs: marker + tgz each)
(cd A && socket-patch rollback --json --yes)          # status: success, no warning
find shared -type f | wc -l          # 0  <- B's artifacts are gone too
cd B && rm -rf node_modules && yarn install --frozen-lockfile --offline
# error "./.socket/vendor/npm/1111…/is-number-7.0.0.tgz": Tarball is not in network and can not be located in cache   (exit 1)
socket-patch vendor --check --json   # partialFailure: artifact verification failed: Missing (x2)

vendor --revert in A instead of rollback gives the same result.

Expected vs actual

  • Expected: a symlinked .socket/vendor/<eco> (or <uuid>) dir is refused before any write, as the other symlinked vendored inputs are. Hosted redirect_symlinked_file_unsupported, the cargo / pypi *_symlink_unsupported refusals and the bun / hatch backends' own checks all refuse before writing (CLI_CONTRACT error table). At minimum, a revert must never delete through a link the sweep treats as "cannot be ours".
  • Actual: vendoring writes through the link, and the revert deletes the uuid dirs at the link's target. Exit 0 / success both ways.

Matrix (Linux, main 045d7ec, Node 22)

yarn A + B vendored A rollback empties the shared store B frozen offline install afterwards
1.7.0 success yes fails (exit 1)
1.10.1 success yes fails (exit 1)
1.22.22 success (x2) yes (x2, also via vendor --revert) fails (exit 1)

Single project with a symlinked .socket/vendor/npm: the scan, the fresh frozen install, vendor --check and rollback all "work" (the artifacts live at the link target). The damage only shows when the store is shared. A whole-.socket symlink shares vendor/state.json too, so that's a different shape; it wasn't tested for cross-project effects.

Suspect code

No probe run: the behaviour comes from a path-handling choice that doesn't depend on the OS. macOS / Windows (directory junctions) are untested.

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