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
mikolalysenko commentedon Oct 9, 2026
[agent] Same gap for a git copy under another name (the #363 shape, renamed). Main
f3c6313, yarn 1.22.22, Linux. I used a localgit+file://repo of left-pad 1.3.0 because the sandbox can't reach github.com.scan --vexvexnode_modules/lp3after fresh--frozen-lockfileredirect_yarn_classic_berry_migration_risk)The same-name git copy is handled: no ref, plus a
patched_ref_unattributable"installs from git" diagnostic (classic_git_pattern_copies_are_never_attested). The renamed one is read aslp3@1.3.0(vex/discover/yarn.rs:151), so it never shadows the left-pad ref. A git key does name its repo, but its package name is only in the fetchedpackage.json, so the fix for directories (reading the manifest) probably applies here too.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
[agent] Triage: priority:p1 (yarn classic; a false
not_affectedVEX attestation). Not a duplicate: it's the yarn classic counterpart of #939 (yarn berry, still open), which goes through a different rewriter and lock reader. Extends #921/#924, which only covers a same-namefile:copy. No open PR covers it.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
[agent] Claiming this issue (shared root cause: yarn classic VEX discovery and the hosted/vendored classic rewriters take a copy's package identity from the lock key, so a file:/url copy locked under another dependency name is never read for the package it really installs). Branch: agent/fix-yarn-classic-other-name-copy. Claim-ID: 2026-10-09T07:22:27Z-55af4e
Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
[agent] Draft PR: #1242
Generated by Claude Code
Stop yarn classic VEX attesting a renamed copy
Name renamed yarn classic copies in scans
Test and document renamed yarn classic copies
Strip package.json BOMs through the shared helper
mikolalysenko commentedon Oct 9, 2026
[agent] #1242 is ready for review. It fixes the reported
file:directory shape, plusfile:tarball and registry-URL copies under another name, invex(lock-only and in-run) and in the hosted and vendored scan warnings. It saysRefsrather thanFixes, and this issue stays open for two follow-ups:vexalready catches it. A lock-only fix needs a policy decision.file:tarball (VEX already handles it).Generated by Claude Code
3 remaining items
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