Skip to content

Yarn berry vendored and hosted pins copy the registry entry's bin: paths, but yarn re-reads them from the tarball (./dist/bin/uuid), so vendored installs and hardened hosted installs fail YN0028 for packages like uuid and acorn #718

Description

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

Summary

Both Yarn Berry pin shapes rebuild the lock entry by copying the registry npm: entry's body. Hosted copies everything after the key line (patch/redirect/mod.rs:3788-3791). Vendored copies carried_sections (vendor/yarn_berry_lock.rs:338, :357). For an npm: locator, yarn builds that body from the registry metadata. For the new tarball-URL locator (hosted) and file: locator (vendored), yarn builds it from the tarball's own package.json.

Those two sources often differ in bin:. The registry metadata has normalized paths (bin/acorn, dist/bin/uuid), while the published package.json keeps the leading ./ (./bin/acorn, ./dist/bin/uuid). Whenever yarn re-resolves the pinned entry, its rendered lock differs from what socket-patch wrote:

➤ YN0028: │ -    uuid: dist/bin/uuid
➤ YN0028: │ +    uuid: ./dist/bin/uuid
➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
  • Vendored: yarn re-resolves file: entries on every install, so every fresh yarn install --immutable fails, hardened or not.
  • Hosted: a plain --immutable install trusts the locked entry and passes. Hardened mode (enableHardenedMode, which yarn turns on by itself for CI on public PRs) implies --refresh-lockfile, so it fails. yarn install --immutable --refresh-lockfile fails the same way.

Impact

This affects common packages: of 20 popular packages with bins I checked, 6 differ ([email protected], [email protected], [email protected], [email protected], [email protected], [email protected]). For those packages:

  • Vendored mode bricks CI for every Yarn 4 project.
  • Hosted mode bricks every hardened install, including public-fork PR CI.

scan/vendor report success with no warning. The unpatched project passes the same installs (control below).

Repro

This uses a local mock of the patch API: batch, patches/package with tarball + yarn-berry-zip yarnBerry10c0, view, and the tarball route. The patched tgz is the published tarball with a marker prepended to dist/index.js, and its 10c0 checksum is bootstrapped with a real yarn resolutions: file: install.

mkdir app && cd app
echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"uuid":"^9.0.0"}}' > package.json
printf 'nodeLinker: node-modules\nenableGlobalCache: false\n' > .yarnrc.yml
echo node_modules/ > .gitignore; touch yarn.lock; yarn install
git init -q && git add -A && git commit -qm init
# hosted (or: --mode vendored)
socket-patch scan --mode hosted --json --yes --api-url http://127.0.0.1:18765 --org org --api-token x
git add -A && git commit -qm pin
git clone -q . ../fresh && cd ../fresh
YARN_UNSAFE_HTTP_WHITELIST=127.0.0.1 yarn install --immutable                              # hosted: ok, vendored: YN0028
YARN_UNSAFE_HTTP_WHITELIST=127.0.0.1 YARN_ENABLE_HARDENED_MODE=1 yarn install --immutable  # both: YN0028

You can reproduce it without socket-patch. Re-key the registry entry exactly the way hosted mode does: add "resolutions": {"uuid@npm:9.0.1": "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz"}, change the lock key and resolution: to that URL, and keep the body. YARN_ENABLE_HARDENED_MODE=1 yarn install --immutable then fails with the same diff. A yarn-generated lock for that same resolutions entry carries uuid: ./dist/bin/uuid.

Expected vs actual

  • Expected: docs/testing/yarn-berry-compatibility.md says the hosted pin "is what yarn itself writes for a root resolutions entry" and that "hosted mode assumes every berry project may run hardened". The vendored entry is documented as "the exact entry yarn 4 emits for a resolutions-driven file: tarball" (vendor/yarn_berry_lock.rs:349). Both pins should therefore pass a fresh yarn install --immutable, hardened included, just as the unpatched lock does.
  • Actual: the bin: map is the registry's, not the tarball's. Yarn rewrites it, and --immutable fails YN0028.

Matrix (Linux, node-modules linker, fresh clone)

yarn package hosted --immutable hosted hardened vendored --immutable vendored hardened unpatched hardened (control)
4.0.2 [email protected] pass YN0028 YN0028 YN0028 pass
4.12.0 [email protected] pass YN0028 YN0028 YN0028 pass
4.18.1 [email protected] pass YN0028 YN0028 YN0028 pass
4.18.1 [email protected] pass YN0028 YN0028 YN0028 pass

Packages whose registry and tarball bin: agree (left-pad, semver, js-yaml, which, mkdirp, …) are unaffected, which is why the existing e2e fixtures (left-pad) pass.

First bad version

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:3788-3791: "every other line — version, dependencies, bin, languageName — carries over verbatim".
  • crates/socket-patch-core/src/vendor/yarn_berry_lock.rs:336-357: carried_sections copies bin: (and the dependency maps) from the registry entry.

The bin: (and arguably the dependency maps) should be derived the way yarn derives them from the served tarball's package.json, or the pin should be refused when they differ. Note that vendor_dep_manifest_stale only fires when the patch itself edits package.json.

No probe run: probe-branch deletes are still failing for this routine (see the ledger), so macOS and Windows are untested. Nothing here is OS-specific.

Activity

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