[agent] Found by the scheduled Cargo bug-hunt routine (ledger #315).
Summary
Take a crate that was first patched in agent mode, which writes into the shared $CARGO_HOME/registry/src cache, and later moved to vendored mode with socket-patch vendor. A bare rollback on that crate then does three things:
- It reverts the vendored copy.
- It skips the in-place restore for the purl, because vendor-owned purls are excluded from the agent leg.
- It drops the manifest entry and garbage-collects the before and after blobs.
The shared registry copy stays patched, and rollback exits 0 with success. Worse, the revert data is gone: a second rollback has nothing to do, and every other project on the machine that uses cfg-if 1.0.0 keeps building the patched bytes, with no socket-patch record left anywhere.
vendor itself also leaves the in-place patch behind without saying anything: no warning and no event. So an agent→vendored takeover never cleans up the cache edit.
Impact
rollback is documented as moving the system "toward fully unpatched". It also promises that "everything that leaves the system still patched DOES flip it to partial_failure exit 1". Here it reports success while a patched shared cache remains, and it deletes the only blobs that could restore it.
- The leftover edit silently changes the source of an unrelated project on the same
CARGO_HOME, which the second repro below shows. The only recovery is cargo clean/cache prune or deleting registry/src by hand.
Repro
The patch is a hand-staged .socket/manifest.json plus both blobs (stage.py is below). It appends pub fn socket_patched() to cfg-if 1.0.0.
W=$(mktemp -d); cd "$W"; export CARGO_HOME=$W/cargo-home
cargo init -q --name app --vcs none && cargo add -q cfg-if@=1.0.0 && cargo fetch -q
C=$(ls -d cargo-home/registry/src/*/cfg-if-1.0.0)
python3 stage.py . pkg:cargo/[email protected] "$C" src/lib.rs
socket-patch apply --json --offline # success; $C/src/lib.rs patched (grep -c socket_patched → 1)
socket-patch vendor --json --offline # success; no warning about the in-place edit; $C still patched
socket-patch rollback --json --offline # "status": "success", "rolledBack": 0, vendoredReverted: 1
grep -c socket_patched $C/src/lib.rs # 1 ← shared cache still patched
find .socket -type f # only manifest.json, with "patches": {} (blobs GC'd)
# an unrelated project on the same CARGO_HOME still compiles the patched crate:
mkdir other && cd other && cargo init -q --name other --vcs none && cargo add -q --offline cfg-if@=1.0.0
echo 'fn main(){ println!("patched={}", cfg_if::socket_patched()); }' > src/main.rs
cargo run -q --offline # patched=1
cd .. && socket-patch rollback --json --offline # "status": "success", nothing to restore
stage.py:
import sys, json, hashlib, os
proj, purl, src, rel = sys.argv[1:5]
def g(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest()
before = open(os.path.join(src, rel), "rb").read()
after = before + b"\n/// socket marker\npub fn socket_patched() -> u32 { 1 }\n"
s = os.path.join(proj, ".socket"); os.makedirs(os.path.join(s, "blobs"), exist_ok=True)
m = {"patches": {purl: {"uuid": "11111111-2222-4333-8444-555555555555",
"exportedAt": "2026-01-01T00:00:00Z", "files": {rel: {"beforeHash": g(before), "afterHash": g(after)}},
"vulnerabilities": {"GHSA-xxxx-xxxx-xxxx": {"cves": ["CVE-2024-1"], "summary": "s", "severity": "high", "description": "d"}},
"description": "m", "license": "MIT", "tier": "free"}}}
json.dump(m, open(os.path.join(s, "manifest.json"), "w"), indent=2)
for b in (before, after): open(os.path.join(s, "blobs", g(b)), "wb").write(b)
This was reproduced twice on the current main, with the same output both times.
Expected vs actual
-
Expected (CLI_CONTRACT.md, "Default behavior: full-state rollback"): a bare rollback "restores the SYSTEM to unpatched". GC keeps before-blobs for any entry whose revert is still needed, and anything left patched means partial_failure/exit 1. So either:
vendor reverts the in-place edit when it takes over an agent-applied purl (it has the before-blob), or
rollback's agent leg still restores installed copies whose on-disk hash equals the recorded afterHash, even when the purl is vendor-owned.
At a minimum, it shouldn't drop the manifest entry and GC the blobs while a patched copy is still on disk.
-
Actual: exit 0 success, the shared cache stays patched, and the revert data is deleted.
Matrix
| OS |
cargo |
main f6b7fb9 |
release 4.0.0 |
| Linux (sandbox) |
1.93.1 |
fail: cache left patched, manifest entry and blobs deleted, exit 0. Reproduced 2× |
fail, less severe: rollback reverts nothing (no vendored leg in 4.0.0) and the cache stays patched, but the manifest entry and blobs are kept, so the revert data survives |
The logic isn't OS-specific (it's the rollback partition and GC). macOS and Windows weren't probed.
First bad
The data loss (manifest entry and blobs removed while the cache is still patched) is new since 4.0.0. It comes from the v5 full-state rollback (vendored leg + manifest cleanup + GC, #231). The leftover in-place edit after vendor is present in 4.0.0 too.
Suspect code
crates/socket-patch-cli/src/commands/rollback.rs:2283-2292: vendor-owned purls are partitioned out of the in-place restore. The comment's premise ("their patch lives in the committed .socket/vendor/ artifact … not in the installed tree") doesn't hold after an agent→vendored takeover.
- The same file's manifest-cleanup and GC phases then treat the purl as fully rolled back.
crates/socket-patch-core/src/vendor/cargo.rs (vendor takeover) doesn't revert, or warn about, an existing in-place patch of the same purl.
This isn't strictly Cargo-only, since the rollback partition is ecosystem-agnostic. But Cargo is where the leftover is persistent and shared across projects: other ecosystems' installers usually overwrite the installed copy on the next install.
[agent] Found by the scheduled Cargo bug-hunt routine (ledger #315).
Summary
Take a crate that was first patched in agent mode, which writes into the shared
$CARGO_HOME/registry/srccache, and later moved to vendored mode withsocket-patch vendor. A barerollbackon that crate then does three things:The shared registry copy stays patched, and
rollbackexits 0 withsuccess. Worse, the revert data is gone: a secondrollbackhas nothing to do, and every other project on the machine that usescfg-if 1.0.0keeps building the patched bytes, with no socket-patch record left anywhere.vendoritself also leaves the in-place patch behind without saying anything: no warning and no event. So an agent→vendored takeover never cleans up the cache edit.Impact
rollbackis documented as moving the system "toward fully unpatched". It also promises that "everything that leaves the system still patched DOES flip it topartial_failureexit 1". Here it reportssuccesswhile a patched shared cache remains, and it deletes the only blobs that could restore it.CARGO_HOME, which the second repro below shows. The only recovery iscargo clean/cache prune or deletingregistry/srcby hand.Repro
The patch is a hand-staged
.socket/manifest.jsonplus both blobs (stage.pyis below). It appendspub fn socket_patched()tocfg-if 1.0.0.stage.py:This was reproduced twice on the current main, with the same output both times.
Expected vs actual
Expected (CLI_CONTRACT.md, "Default behavior: full-state rollback"): a bare
rollback"restores the SYSTEM to unpatched". GC keeps before-blobs for any entry whose revert is still needed, and anything left patched meanspartial_failure/exit 1. So either:vendorreverts the in-place edit when it takes over an agent-applied purl (it has the before-blob), orrollback's agent leg still restores installed copies whose on-disk hash equals the recordedafterHash, even when the purl is vendor-owned.At a minimum, it shouldn't drop the manifest entry and GC the blobs while a patched copy is still on disk.
Actual: exit 0
success, the shared cache stays patched, and the revert data is deleted.Matrix
f6b7fb9rollbackreverts nothing (no vendored leg in 4.0.0) and the cache stays patched, but the manifest entry and blobs are kept, so the revert data survivesThe logic isn't OS-specific (it's the rollback partition and GC). macOS and Windows weren't probed.
First bad
The data loss (manifest entry and blobs removed while the cache is still patched) is new since 4.0.0. It comes from the v5 full-state rollback (vendored leg + manifest cleanup + GC, #231). The leftover in-place edit after
vendoris present in 4.0.0 too.Suspect code
crates/socket-patch-cli/src/commands/rollback.rs:2283-2292: vendor-owned purls are partitioned out of the in-place restore. The comment's premise ("their patch lives in the committed.socket/vendor/artifact … not in the installed tree") doesn't hold after an agent→vendored takeover.crates/socket-patch-core/src/vendor/cargo.rs(vendor takeover) doesn't revert, or warn about, an existing in-place patch of the same purl.This isn't strictly Cargo-only, since the rollback partition is ecosystem-agnostic. But Cargo is where the leftover is persistent and shared across projects: other ecosystems' installers usually overwrite the installed copy on the next install.