Repository navigation
Yarn classic renamed-copy detection (#1489) misses a file: copy outside the project root and a tarball whose root isn't package/, so scans succeed and VEX attests while that copy installs unpatched #1516
Description
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
#1489 (for #1236 / #939) works out which package a renamed yarn copy holds ("lp2": "file:./lpdir" locks as lp2@file:./lpdir) by reading the copy itself through formats::yarn::copy::CopyRef. Hosted, vendored and vex all use that reader. When the read fails, the copy is treated as "not this package" and ignored. That's fail-open. Two common shapes make the read fail even though yarn installs the copy normally:
- A
file:path outside the project root, e.g."lp2": "file:../shared/left-pad"(a directory) or"lp2": "file:../shared/left-pad-1.3.0.tgz"(a tarball). Shared code in a non-workspace monorepo is often declared this way.classic_copy_refreturnsNonefor any path that leaves the root (normalize_rel("", path)?). - A
file:tarball whose top-level directory isn'tpackage/, e.g. a tarball made withgit archive --prefix=left-pad-1.3.0/or a GitHub release asset. yarn 1 strips the first path segment whatever it's called.tarball_manifest_nameuses the lenient decoder, which strips only a literalpackage/, so it finds nopackage.json.
In both shapes, hosted and vendored scans exit 0 success with no copy warning. The in-run scan --vex and the lock-only vex attest not_affected for pkg:npm/left-pad@1.3.0. A fresh yarn install --frozen-lockfile installs node_modules/lp2 (left-pad@1.3.0) unpatched.
The same-name control shows the inconsistency. In a workspace member, "left-pad": "file:../../ext/lpdir" (same outside-root directory) is caught: redirect_yarn_classic_directory_skipped, vex_omitted, exit 1. Only the dependency name differs.
A third shape fails the same way: a renamed non-registry URL tarball ("lp2": "http://host/lp.tgz"), or a renamed GitHub shorthand, which yarn locks as a codeload URL. registry_tarball_name can't name it, so it's ignored. The docs only promise identification from "a registry tarball URL's path", so that one may be intended. Even so, ignoring a copy whose identity can't be read means VEX can attest while an unpatched copy is installed.
Impact
The VEX document says not_affected while the project ships an unpatched copy of the vulnerable name@version, and the scan gives no warning. This is the false attestation #1236 was filed for, reached through a different path spelling or a different tarball layout.
Repro (Linux, Node 22, main 23fa8d4, local mock patch API)
The mock serves a left-pad@1.3.0 patch that prepends a marker to index.js (SOCKET_API_URL / SOCKET_PROXY_URL / SOCKET_PATCH_SERVER_URL → http://127.0.0.1:8787).
mkdir -p ext/lpdir p && curl -sL https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz | tar xz -C ext/lpdir --strip-components=1
cd p
echo '{"name":"p","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","lp2":"file:../ext/lpdir"}}' > package.json
yarn install # yarn.lock: "lp2@file:../ext/lpdir": version "1.3.0"
socket-patch scan --mode hosted --json --yes --vex vin.json # exit 0, status success, warnings: [redirect_yarn_classic_berry_migration_risk] only
# vin.json: not_affected (left-pad@1.3.0)
rm -rf node_modules
socket-patch vex --output vlock.json --json # exit 0, not_affected
yarn install --frozen-lockfile # exit 0
grep -c SOCKET-PATCHED node_modules/left-pad/index.js # 1
grep -c SOCKET-PATCHED node_modules/lp2/index.js # 0 <- unpatched left-pad@1.3.0
# tarball-root shape (inside the project):
mkdir -p s/left-pad-1.3.0 && tar xzf left-pad-1.3.0.tgz -C s/left-pad-1.3.0 --strip-components=1
tar czf lp.tgz -C s left-pad-1.3.0 # root dir "left-pad-1.3.0/", not "package/"
# dependencies: {"left-pad":"1.3.0","lp2":"file:./lp.tgz"} -> same result as aboveControl, same path layout but with a package/ root and inside the project ("lp2": "file:./lp.tgz"): hosted warns redirect_copy_unpatched, vendored skips it with vendor_yarn_classic_non_registry_entry_skipped, vex_omitted, exit 1. That's correct, and it's what #1489 added.
Post-install vex (with node_modules present) refuses correctly in every shape, as in #1236.
Expected vs actual
- docs/ecosystems.md, "yarn copies under another dependency name": "Hosted mode still pins the registry entry and names the copy (
redirect_copy_unpatched…); vendored mode … names it; andvexnever attests the package from that lock while the copy is there." The "yarn classicfile:directory dependencies" section adds: "This holds whatever dependency name the project gives the copy." - Expected: an outside-root
file:copy is read the way yarn reads it (the same-name skip already treats it as a copy), and a tarball's manifest is read after stripping its first path segment, as yarn does. If a renamed copy's identity can't be read at all (unreadable path, non-registry URL), the code should fail closed: warn, and don't attest the package from that lock. - Actual: the copy is silently ignored. The scan reports
successand VEX attestsnot_affected.
Matrix (Linux, main 23fa8d4; each cell ×1, the 1.22.22 hosted cells ×2)
| yarn | mode | file:../ext/lpdir |
file:../ext/lp.tgz |
file:./lp.tgz (root left-pad-1.3.0/) |
http://127.0.0.1:8799/lp.tgz |
control file:./lp.tgz (package/) |
control same-name file:../../ext/lpdir |
|---|---|---|---|---|---|---|---|
| 1.7.0 | hosted | missed | missed | missed | missed | — | — |
| 1.10.1 | hosted | missed | missed | missed | missed | — | — |
| 1.22.22 | hosted | missed | missed | missed | missed | warned, omitted | warned, omitted |
| 1.22.22 | vendored | missed | — | missed | missed | warned, omitted | — |
"missed" means: scan exits 0 success with no copy warning, in-run and lock-only VEX say not_affected, and node_modules/lp2 installs unpatched. (.tar tarballs aren't a yarn 1 shape: yarn 1.22.22 fails to install file:./lp.tar.) macOS and Windows weren't probed. The logic is path and archive parsing, not OS-specific, though Windows file:..\\x spellings might add more cases.
First bad: not a regression. #1489 (after v5.0.1) introduced the renamed-copy detection, and these shapes were never covered. v5.0.1 misses every renamed copy (#1236).
Suspect code
crates/socket-patch-core/src/formats/yarn/copy.rs:107:normalize_rel("", path)?drops anyfile:path that leaves the root, soclassic_copy_refreturnsNone. The callersvex/discover/yarn.rs:161andvendor/yarn_copies.rs:64then skip the block with?. The same-name path (classic_copy_source→CopySource::Directory) has no such limit.crates/socket-patch-core/src/formats/yarn/copy.rs:183:tarball_manifest_namereads withread_archive_bytes_to_map(lenient: strips only a literalpackage/). yarn 1, like npm, strips the first segment whatever it is (see theread_archive_bytes_to_map_strictdoc comment inpatch/package.rs:123).crates/socket-patch-core/src/formats/yarn/copy.rs:69: a non-registry URL returnsNone(unknown), and every caller treats unknown as "not a copy of this package".berry_copy_path(copy.rs,normalize_rel(&workspace, path)) has the same outside-root limit for yarn berry. I've handed that over to the yarn-berry routine; it's untested here.
Activity
yarn-classic: run 39 ledger (#1516, #1236 closed) + yarn-berry handover
mikolalysenko commentedon Oct 14, 2026
[agent] Triage:
priority:p1(yarn classic, npm family). Not a duplicate: #1236 / #939 were the renamed-copy miss as a whole, which #1489 fixed for in-rootfile:copies with apackage/tarball root. This is a new set of shapes that the same reader drops.I confirmed the suspect code on main
23fa8d4:formats/yarn/copy.rsclassic_copy_ref:normalize_rel("", path)?returnsNonefor anyfile:path outside the root, so callers treat the copy as unknown and skip it.tarball_manifest_namereads through the lenientread_archive_bytes_to_map, which only strips a literalpackage/prefix. yarn 1 strips the first segment whatever its name.CopyRef::Urlfalls back toregistry_tarball_name, so a non-registry URL gets no name and is also skipped.All three share one cause: an unreadable copy identity is treated as "not this package" (fail-open). The fix belongs at that boundary: read outside-root paths the way yarn does, strip the first tarball segment, and when identity still can't be read, warn and stop attesting. No open PR covers it yet.
Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Claiming this issue (shared root cause: the yarn classic renamed-copy reader treats an unreadable copy identity as "not this package"). Branch: agent/fix-yarn-copy-identity-fail-open. Claim-ID: 2026-10-14T02:20:56Z-5b407f
Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Draft fix PR: #1517
Generated by Claude Code
Read yarn copies outside root or package/ dir
Format and document unread yarn copy handling
mikolalysenko commentedon Oct 14, 2026
[agent] Yarn Berry evidence from the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305). The berry side shares the same reader (
berry_copy_ref→berry_copy_path→normalize_rel, plustarball_manifest_name), and it fails the same way with a real yarn install.Setup: Linux, Node 22, real
yarn install(node-modules linker). The repo's wiremock hosted fixture (berry_hosted_project) with a rootleft-pad@1.3.0, plus a renamed copylp2of left-pad@1.3.0. Thenscan --mode hosted --vex, a standalone lock-onlyvex --patch-server-url <mock>, and an in-placeyarn install --immutable.lp2descriptor23fa8d4(4.0.2 / 4.18.1)6289505(4.18.1)portal:../ext/lpdir(outside the root)not_affectedforpkg:npm/left-pad@1.3.0;node_modules/lp2unpatchedredirect_copy_unpatched+vex_omitted, exit 1; lock-onlyvexexit 2file:../ext/lp.tgzfile:../ext/lpdir(4.18.1 only)file:./lp.tgzwith aleft-pad-1.3.0/root (berry also strips the first segment, whatever its name)file:./lpdir,file:./lp.tgzwith apackage/rootredirect_copy_unpatched(pass)Every failing cell reproduced twice. Lock entries look like
"lp2@portal:../ext/lpdir::locator=…%40workspace%3A."(version: 0.0.0-use.local) and"lp2@file:./lp.tgz#./lp.tgz::hash=…&locator=…". I didn't test vendored berry (vendor_copy_unpatched) on either SHA. When #1517 merges, the Berry routine will re-check these cells on 4.0.2 and 4.18.1.Generated by Claude Code