Skip to content

Yarn 4 pnpm linker: transitive packages that live only in node_modules/.store are "not installed" in agent mode and stay unpatched #495

Description

[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).

Summary

With Yarn 4's nodeLinker: pnpm, a transitive dependency exists only inside node_modules/.store/<name>-npm-<version>-<hash>/package/. The only path to it is the store entry's node_modules/<name> symlink (-> ../package). Agent-mode apply never finds that copy. It reports skipped / package_not_installed and leaves the package unpatched. When the same run patches other (direct) packages, it ends success with exit 0.

#359 was fixed on main by #365 (61cfb9b, "Find packages in npm .store and pnpm virtualStoreDir"), which now walks node_modules/.store. That fix covers npm's linked layout, where .store/<name>@<v>-<hash>/node_modules/<name> is a real directory. Yarn 4 uses the same .store name, but there the node_modules/<name> child is a symlink to the sibling package/ dir, so the store-entry probe rejects it.

Yarn 3.x's pnpm linker writes real dirs at .store/<slug>/node_modules/<name> and isn't affected.

Impact

Every transitive dependency of a Yarn 4 project on the pnpm linker is unpatchable in agent mode, and when other patches apply the run gives no failure signal (exit 0). The docs say this layout is supported:

  • docs/ecosystems.md: "Inside each node_modules, the package stores of isolated layouts are walked too, since they are the only home of transitive dependencies: … npm's install-strategy=linked store node_modules/.store."
  • docs/testing/yarn-berry-compatibility.md: the node-modules and pnpm linkers "are covered end to end". The pnpm-linker e2e (e2e_yarn4_pnpm_linker_build) only patches a direct dependency.

Repro (Linux, Node 22, yarn 4.12.0)

mkdir p && cd p
echo '{"name":"p","version":"1.0.0","dependencies":{"is-odd":"3.0.1","left-pad":"1.3.0"}}' > package.json
printf 'nodeLinker: pnpm\n' > .yarnrc.yml && touch yarn.lock
yarn install
ls -l node_modules/.store/is-number-npm-6.0.0-*/node_modules/   # is-number -> ../package
# Stage an agent-mode manifest (.socket/manifest.json + .socket/blobs) with two patches:
#   pkg:npm/is-number@6.0.0  (transitive, via is-odd) file package/index.js
#   pkg:npm/left-pad@1.3.0   (direct)                 file package/index.js
# (after blob = "/* SOCKET-PATCHED */\n" + original index.js; git-sha256 hashes)
SOCKET_NO_CONFIG=1 socket-patch apply --json; echo "exit=$?"
head -c 20 node_modules/.store/is-number-npm-6.0.0-*/package/index.js

Actual (main 61cfb9b):

status: success  summary: {"applied": 1, "skipped": 1, "failed": 0, …}
{"action":"applied","purl":"pkg:npm/left-pad@1.3.0","files":[{"path":"package/index.js","verified":true,"appliedVia":"blob"}]}
{"action":"skipped","purl":"pkg:npm/is-number@6.0.0","reason":"No installed package matches this PURL","errorCode":"package_not_installed"}
exit=0
/*!\n * is-number      <- unpatched

With only the is-number patch staged, the run is partialFailure / exit 1 with the same package_not_installed skip.

Expected vs actual

  • Expected: the store copy at .store/is-number-npm-6.0.0-<hash>/package is found and patched, as the docs above promise for isolated-store transitive dependencies (and as already happens for npm's .store, pnpm's .pnpm and Yarn 3's pnpm linker).
  • Actual: package_not_installed, file untouched, exit 0 when anything else applied.

OS × version

OS yarn linker transitive dep in .store
Linux 3.8.7 pnpm pass (real dir in the store entry)
Linux 4.0.2 pnpm fail
Linux 4.12.0 pnpm fail (reproduced twice, plus a scoped direct dep as control: patched)
Linux 4.18.1 pnpm fail
Linux 4.12.0 node-modules pass (control: hoisted node_modules/is-number patched)

macOS and Windows weren't probed. The layout is yarn's own (symlinks on POSIX, junctions on Windows), so they're likely affected the same way.

First bad version

Not a regression. Release 4.0.0 (npm @socketsecurity/socket-patch) behaves identically on the same tree. 61cfb9b didn't extend .store support to Yarn's layout.

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:1140: in visit_resolver_dir, a store-entry node_modules rejects any target that isn't a real directory (is_real_package_dir_sync, npm_crawler.rs:2361, uses symlink_metadata(..).is_dir()). Yarn's node_modules/<name> -> ../package is a symlink to the entry's own package/ dir, which is the only physical copy. Nothing walks <entry>/package directly.
  • list_npm_store_entries_sync / decode_npm_store_entry_name (npm_crawler.rs:231, ~1951) parse npm's <name>@<version>-<hash> key. Yarn's keys are <slug>-npm-<version>-<hash> (scoped: @scope-name-npm-…), so the advertised name/version is never decoded for them either.

No probe runs yet. macOS/Windows are on the routine's backlog.

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