[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).
Summary
On a yarn 4 project, scan --mode hosted (and get --mode hosted) for a package that yarn itself builtin-patches (resolve, typescript, fsevents) rewrites only the plain <pkg>@npm:<v> source entry in yarn.lock. It leaves the <pkg>@patch:<pkg>@npm%3A<v>#optional!builtin<compat/…> entry alone, and that is the entry every dependent actually installs. The command exits 0 with status: success and redirected: 1, and the in-run VEX attests not_affected. The only sign of trouble is a redirect_yarn_berry_unsupported_protocol warning.
The patch: entry's resolution embeds the source locator, so the lock is no longer self-consistent. The next yarn install --immutable fails with YN0028 ("The lockfile would have been modified by this install, which is explicitly forbidden"). That includes every CI install, because yarn turns enableImmutableInstalls on when CI is set. A plain yarn install rewrites the patch: entry to wrap the hosted locator, so the package only gets patched if someone runs an unfrozen install and commits the lock afterwards.
Impact
resolve and typescript are among the most common transitive dependencies in the npm ecosystem, and yarn applies its compat patch to them in every berry project on every linker. For any hosted patch to one of them, the committed lock breaks frozen installs.
- The hosted run reports success and writes a
not_affected VEX for a lock that can't install. CLI_CONTRACT / the testing guidance count "a rewrite that looks right but makes the next frozen install fail" as a failure.
- Vendored mode refuses the same package fail-closed (
vendor_override_conflict, "resolves resolve through a protocol vendor cannot own"). Hosted mode should fail closed the same way, or rewrite the patch: entry too.
Repro (Linux, yarn 4.12.0; mock patch API)
The self-contained script is in the probe workflow linked below (probe.sh, case A-resolve). In short:
mkdir proj && cd proj
echo '{"name":"app","version":"1.0.0","dependencies":{"resolve":"1.22.8"}}' > package.json
printf 'nodeLinker: node-modules\nenableGlobalCache: false\nunsafeHttpWhitelist:\n - "127.0.0.1"\n' > .yarnrc.yml
touch yarn.lock && yarn install # lock has resolve@npm:1.22.8 AND resolve@patch:…#optional!builtin<compat/resolve>
# mock API (batch / by-package / patches/package with a yarn-berry-zip 10c0 checksum / view / tarball route),
# serving resolve-1.22.8 with a marker prepended to index.js; checksum bootstrapped with a real yarn file: install
socket-patch scan --mode hosted --json --yes --api-url http://127.0.0.1:18801 --org test-org --api-token fake \
--vex out.vex.json --vex-product pkg:npm/app@1.0.0
# -> exit 0, status success, redirected 1, warning redirect_yarn_berry_unsupported_protocol, VEX not_affected
# fresh checkout (package.json, yarn.lock, .yarnrc.yml, .socket):
yarn install --immutable
Actual output:
➤ YN0028: │ - resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin<compat/resolve>::version=1.22.8&hash=c3c19d"
➤ YN0028: │ + resolution: "resolve@patch:resolve@npm%3A1.22.8%3A%3A__archiveUrl=http%253A%252F%252F127.0.0.1…%252Fresolve-1.22.8.tgz#optional!builtin<compat/resolve>::version=1.22.8&hash=c3c19d"
➤ YN0028: │ - checksum: 10c0/0446f024…
➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
It's the same with typescript@5.4.5 (the compat/typescript entry). The control case (left-pad@1.3.0, no builtin patch) passes: the fresh --immutable install gets the patched bytes.
Expected vs actual
- Expected: docs/testing/yarn-berry-compatibility.md and docs/ecosystems.md ("yarn berry — the redirect edits the yarn.lock entry …") say hosted mode produces a lock that a fresh
yarn install --immutable installs with the patched bytes. That's also what the e2e capstones assert. If the tool can't do that, it should refuse fail-closed, as vendored does for the same package: non-zero exit, redirected: 0, and no not_affected VEX.
- Actual: exit 0, success,
not_affected, and a lock that fails every frozen install.
Matrix
| OS |
yarn |
resolve (builtin compat patch) |
left-pad (control) |
| Linux (sandbox) |
4.0.2 |
fails (YN0028) |
pass |
| Linux (sandbox) |
4.12.0 |
fails (YN0028); typescript 5.4.5 fails too |
pass |
| Linux (sandbox) |
4.18.1 |
fails (YN0028) |
pass |
| macOS (probe) |
4.12.0, 4.18.1 |
fails (YN0028) |
pass |
| Windows (probe, CRLF lock written by yarn) |
4.12.0, 4.18.1 |
fails (YN0028) |
pass |
Yarn 2/3 (cacheKey 7/8) aren't affected: hosted mode refuses them with redirect_yarn_berry_cache_unsupported.
First bad release: socket-patch 4.0.0 (npm) already behaves this way. 3.3.0's hosted scan can't run against this API shape, so it isn't comparable.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:4127 (the protocol gate in rewrite_yarn_berry, which skips patch: entries with only a warning, at around line 4185) together with the plain npm: entry rewrite. When a patch: entry wraps the redirected npm: locator, the result is inconsistent. The hermetic test berry_hosted_redirect_of_builtin_patched_package_skips_patch_entry (crates/socket-patch-core/src/vendor/yarn_layering_tests.rs) pins this behaviour, but it never runs a real yarn install.
Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36764922521 (its case outputs are in the Probe step log for each OS job)
[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).
Summary
On a yarn 4 project,
scan --mode hosted(andget --mode hosted) for a package that yarn itself builtin-patches (resolve,typescript,fsevents) rewrites only the plain<pkg>@npm:<v>source entry inyarn.lock. It leaves the<pkg>@patch:<pkg>@npm%3A<v>#optional!builtin<compat/…>entry alone, and that is the entry every dependent actually installs. The command exits 0 withstatus: successandredirected: 1, and the in-run VEX attestsnot_affected. The only sign of trouble is aredirect_yarn_berry_unsupported_protocolwarning.The
patch:entry's resolution embeds the source locator, so the lock is no longer self-consistent. The nextyarn install --immutablefails with YN0028 ("The lockfile would have been modified by this install, which is explicitly forbidden"). That includes every CI install, because yarn turnsenableImmutableInstallson whenCIis set. A plainyarn installrewrites thepatch:entry to wrap the hosted locator, so the package only gets patched if someone runs an unfrozen install and commits the lock afterwards.Impact
resolveandtypescriptare among the most common transitive dependencies in the npm ecosystem, and yarn applies its compat patch to them in every berry project on every linker. For any hosted patch to one of them, the committed lock breaks frozen installs.not_affectedVEX for a lock that can't install. CLI_CONTRACT / the testing guidance count "a rewrite that looks right but makes the next frozen install fail" as a failure.vendor_override_conflict, "resolves resolve through a protocol vendor cannot own"). Hosted mode should fail closed the same way, or rewrite thepatch:entry too.Repro (Linux, yarn 4.12.0; mock patch API)
The self-contained script is in the probe workflow linked below (
probe.sh, caseA-resolve). In short:Actual output:
It's the same with
typescript@5.4.5(thecompat/typescriptentry). The control case (left-pad@1.3.0, no builtin patch) passes: the fresh--immutableinstall gets the patched bytes.Expected vs actual
yarn install --immutableinstalls with the patched bytes. That's also what the e2e capstones assert. If the tool can't do that, it should refuse fail-closed, as vendored does for the same package: non-zero exit,redirected: 0, and nonot_affectedVEX.not_affected, and a lock that fails every frozen install.Matrix
Yarn 2/3 (cacheKey 7/8) aren't affected: hosted mode refuses them with
redirect_yarn_berry_cache_unsupported.First bad release: socket-patch 4.0.0 (npm) already behaves this way. 3.3.0's hosted scan can't run against this API shape, so it isn't comparable.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:4127(the protocol gate inrewrite_yarn_berry, which skipspatch:entries with only a warning, at around line 4185) together with the plainnpm:entry rewrite. When apatch:entry wraps the redirectednpm:locator, the result is inconsistent. The hermetic testberry_hosted_redirect_of_builtin_patched_package_skips_patch_entry(crates/socket-patch-core/src/vendor/yarn_layering_tests.rs) pins this behaviour, but it never runs a real yarn install.Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36764922521 (its case outputs are in the
Probestep log for each OS job)