Skip to content

Hosted yarn classic refuses a patch that doesn't change dependencies when the registry metadata's ranges differ from the tarball's package.json (redirect_yarn_classic_dep_manifest_unlocked) #1578

Closed

Description

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

Summary

The #591 check in hosted yarn classic compares every name@range in the served tarball's package.json against the yarn.lock keys, and refuses the pin when one isn't keyed. But yarn 1 keys a package's dependency blocks by the ranges in the registry metadata (packument). It writes the block's dependencies: sub-map from the tarball's package.json. When the two disagree, the check fires on a patch that doesn't touch package.json at all.

Real example: meteor-node-stubs@1.2.3 (also 1.2.2). Its packument lists 23 dependencies as "*", and its tarball package.json lists real ranges (url: ^0.11.0, …). On yarn 1.7.0, 1.10.1 and 1.22.22 the lock says:

meteor-node-stubs@1.2.3:
  ...
  dependencies:
    url "^0.11.0"        # from the tarball
...
url@*:                   # key from the packument

The hosted scan then reports redirect_unconfirmed plus redirect_yarn_classic_dep_manifest_unlocked: "the patched meteor-node-stubs@1.2.3 depends on assert@^2.0.0, browserify-zlib@^0.2.0, … which yarn.lock does not lock … Lock them first (for example yarn add …)". The patch only changes package/deps/assert.js. The lock is left unpinned, and the scan exits 0 success. The suggested remedy doesn't help. The pin itself works fine:

  • v4.0.0 (before the check) writes the pin.
  • yarn install --frozen-lockfile installs the patched bytes.
  • Main's own vex attests that pin, both installed and lock-only.

Main's rescan of that v4 pin reports it redirect_unconfirmed ("an earlier run's pin of it is not confirmed"), so scan and vex disagree about the same lock.

A registry scan (packument vs tarball, about 1,500 packages) found more packages of this kind. Most are git dependencies that npm's publish-time normalisation rewrites in the packument (user/repo → github:user/repo, https://github.com/x/y → git+https://github.com/x/y.git): grunt-webfonts@1.1.3, gulp-webdriver@1.0.3, liquidjs-lib@5.1.6, xmlshim@0.2.7, @bitgo/utxo-lib@1.7.x, among others. They probably hit the same false refusal. That's not reproduced here, because the sandbox can't reach codeload.github.com.

Vendored mode is unaffected for a patch that doesn't list package/package.json: it wires the lock, and the fresh frozen install is patched.

Impact

Hosted mode can never patch these packages, and gives a misleading remedy. The run still exits 0 success, so CI pipelines report the package as handled when the warning is easy to miss.

Repro (Linux, yarn 1.22.22; same on 1.7.0 / 1.10.1)

mkdir p && cd p
echo '{"name":"r","version":"1.0.0","private":true,"dependencies":{"meteor-node-stubs":"1.2.3"}}' > package.json
yarn install
grep -n '^url@' yarn.lock                                   # url@*:
awk '/^meteor-node-stubs@/,/^$/' yarn.lock | grep ' url '   #     url "^0.11.0"
# Mock patch API serving a meteor-node-stubs@1.2.3 patch that only prepends a comment to package/deps/assert.js
# (the run-18 ledger mock with MN=meteor-node-stubs MV=1.2.3 MF=package/deps/assert.js)
socket-patch scan --mode hosted --json --yes
#   events: redirect_unconfirmed pkg:npm/meteor-node-stubs@1.2.3
#   warnings: redirect_yarn_classic_dep_manifest_unlocked "... depends on assert@^2.0.0, ..., url@^0.11.0, ... which yarn.lock does not lock"
#   exit 0, yarn.lock unchanged

Reproduced twice per version.

Expected vs actual

  • Expected: docs/ecosystems.md (hosted yarn classic) and CLI_CONTRACT redirect_yarn_classic_dep_manifest_unlocked say the refusal is for a patch that "adds a dependency (or changes a range) that no yarn.lock block locks", because "a pin would install the patched package without that dependency". Here the patch changes no dependency, and yarn installs every one of them through the * blocks, so the entry should be pinned. That's what v4.0.0 did, and main's vex agrees the pin is good.
  • Actual: the pin is refused (or an existing pin is never confirmed) with a remedy that can't clear it.

Matrix (Linux; the other OSes don't matter, this is lock-text logic)

yarn hosted main e76cb9b hosted v5.0.1 hosted v4.0.0 vendored main
1.7.0 refused – – –
1.10.1 refused – – –
1.22.22 refused (×2) refused pins, frozen install patched pass, frozen install patched

First bad release: v5.0.1 (the #591 fix; v5.0.0 isn't on npm). v4.0.0 pins it correctly.

Suspect code

crates/socket-patch-core/src/patch/redirect/mod.rs:4144: unlocked_descriptors_in(locked, pkg) checks the tarball manifest's descriptors against the lock keys. A narrower check would refuse only descriptors where the tarball's manifest differs from the block's current sub-map (what the patch actually changes), since the existing sub-map entries are already locked under whatever pattern yarn resolved them by. The same assumption probably sits in vendored vendor_dep_manifest_unlocked for a patch that does list package/package.json (vendor/yarn_classic_lock.rs).

No probe branch was needed.

Activity

  1. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (Yarn classic). Not a duplicate. Related to #1558 (also yarn classic dependency-map checking) but a different code path: this one is the hosted refusal in unlocked_descriptors_in (patch/redirect/mod.rs), which checks tarball-manifest ranges against lock keys; #1558 is the lock-only VEX dep_maps_declare comparison. Not clustered. No open PR covers it yet.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 16, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #1558; shared root cause: formats/yarn/classic_deps.rs reads the patched package.json's dependency maps literally instead of the way yarn 1 records them in a lock block: it drops "" ranges, skips cleanDependencies normalisation, and ignores the descriptors the block's sub-map already carries). Branch: agent/fix-yarn-classic-dep-manifest-model. Claim-ID: 2026-10-16T00:20:55Z-09f30c


    Generated by Claude Code

  3. mikolalysenko commented on Oct 16, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1596


    Generated by Claude Code

  4. mikolalysenko commented on Oct 16, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Yarn classic bug-hunt run 43 re-check (Linux, local mock patch API, meteor-node-stubs@1.2.3, patch prepends a marker to package/deps/assert.js).

    New shape on main 657fce2: vendored is hit too when the patch also lists package/package.json. I tested a patch that changes only the description. scan --mode vendored exits 1 with vendor_dep_manifest_unlocked, and the lock stays unchanged. Seen on yarn 1.10.1 and 1.22.22.

    PR #1596 (head 4d3698c1) fixes every cell I re-ran:

    cell main 657fce2 PR #1596
    hosted, yarn 1.7.0 / 1.10.1 / 1.22.22 refused (redirect_yarn_classic_dep_manifest_unlocked), lock unchanged pinned; frozen install patched; installed and lock-only vex attest
    vendored + package.json in the record, yarn 1.10.1 / 1.22.22 exit 1 vendor_dep_manifest_unlocked wired (vendor_dep_manifest_rewritten); frozen install patched; both vex modes attest
    negative: the patch changes url to ^0.12.0 (unlocked) refused still refused, and the remedy names only url@^0.12.0
    negative: is-odd patch adds an unlocked kind-of refused still refused

    One side note, so nobody files it: a plain yarn install re-keys meteor-node-stubs' dependency blocks (url@* → url@^0.11.0, buffer 6.0.3 → 5.7.1). The unpatched lock does the same thing, so this is yarn 1's own packument-vs-tarball behaviour and not something socket-patch causes.


    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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions