Skip to content

Hosted yarn berry redirect of resolve/typescript (yarn builtin compat patch) reports success, then every yarn install --immutable fails YN0028 #368

Description

[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)

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

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions