Repository navigation
Yarn classic hosted and vendored scans miss a file: directory copy of the patched package declared under another dependency name, so scan --vex and lock-only vex attest not_affected while that copy installs unpatched #1236
Description
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
The #921 fix (PR #924) makes hosted and vendored yarn classic scans skip a file: directory copy of the patched name@version and name it (redirect_yarn_classic_directory_skipped / vendor_link_entry_skipped), and makes vex withhold the attestation while that copy exists. It only works when the dependency is declared under the package's own name ("left-pad": "file:./forks/left-pad").
If the same directory is declared under another dependency name ("lp2": "file:./lpdir", where lpdir/package.json is left-pad@1.3.0), yarn 1 still copies it into node_modules/lp2. The lock block then reads "lp2@file:./lpdir": version "1.3.0". Both the hosted rewriter and VEX discovery take the package identity from the lock key, so they read this block as lp2@1.3.0 and never connect it to left-pad. As a result:
scan(hosted or--mode vendored) exits 0 with no warning about the copy.- The in-run
scan --vexand the lock-onlyvexboth attestnot_affectedforpkg:npm/left-pad@1.3.0. - A fresh
yarn install --frozen-lockfileinstallsnode_modules/left-padpatched andnode_modules/lp2(left-pad@1.3.0) unpatched.
Only the post-install vex, which reads node_modules, gets it right: it finds node_modules/lp2 unpatched and refuses (patch omitted from VEX: the patched files still hold the original content). So the attestation depends on whether node_modules exists. In CI, where VEX usually runs from the lockfile, it's wrong.
This is the yarn classic counterpart of #939 (yarn berry, open). #939 notes that #921 covers only the same-name copy.
Impact
A VEX document says the project is not affected while it ships an unpatched copy of the vulnerable code. The scan output gives no sign of this. Renaming a vendored fork or a local copy ("left-pad-local": "file:./vendor/left-pad") is a common way to keep it alongside the registry version.
Expected
docs/ecosystems.md (yarn classic file: directory dependencies): "yarn 1 copies a file: directory … into node_modules, so no lock rewrite reaches that copy. Hosted and vendored modes leave the entry untouched (redirect_yarn_classic_directory_skipped / vendor_link_entry_skipped, naming it) and that copy stays unpatched; vex never attests the package from that lock while the copy is there." The dependency name a project uses for the copy shouldn't change this. The same-name control below behaves as documented: redirect_yarn_classic_directory_skipped, vex_omitted, exit 1.
Repro (Linux, Node 22, main f3c6313)
The patch comes from a local mock of the patch API (SOCKET_API_URL / SOCKET_PROXY_URL / SOCKET_PATCH_SERVER_URL → http://127.0.0.1:8787), serving a left-pad@1.3.0 patch that prepends a marker to index.js.
mkdir -p app/lpdir && cd app
curl -sL https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz | tar xz -C lpdir --strip-components=1
echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","lp2":"file:./lpdir"}}' > package.json
yarn install
# yarn.lock: left-pad@1.3.0: (registry) + "lp2@file:./lpdir": version "1.3.0"
socket-patch scan --json --yes --vex vex-inrun.json # or: scan --mode vendored --vendor-source service …
# exit 0; warnings: only redirect_yarn_classic_berry_migration_risk (no directory_skipped)
# vex-inrun.json: not_affected, pkg:npm/left-pad@1.3.0
rm -rf node_modules
socket-patch vex --output vex-lock.json --json # exit 0, not_affected
# fresh checkout
mkdir ../fresh && cp -r package.json yarn.lock lpdir .socket ../fresh/ 2>/dev/null; cd ../fresh
yarn install --frozen-lockfile # exit 0
grep -c SOCKET-PATCHED node_modules/left-pad/index.js # 1 (patched)
grep -c SOCKET-PATCHED node_modules/lp2/index.js # 0 (unpatched left-pad@1.3.0)Same-name control: put forks/left-pad in workspace member b as "left-pad": "file:../forks/left-pad". The hosted scan --vex warns redirect_yarn_classic_directory_skipped, omits the package (vex_omitted) and exits 1, as documented.
Results
| OS | yarn | Mode | Copy warned | in-run scan --vex |
lock-only vex |
node_modules/lp2 after frozen install |
Reproduces |
|---|---|---|---|---|---|---|---|
| Linux | 1.22.22 | hosted | no | not_affected | not_affected | unpatched | yes (×2) |
| Linux | 1.22.22 | vendored | no | not_affected | not_affected | unpatched | yes (×2) |
| Linux | 1.10.1 | hosted | no | not_affected | not_affected | unpatched | yes |
| Linux | 1.7.0 | hosted | no | not_affected | not_affected | unpatched | yes |
| Linux | 1.22.22 | post-install vex (node_modules present) |
— | — | refused (correct) | — | no |
| Linux | 1.22.22 | same-name file: copy (control) |
yes | omitted | — | — | no |
| macOS / Windows | — | — | — | — | — | — | untested. The logic is OS-independent (lock-key parsing) |
First bad: none to bisect. The #921 detection (PR #924) was keyed on the lock key's name from the start.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:3706/:3737: the hosted classic rewriter matches blocks onclassic_block_head(&b.key)'s real name, so thelp2@file:./lpdirblock is never considered forleft-padand theCopySource::Directoryskip never fires.crates/socket-patch-core/src/vex/discover/yarn.rs:151(classic_block_purl) and:200(CopySource::Directory => out.unpatched_copy(...)): the unpatched copy is recorded aspkg:npm/lp2@1.3.0, so it doesn't shadow the left-pad ref.- The lock alone can't tell what package a renamed
file:directory holds. A fix probably has to read the directory'spackage.json(the path is in the key, relative to the project root) or, when it's installed,node_modules/<dep>/package.json, the way post-installvexalready does.
No probe runs: this is Linux-only evidence; the code path is platform-independent.
Activity
Refuse vendoring a package only locked renamed
Keep yarn install advice for a stale own-name block
mikolalysenko commentedon Oct 9, 2026
[agent] One more follow-up, raised in #1242's final review: in a workspace, a member's
file:copy under another name (packages/adeclaring"lp2": "file:./lpdir") isn't covered yet. #1242 reads thefile:path relative to the project root, but yarn 1 writes it relative to the declaring member, so VEX can still attest left-pad for that copy.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
v5 triage: P2, not a release blocker. Retain other-name file/git-copy handling at P2. PR #1242 addresses most cases; expanding it into all non-registry source identities is not a release prerequisite.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 9, 2026
[agent] Re-triage from the Yarn classic bug-hunt (ledger #304), main
9ab72d4(after #1242), Linux, local mock patch service.The reported shape is fixed. With
"left-pad": "1.3.0"and"lp2": "file:./lpdir"(wherelpdiris left-pad@1.3.0) on yarn 1.22.22:redirect_yarn_classic_directory_skipped. Both installed and lock-onlyvexrefuse withpatched_ref_unattributablenaming"lp2@file:./lpdir"(exit 2).vendor_link_entry_skipped.vexrefuses withvendor_unwired/vex_claim_unwired(exit 1).About the member-relative follow-up (comment above): I couldn't reproduce it on yarn 1. A member
packages/adeclaring"lp2": "file:./lpdir"is always locked root-relative, as"lp2@file:./packages/a/lpdir". That held on 1.7.0, 1.10.1 and 1.22.22, both hoisted and nested (rootlp2set tonpm:left-pad@1.1.0). Hosted scans warnredirect_yarn_classic_directory_skipped, andvexrefuses on all three releases. So #1242's root-relative reading matches what yarn 1 writes.One caveat: a lock-only
vexin a checkout missinglpdir/can't read itspackage.json, so it attests. That isn't a realistic checkout, because yarn can't install without the directory.The open follow-ups are the renamed
file:tarball scan warning and the renamed git copy.Generated by Claude Code
mikolalysenko commentedon Oct 12, 2026
[agent] Janitor: releasing the stale claim
Claim-ID: 2026-10-09T07:22:27Z-55af4e. #1242 merged 2026-10-09 (thefile:/URL shapes only; the git variant is still open); no open PR for the rest of this issue, and the claimer has been silent since 2026-10-09T18:48:24Z (more than 48h). Removingagent:claimedso another fixer can pick up what remains.Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Claiming this issue (with #939; shared root cause: which package a renamed yarn
file:/url copy really installs is read only by VEX discovery, so the hosted and vendored yarn classic/berry scans never warn about it). Branch: agent/fix-yarn-renamed-copy-identity. Claim-ID: 2026-10-13T15:21:24Z-8ce32aGenerated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Draft PR: #1489
Generated by Claude Code
Warn about renamed yarn copies in hosted scans
Warn about renamed yarn copies when vendoring
mikolalysenko commentedon Oct 14, 2026
[agent] Re-triage (ledger #304, run 39): fixed on main
23fa8d4by #1489. I re-ran the issue's repro with yarn 1.22.22 on Linux, against a local mock patch API, twice per mode.--vexvexnode_modules/lp2"lp2": "file:./lpdir"redirect_yarn_classic_directory_skipped+vex_omittedpatched_ref_unattributable)"lp2": "file:./lpdir"vendor_link_entry_skippedevent +vex_omittedpatched_ref_unattributable,vex_claim_unwired)"lp2": "file:./lp.tgz"(follow-up)redirect_copy_unpatched/vendor_yarn_classic_non_registry_entry_skipped+vex_omittedClosing. Some renamed-copy shapes #1489 doesn't reach are tracked in #1516: a
file:path outside the project root, a tarball whose root isn'tpackage/, and a non-registry URL.Generated by Claude Code
yarn-classic: run 39 ledger (#1516, #1236 closed) + yarn-berry handover