[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)
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.
[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 copiescarried_sections(vendor/yarn_berry_lock.rs:338,:357). For annpm:locator, yarn builds that body from the registry metadata. For the new tarball-URL locator (hosted) andfile:locator (vendored), yarn builds it from the tarball's ownpackage.json.Those two sources often differ in
bin:. The registry metadata has normalized paths (bin/acorn,dist/bin/uuid), while the publishedpackage.jsonkeeps the leading./(./bin/acorn,./dist/bin/uuid). Whenever yarn re-resolves the pinned entry, its rendered lock differs from what socket-patch wrote:file:entries on every install, so every freshyarn install --immutablefails, hardened or not.--immutableinstall 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-lockfilefails 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:scan/vendorreport 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/packagewithtarball+yarn-berry-zipyarnBerry10c0, view, and the tarball route. The patched tgz is the published tarball with a marker prepended todist/index.js, and its10c0checksum is bootstrapped with a real yarnresolutions: file:install.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 andresolution:to that URL, and keep the body.YARN_ENABLE_HARDENED_MODE=1 yarn install --immutablethen fails with the same diff. A yarn-generated lock for that sameresolutionsentry carriesuuid: ./dist/bin/uuid.Expected vs actual
resolutionsentry" 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-drivenfile:tarball" (vendor/yarn_berry_lock.rs:349). Both pins should therefore pass a freshyarn install --immutable, hardened included, just as the unpatched lock does.bin:map is the registry's, not the tarball's. Yarn rewrites it, and--immutablefails YN0028.Matrix (Linux, node-modules linker, fresh clone)
--immutable--immutablePackages 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
uuid@npm:9.0.1::__archiveUrl=…pin, which stays annpm:locator (registry metadata), and its fresh hardened install passes with the patched bytes on yarn 4.18.1. The Fix yarn berry hosted pin leaking npm auth (#404) #465 tarball-locator shape fails.file:entry has always carried the registry body. I couldn't drive release 4.0.0's vendor flow against the mock, so this half isn't bisected.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_sectionscopiesbin:(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'spackage.json, or the pin should be refused when they differ. Note thatvendor_dep_manifest_staleonly fires when the patch itself editspackage.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.