Skip to content

Yarn berry drops a relative portal: / link: copy whose target directory is missing (e.g. an uninitialized git submodule), so hosted and vendored scans succeed and VEX attests not_affected while that copy installs unpatched #1561

Closed

Description

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

Summary

When a yarn berry portal: / link: dependency under another name ("lp2": "portal:./vend/lp") points at a relative directory that doesn't exist at scan or vex time, socket-patch drops that copy. It's then neither named as a copy nor treated as unidentified. Hosted and vendored scans report success, and the in-run and lock-only VEX attest not_affected. Once the directory exists and yarn installs, node_modules/lp2 holds unpatched left-pad@1.3.0.

The file: twin ("lp2": "file:./vend/lp", same missing directory) fails closed as #1516 intends: the scan warns redirect_copy_unpatched and VEX refuses. Only the linked protocols fail open.

A realistic way to hit this is a portal target that is a git submodule. actions/checkout doesn't fetch submodules by default, so a lock-only socket-patch vex or scan in CI sees no vend/lp, while the build that installs with submodules ships the unpatched copy. A portal into a generated or gitignored directory works the same way.

Impact

This produces a false not_affected VEX statement and a clean scan, while the shipped tree holds an unpatched copy of the patched package@version. It's the same fail-open class as #1516 and #1540, through a path those fixes left open.

Repro (Linux, real yarn 4.18.1 / 4.0.2)

# project: dependencies {"left-pad":"1.3.0","lp2":"portal:./vend/lp"}, nodeLinker: node-modules
mkdir -p vend && npm pack left-pad@1.3.0 && tar xzf left-pad-1.3.0.tgz && mv package vend/lp
yarn install                      # lock: "lp2@portal:./vend/lp::locator=app%40workspace%3A." (linkType: soft)
# lock-only checkout without the portal target (submodule not checked out):
mkdir ../co && cp package.json yarn.lock .yarnrc.yml ../co/
socket-patch scan --mode hosted --json --yes --cwd ../co --api-url $API --org org --api-token x \
  --vex ../co/out.vex.json        # exit 0, status success, no redirect_copy_unpatched; VEX: not_affected pkg:npm/left-pad@1.3.0
socket-patch vex --cwd ../co ...  # exit 0, 1 statement, not_affected
# restore the target, then install:
cp -r vend ../co/ && (cd ../co && yarn install --immutable)
head -c 60 ../co/node_modules/lp2/index.js   # unpatched left-pad 1.3.0

I used a wiremock patch API (the berry e2e fixture from e2e_redirect_yarn_berry_build.rs with a marker-prepended index.js) and the real yarn bundles from @yarnpkg/cli-dist.

Expected vs actual

docs/ecosystems.md ("yarn copies under another dependency name"): "A copy whose package can't be read (a non-registry URL, a missing directory) is treated as possibly any package at its lock version … and vex attests nothing that lock wires", and "a portal: / link: copy whose package.json isn't read has no version either, so it may be any version of any package and vex attests nothing that lock wires".

Matrix (Linux, main 7b6371c)

Cell yarn 4.0.2 yarn 4.18.1
portal:./vend/lp missing, lock-only vex fail (exit 0, attests) fail ×2
link:./vend/lp missing, lock-only vex – fail
portal: missing, scan --mode hosted --vex fail (exit 0, attests) fail
link: missing, scan --mode hosted --vex – fail
portal: missing, scan --mode vendored --vex – fail (exit 0, attests)
link: missing, scan --mode vendored --vex fail –
Control: file:./vend/lp missing, hosted / vendored / vex pass (fails closed) pass
Control: portal:/link: target present pass (redirect_copy_unpatched, vex refuses) pass
#1540 absolute portal: (target present) pass pass

In every failing cell, a later yarn install --immutable with the target restored puts unpatched bytes in node_modules/lp2 and patched bytes in node_modules/left-pad.

macOS and Windows weren't probed. The code path is OS-independent.

First bad

The copy has been dropped since berry renamed-copy detection landed (#1489). #1551 (fix for #1540) widened the fail-closed path only to absolute (read_path() == None) linked copies. A relative linked copy whose read fails still hits the old continue.

Suspect code

  • crates/socket-patch-core/src/vex/discover/yarn.rs:894-897: if matches!(copy.source, CopyRef::Linked(_)) && !unread_linked { continue; } treats "read failed: NotFound / not a directory" the same as "read and found to hold no versioned package".
  • crates/socket-patch-core/src/vendor/yarn_copies.rs:113: the same condition on the vendored and hosted side.
  • crates/socket-patch-core/src/formats/yarn/copy.rs:64 is_unread_linked only covers anchored paths.

To keep the #1551 review intent (a link: to an existing plain directory with no package.json stays ignored), the check probably needs to tell "the directory exists but has no manifest" apart from "the directory is missing or unreadable".

Activity

  1. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (yarn berry, npm family). Confirmed on main 7b6371c: CopyRef::is_unread_linked (formats/yarn/copy.rs) is true only for an anchored path, so a relative portal: / link: copy whose package.json read fails (missing or unreadable target) hits the "read and found to hold no versioned package" skip in both vendor/yarn_copies.rs (other_name_copies, None if Linked && !is_unread_linked => continue) and vex/discover/yarn.rs (record_copies). The fix needs to tell "the read failed" apart from "the manifest was read and names no versioned package". Follows #1540 / #1551, not a duplicate. No open PR addresses it.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: a relative portal: / link: copy whose target read fails is treated like one read and found to hold no versioned package). Branch: agent/fix-yarn-berry-missing-linked-copy. Claim-ID: 2026-10-15T08:21:15Z-625c53


    Generated by Claude Code

  3. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1571


    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