Skip to content

In a yarn classic Plug'n'Play project, vex attests a hosted patch as not_affected while the copy .pnp.js loads is still unpatched #519

Description

[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).

Summary

Yarn classic 1.12+ supports Plug'n'Play ("installConfig": {"pnp": true} in package.json). Installs then write a .pnp.js loader that resolves packages straight out of the yarn cache, and there's no node_modules/. scan already knows it can't see these packages: every mode emits yarn_pnp_unsupported ("socket-patch cannot discover or patch them in ANY mode").

Standalone socket-patch vex doesn't apply that refusal. Once yarn.lock carries a hosted pin, for example committed from a lock-only CI checkout where no .pnp.js exists yet and the hosted rewrite goes through, vex sees no installed copy (the crawler can't look inside PnP). It then attests the patch from the lock pin, even though .pnp.js still loads the unpatched cache copy.

Impact

A signed-off not_affected statement for a vulnerable package that is actually running. A developer or CI job that pulls the hosted lock and runs vex before yarn install gets a false attestation. Nothing warns, and vex exits 0.

Repro

# 1. a PnP project, installed (the .pnp.js loads the registry bytes)
mkdir dev && cd dev
echo '{"name":"app","version":"1.0.0","private":true,"installConfig":{"pnp":true},"dependencies":{"left-pad":"1.3.0"}}' > package.json
yarn install
# 2. hosted rewrite on a lock-only checkout (no .pnp.js there, so no refusal)
mkdir ../ci && cp package.json yarn.lock ../ci && (cd ../ci && socket-patch scan --mode hosted --json --yes)   # redirected: 1
cp ../ci/yarn.lock yarn.lock          # e.g. `git pull` of the CI commit
# 3. attest before reinstalling
node -r ./.pnp.js -e "console.log(require('fs').readFileSync(require.resolve('left-pad'),'utf8').slice(0,30))"
#   -> "/* This program is free softwa"  (unpatched)
socket-patch vex --json --output v.json
#   -> exit 0, events: [{action: "verified", purl: "pkg:npm/left-pad@1.3.0", status: "not_affected"}]

I ran this against a local mock patch API (--api-url, SOCKET_PATCH_SERVER_URL).

Expected vs actual

  • Expected: CLI_CONTRACT.md (VEX table, (redirected) row) says a post-install vex "hash-verifies the installed copy the build consumes — or, with nothing installed, attests a discovered lockfile reference from its integrity pin". The "Manifest-less VEX" row adds that "installed evidence wins", and that a purl the crawler didn't look at (the --ecosystems case) must stay omitted because "not installed" has to mean the crawler looked. In a PnP project the crawler structurally can't look; scan's own yarn_pnp_unsupported warning says so. vex should therefore omit the purl (or refuse with yarn_pnp_unsupported), not attest it. A default node_modules project in the same state is correctly omitted with not_applied (exit 1).
  • Actual: vex treats the PnP-installed copy as absent and attests not_affected from the lock pin.

Matrix (Linux, main 61cfb9b)

OS yarn stale .pnp.js + hosted pin → vex
Linux 1.12.3 attests (exit 0)
Linux 1.17.3 attests (exit 0)
Linux 1.22.22 attests (exit 0, reproduced 3×)
Linux 1.22.22, default node_modules (control) omitted, not_applied (exit 1)
macOS / Windows — untested. The decision is OS-independent (no installed copy found → lock-basis excuse)

After a real yarn install --frozen-lockfile, .pnp.js loads the patched hosted copy, so the attestation becomes true. The bug is the window where it isn't.

First bad: release 4.0.0 isn't affected: the same vex exits 2 with manifest_not_found, because it has no manifest-less VEX. The bug arrives with the v5 lockfile-basis attestation on main.

Suspect code

Activity

  1. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] New information from the Yarn Berry bug-hunt routine (ledger #305): this also reproduces on yarn berry PnP, on main 61cfb9b, Linux.

    Setup: a nodeLinker: pnp project installed from the registry lock, so .pnp.cjs and .yarn/cache hold the unpatched left-pad@npm:1.3.0. Then the hosted lock from a lock-only checkout is pulled in (left-pad@npm:1.3.0::__archiveUrl=… plus its 10c0 checksum), and standalone vex --output v.json --product pkg:npm/app@1.0.0 runs before yarn install. I used the repo's own e2e_redirect_yarn_berry_build fixture (wiremock patch API, real 10c0 checksum bootstrap).

    OS yarn node -r ./.pnp.cjs loads standalone vex
    Linux 4.0.2 unpatched verified / not_affected "(redirected)", exit 0 (2/2 runs)
    Linux 4.12.0 unpatched verified / not_affected, exit 0 (2/2)
    Linux 4.18.1 unpatched verified / not_affected, exit 0 (2/2)
    Linux 4.12.0, after yarn install --immutable patched (attestation now true)

    Berry-specific notes for the fix:

    • docs/testing/yarn-berry-compatibility.md:11 says that under PnP "standalone vex still attests a hosted lock's checksum: pin", and the pnp-linker cell in tests/yarn_berry_common/mod.rs:958 asserts it, but only after a fresh install. So on berry, a blanket "PnP ⇒ not crawled" fix like the one proposed above would flip a documented, tested contract. A narrower check is possible on berry: a stale install is detectable, because the .pnp.cjs package registry (and .yarn/install-state.gz) still names the registry locator left-pad@npm:1.3.0, not the ::__archiveUrl= locator that the lock now pins.
    • The suspect code is the same: crates/socket-patch-cli/src/commands/vex.rs:603-611 (package_not_found + lockfile_basis + crawled()).

    Generated by Claude Code

  2. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (yarn classic and berry, npm family). Not a duplicate, and no open fix PR.

    Related to #493 and #518 (also "vex reads an uncrawled install as absent"), but the cause is different. Those two have a real node_modules tree that crawl-root discovery misses, and they'll be fixed in the crawler. Under PnP there is no tree to crawl. The fix belongs in the lockfile-basis excuse in crates/socket-patch-cli/src/commands/vex.rs: a PnP loader (.pnp.js / .pnp.cjs) should count as "not crawled" for npm purls. On berry it needs the narrower stale-install check from the comment above, so the documented pnp-linker contract still holds after a fresh install. Left unclustered.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triage on main e61a845, after #978 changed PnP detection to follow nodeLinker: this still reproduces. Yarn 1.22.22, Linux, run twice. The issue's exact repro gives the same result: a lock-only CI checkout pins (redirected: 1), node -r ./.pnp.js still loads the unpatched left-pad, and socket-patch vex exits 0 with verified / not_affected for pkg:npm/left-pad@1.3.0. #978 correctly treats a loader next to a classic lock as live, but vex never asks. PR #1033 is still open.


    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

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions