Skip to content

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 --vex and the lock-only vex both attest not_affected for pkg:npm/left-pad@1.3.0.
  • A fresh yarn install --frozen-lockfile installs node_modules/left-pad patched and node_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 on classic_block_head(&b.key)'s real name, so the lp2@file:./lpdir block is never considered for left-pad and the CopySource::Directory skip 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 as pkg: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's package.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-install vex already does.

No probe runs: this is Linux-only evidence; the code path is platform-independent.

Activity

  1. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Same gap for a git copy under another name (the #363 shape, renamed). Main f3c6313, yarn 1.22.22, Linux. I used a local git+file:// repo of left-pad 1.3.0 because the sandbox can't reach github.com.

    echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","lp3":"git+file:///…/lpgit#v1.3.0"}}' > package.json
    yarn install
    # yarn.lock: "lp3@git+file:///…/lpgit#v1.3.0": version "1.3.0", resolved "git+file:///…/lpgit#b4d84b1…"
    Mode Copy warned in-run scan --vex lock-only vex node_modules/lp3 after fresh --frozen-lockfile
    hosted no (only redirect_yarn_classic_berry_migration_risk) not_affected not_affected unpatched
    vendored no not_affected not_affected unpatched

    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 as lp3@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 fetched package.json, so the fix for directories (reading the manifest) probably applies here too.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (yarn classic; a false not_affected VEX 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-name file: copy. No open PR covers it.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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

  4. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1242


    Generated by Claude Code

  5. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] #1242 is ready for review. It fixes the reported file: directory shape, plus file: tarball and registry-URL copies under another name, in vex (lock-only and in-run) and in the hosted and vendored scan warnings. It says Refs rather than Fixes, and this issue stays open for two follow-ups:

    • a git copy under another name (the comment above): yarn 1's lock records no package name for it, so a lock-only reader can't tell it is left-pad. Post-install vex already catches it. A lock-only fix needs a policy decision.
    • the hosted/vendored scan warning for a renamed file: tarball (VEX already handles it).

    Generated by Claude Code

  6. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] One more follow-up, raised in #1242's final review: in a workspace, a member's file: copy under another name (packages/a declaring "lp2": "file:./lpdir") isn't covered yet. #1242 reads the file: 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

  7. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    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.

  8. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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" (where lpdir is left-pad@1.3.0) on yarn 1.22.22:

    • hosted scan: exit 0, pins the registry copy and warns redirect_yarn_classic_directory_skipped. Both installed and lock-only vex refuse with patched_ref_unattributable naming "lp2@file:./lpdir" (exit 2).
    • vendored scan: exit 0 with vendor_link_entry_skipped. vex refuses with vendor_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/a declaring "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 (root lp2 set to npm:left-pad@1.1.0). Hosted scans warn redirect_yarn_classic_directory_skipped, and vex refuses on all three releases. So #1242's root-relative reading matches what yarn 1 writes.

    One caveat: a lock-only vex in a checkout missing lpdir/ can't read its package.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

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