Skip to content

remove <purl> garbage-collects the beforeHash blobs of every other patch still in the manifest, so a later offline rollback of those patches fails missing_blob #559

Description

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

Summary

socket-patch remove <purl> sweeps unused blobs against the post-removal manifest, but it pins beforeHash blobs only for the removed entries that were not installed. The sweep keeps only afterHash blobs, so it also deletes the beforeHash (revert) blobs of every patch that stays in the manifest. The next offline rollback of one of those still-active patches aborts with missing_blob, and the package stays patched.

A scoped rollback <purl> does pin them. Its GC pins "EVERY entry remaining in the post-removal manifest". So rollback ms keeps left-pad's revert data, and remove ms destroys it.

This isn't specific to yarn: the GC is PM-agnostic. I reproduced it with a real Yarn Berry install (node-modules linker), in both agent mode and v5 manifest-mode vendor.

Impact

  • In an air-gapped or --offline setup, or once the patch API no longer serves a blob, removing one patch makes every other applied patch impossible to roll back locally. The only local revert data is gone, and remove exits 0 without warning (blobsRemoved: 3 in the envelope, where only the 2 blobs belonging to the removed patch should go).
  • remove and scoped rollback behave inconsistently, although both are single-patch operations.

Repro (Linux, yarn 4.12.0, node-modules linker)

mkdir p && cd p
echo '{"name":"r","private":true,"dependencies":{"left-pad":"1.3.0","ms":"2.1.3"}}' > package.json
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
yarn install
# stage .socket/manifest.json with two patches (left-pad@1.3.0, ms@2.1.3; one file each)
# plus BOTH before and after blobs in .socket/blobs/
socket-patch apply --offline                        # exit 0, both patched
ls .socket/blobs                                    # 4 blobs (2 before, 2 after)
socket-patch remove pkg:npm/ms@2.1.3 --yes --offline   # exit 0, "blobsRemoved": 3
ls .socket/blobs                                    # only left-pad's AFTER blob is left
socket-patch rollback pkg:npm/left-pad@1.3.0 --offline
# exit 1: "Cannot roll back: package/index.js - Before blob not found: ad1fd20… and --offline prevents fetching"
head -c 18 node_modules/left-pad/index.js           # still /*SOCKET-MARKER*/

Control: replace the remove with socket-patch rollback pkg:npm/ms@2.1.3 --offline. Afterwards .socket/blobs still holds left-pad's before blob ad1fd20… along with its after blob, and a later left-pad rollback works.

In vendored (manifest-mode vendor) projects, remove ms also deletes left-pad's before blob (blobsRemoved: 3).

Expected vs actual

  • Expected: crates/socket-patch-cli/CLI_CONTRACT.md:829 (rollback GC) says beforeHash blobs are pinned for "EVERY entry remaining in the post-removal manifest — still-active patches … keep their revert data, so a scoped or failed run never destroys the blobs a later rollback needs; only blobs referenced solely by genuinely-removed entries are swept". It also describes the not-installed pin as "remove parity", so both commands are meant to share this GC posture. remove is the single-patch counterpart of a scoped rollback.
  • Actual: remove sweeps every beforeHash blob except those of not-installed removed entries, including the revert data of patches it did not touch.

Matrix

OS yarn socket-patch Reproduces
Linux 4.0.2 main 61cfb9b yes
Linux 4.12.0 main 61cfb9b (2/2: agent and vendored) yes
Linux 4.18.1 main 61cfb9b yes
Linux 4.12.0 release 4.0.0 (npm) yes
Linux 4.12.0 release 3.3.0 (npm) yes
macOS / Windows — — untested (the GC code path is OS-independent)

Not a regression: rollback's "pin every remaining entry" and remove's narrower pin both arrived in the same commit (a176aa7, #235).

Suspect code

  • crates/socket-patch-cli/src/commands/remove.rs:946-950: pinned_purls holds only retained_not_installed.
  • Compare crates/socket-patch-cli/src/commands/rollback.rs:1680-1684, which chains updated_manifest.patches.keys() into the pinned set before pin_before_hash_blobs.

No probe runs: the sandbox's git proxy can't delete probe branches right now, so this was tested on Linux only.

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