[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.
[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 offlinerollbackof one of those still-active patches aborts withmissing_blob, and the package stays patched.A scoped
rollback <purl>does pin them. Its GC pins "EVERY entry remaining in the post-removal manifest". Sorollback mskeeps left-pad's revert data, andremove msdestroys 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
--offlinesetup, 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, andremoveexits 0 without warning (blobsRemoved: 3in the envelope, where only the 2 blobs belonging to the removed patch should go).removeand scopedrollbackbehave inconsistently, although both are single-patch operations.Repro (Linux, yarn 4.12.0, node-modules linker)
Control: replace the
removewithsocket-patch rollback pkg:npm/ms@2.1.3 --offline. Afterwards.socket/blobsstill holds left-pad's before blobad1fd20…along with its after blob, and a later left-pad rollback works.In vendored (manifest-mode
vendor) projects,remove msalso deletes left-pad's before blob (blobsRemoved: 3).Expected vs actual
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 "removeparity", so both commands are meant to share this GC posture.removeis the single-patch counterpart of a scoped rollback.removesweeps every beforeHash blob except those of not-installed removed entries, including the revert data of patches it did not touch.Matrix
61cfb9b61cfb9b(2/2: agent and vendored)61cfb9bNot 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_purlsholds onlyretained_not_installed.crates/socket-patch-cli/src/commands/rollback.rs:1680-1684, which chainsupdated_manifest.patches.keys()into the pinned set beforepin_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.