Skip to content

Yarn berry still drops a portal: copy whose target has no package.json (e.g. a .gitkeep placeholder) or whose package.json lacks name/version, so scans succeed and VEX attests not_affected while that copy installs unpatched #1579

Closed

Description

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

Summary

#1571 (fix for #1561) treats a relative portal: / link: copy as unknown when its target is missing, empty, or has a package.json that isn't JSON. Two neighbouring shapes still fail open, on both the hosted and vendored paths and in lock-only vex:

  1. A portal: target that is a non-empty directory with no package.json. The real-world case is a placeholder directory committed with a .gitkeep, which a fetch or build step fills before the install. linked_copy_unknown exempts any non-empty directory without a manifest as "a link: to plain assets". That exemption can't apply to portal:: yarn refuses to install a portal without a manifest (YN0001 … lp2@portal:./vend/lp…: Manifest not found, on 4.0.2 and 4.18.1). So a lock that carries a portal: entry whose target has no package.json at scan time proves the target isn't what will be installed, and the copy is unknown.
  2. A portal: / link: target whose package.json parses but has no name or no version. CopyRef::identity needs both, so it returns None. manifest_parsed is true, so the copy is dropped as "names no versioned package". Yarn installs such a portal happily, and in my repro node_modules/lp2 holds unpatched left-pad 1.3.0 code. The file: twin with the same nameless manifest fails closed (redirect_copy_unpatched, vex exit 2), as 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 intends.

In every failing cell the hosted and vendored scans report success with no copy warning, and both in-run and lock-only VEX attest not_affected for pkg:npm/left-pad@1.3.0. After that, yarn install --immutable (with the target populated) puts patched bytes in node_modules/left-pad and unpatched bytes in node_modules/lp2.

Impact

These are false not_affected VEX statements and clean scans for a tree that ships an unpatched copy of the patched package@version. It's the same fail-open class as #1516, #1540 and #1561.

Repro (Linux, real yarn 4.18.1 and 4.0.2 from @yarnpkg/cli-dist)

# project: {"dependencies":{"left-pad":"1.3.0","lp2":"portal:./vend/lp"}}, .yarnrc.yml: 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." version 0.0.0-use.local, linkType: soft

# Shape 1: lock-only checkout, the portal target is a committed placeholder
mkdir ../co && cp package.json yarn.lock .yarnrc.yml ../co/ && mkdir -p ../co/vend/lp && touch ../co/vend/lp/.gitkeep
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, applied 1, no redirect_copy_unpatched; VEX: 1 statement not_affected
socket-patch vex --cwd ../co --api-url $API --org org --api-token x --patch-server-url $API -O ../co/lo.vex.json
#   exit 0, not_affected
(cd ../co && yarn install --immutable)     # with only .gitkeep: YN0001 "Manifest not found", so yarn can't install this as-is
rm -rf ../co/vend && cp -r vend ../co/ && (cd ../co && yarn install --immutable)   # the fetch step ran: Done
head -c 22 ../co/node_modules/lp2/index.js        # unpatched left-pad 1.3.0
head -c 22 ../co/node_modules/left-pad/index.js   # /* SOCKET-PATCHED */

# Shape 2: target present, package.json without "name" (or without "version")
python3 -c "import json;p='vend/lp/package.json';d=json.load(open(p));d.pop('name');json.dump(d,open(p,'w'))"
# same scan / vex: exit 0, success, not_affected; yarn install --immutable: Done, node_modules/lp2 unpatched

For the mock I used a Python stand-in for the patch API: batch, by-package, view, patches/package with tarball (real sha512) and yarn-berry-zip (bootstrapped 10c0 checksum) artifacts, and the tarball route. The patch prepends a marker to index.js.

Expected vs actual

docs/ecosystems.md ("yarn copies under another dependency name") says: "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. A link: to a non-empty directory with no package.json is not a package copy." It also says "A copy whose package can't be read … is treated as possibly any package".

  • Expected: a portal: copy whose target has no package.json is unknown, since the exemption is documented only for link:, and yarn can't install a manifest-less portal anyway. A linked copy whose manifest names no package or no version is also unknown, matching the file: directory twin. Hosted warns redirect_copy_unpatched, vendored warns vendor_copy_unpatched, and vex withholds the lock's packages.
  • Actual: the copy is dropped. Scans exit 0 with success, and every VEX path attests not_affected.

Matrix (Linux, main 2c82c77)

Cell yarn 4.0.2 yarn 4.18.1
portal: target = dir with only .gitkeep, hosted scan --vex + lock-only vex fail fail ×2
same, scan --mode vendored --vex + vex fail fail
portal: target package.json without name, hosted + vex fail fail ×2
portal: target package.json without version, hosted + vex fail fail ×2
same two, vendored + vex – fail
link: target without name / version (hosted + vex) fail fail
link: target with only .gitkeep dropped (documented as no package copy) same
#1561 shapes (missing, empty, LFS pointer), portal: hosted / vendored / vex pass (fixed) pass (fixed)
Control: file:./vend/lp with nameless package.json – pass (fails closed)

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

First bad

This has been present since berry renamed-copy detection landed (#1489). #1551 and #1571 narrowed it but left these shapes.

Suspect code

  • crates/socket-patch-core/src/vendor/yarn_copies.rs:143-163 (linked_copy_unknown): the "non-empty directory, no package.json" exemption applies to portal: as well as link:. CopyRef::Linked doesn't record which protocol it came from, so the two can't be told apart here. Also, if manifest_parsed { return false; } treats a parsed manifest without name/version as "no package copy".
  • crates/socket-patch-core/src/formats/yarn/copy.rs:73 (CopyRef::identity, Linked arm): needs both name and version, else None.
  • crates/socket-patch-core/src/vex/discover/yarn.rs:901 (record_copies) and vendor/yarn_copies.rs:118 share the decision, so hosted, vendored and vex all fail the same way.

Activity

  1. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (Yarn Berry). Not a duplicate: it is a follow-up to #1561 / #1571 covering two shapes that fix left open (a portal: target with no package.json, and a linked manifest without name / version), in linked_copy_unknown / CopyRef::identity. No open PR covers it yet. Same fail-open class as #1516 and #1540, which are closed.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: linked_copy_unknown / CopyRef::identity drop a portal: copy with no manifest, and a linked copy whose manifest lacks name/version, instead of treating it as unknown). Branch: agent/fix-yarn-portal-copy-unknown. Claim-ID: 2026-10-15T18:21:08Z-a649c7


    Generated by Claude Code

  3. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1586


    Generated by Claude Code

  4. added 2 commits that reference this issue on Oct 15, 2026
    3eeff9a
    bca69dd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions