fix(redirect): make the cargo hosted redirect transactional and fail-closed - #193
fix(redirect): make the cargo hosted redirect transactional and fail-closed#193Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
Conversation
…closed A dep now counts as redirected (lock repoint, registry block, ledger record, confirmed count, VEX) ONLY if its Cargo.toml pin fully landed; any occurrence that cannot be pinned skips the dep entirely with one clear warning and zero writes. Fixes audit findings A1-A9: - A1: pin EVERY occurrence across [dependencies], [dev-dependencies], [build-dependencies], and target-specific tables, not the first regex match (partial pins gave one dep two sources - cargo refuses the manifest and every cargo command breaks). - A2: support the multi-line [dependencies.<name>] table form (insert a registry line) instead of repointing the lock while the manifest still says crates.io. - A3/A3b: hosted confirmation keys off the rewriter's new confirmed_cargo_uuids set, never substring presence - the config block contains the index URL while pinning nothing, so config-only rewrites used to confirm, ledger-record, and VEX-attest patches no build would use. - A4: an existing registry = "socket-patch-<uuid>" pin is OURS - supersede it in place instead of reporting not-found and splitting manifest (old uuid) from lock (new uuid). - A5: rename-aware matching - `package = "other"` under a matching key is NOT the patched crate; an alias key with `package = "<crate>"` is. - A6: the [registries] idempotence check ignores commented lines; a commented/degraded managed block is restored on re-run. - A7: line-scoped plain-version rewrite stops swallowing the trailing blank line (and preserves trailing comments). - A8: validate service-supplied inputs before TOML interpolation (canonical uuid grammar via path_safety, quote/control-free sparse index URL, 64-hex cksum). - A9: empty-string cargoCksumSha256 is missing (TS twin parity) - skip the dep instead of writing checksum = "" into Cargo.lock. New shared golden fixtures (two-sections, table-form, renamed, supersede, rerun, commented-config), 19 rewriter unit tests, and two CLI tests adapted from the audit repro probes (the cargo analogue of no_lockfile_redirect_is_not_attested, plus the table-form-without-lock scenario now landing a real pin). The depscan TS byte-twin is ported in a companion depscan PR; its submodule pin needs a bump after this merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: CRLF plain versions refuse redirect
- Added \r? before $ in the regex pattern to optionally match carriage returns from CRLF line endings.
Or push these changes by commenting:
@cursor push fc3bda3158
Preview (fc3bda3158)
diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs
--- a/crates/socket-patch-core/src/patch/redirect/mod.rs
+++ b/crates/socket-patch-core/src/patch/redirect/mod.rs
@@ -1031,7 +1031,7 @@
// swallowed it).
let c = regex::escape(crate_name);
let line_re =
- Regex::new(&format!(r#"^(\s*(?:{c}|"{c}")\s*=\s*)"([^"]+)"([ \t]*(?:#.*)?)$"#))
+ Regex::new(&format!(r#"^(\s*(?:{c}|"{c}")\s*=\s*)"([^"]+)"([ \t]*(?:#.*)?)\r?$"#))
.unwrap();
let Some(m) = line_re.captures(raw) else {
pending.push(Pending::Refuse(
@@ -1943,7 +1943,7 @@
}
}
-// ── composer.lock ────────────────────────────────────────────────────────────
+// ── composer.lock ──────────────────────────────────────────���─────────────────
fn rewrite_composer_lock(
files: &BTreeMap<String, String>,
overrides: &[DepOverride],You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 157f32d. Configure here.
| pending.push(Pending::Refuse( | ||
| "unsupported version-entry spelling".to_string(), | ||
| )); | ||
| continue; |
There was a problem hiding this comment.
CRLF plain versions refuse redirect
High Severity
The A7 plain-version matcher only allows spaces or tabs after the closing quote, so a carriage return left on the line after splitting on \n fails the match. That occurrence is treated as unrewritable and the whole dependency is skipped. Typical Windows or core.autocrlf Cargo.toml files use the crate = "1.0" spelling, so hosted cargo redirect does not land.
Reviewed by Cursor Bugbot for commit 157f32d. Configure here.
| })); | ||
| if ws { | ||
| workspace_pinned = true; | ||
| } |
There was a problem hiding this comment.
Table form misses registry-index
Medium Severity
Table-form entries pin by inserting registry whenever no registry key is present and never look for registry-index. Inline tables already refuse that key as a foreign registry. A [dependencies.<name>] block that only has registry-index still gets a socket registry line, then lock, config, confirmation, and VEX can proceed.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 157f32d. Configure here.
| }; | ||
| let prefix = if config.is_empty() { "" } else { "\n" }; | ||
| Some(CargoConfigPlan { | ||
| content: format!("{config}{sep}{prefix}{block}"), |
There was a problem hiding this comment.
Commented headers duplicate registry tables
Medium Severity
The managed [registries.socket-patch-…] presence check requires the header line to equal the header after trim. An uncommented header with a trailing # comment does not match, so a second table is appended. Duplicate TOML tables break cargo config parse on re-run, while the dep can still be confirmed as redirected.
Reviewed by Cursor Bugbot for commit 157f32d. Configure here.
The table-form branch of plan_cargo_toml checked workspace/path/git and an existing registry value but never registry-index, so a block like [dependencies.anyhow] version = "1.0.0" registry-index = "sparse+https://index.crates.io/" gained an inserted registry = "socket-patch-<uuid>" line next to the existing registry-index line. Cargo rejects a dependency naming both keys as ambiguous at manifest-parse time, so every cargo command broke - while the dep still landed in confirmed_cargo_uuids with zero warnings, printing "Redirected 1 package(s)", persisting a ledger record, and emitting an assume_applied VEX statement for a bricked project. Refuse the whole dep (redirect_cargo_toml_dep_unrewritable, zero writes, no confirmation) when a table-form block carries a registry-index key, mirroring the inline-table branch, and cover it with a unit test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>



LLM Description written by Claude Code:claude-fable-5
What
Makes the hosted cargo redirect rewriter (
scan --mode hosted/--redirect) transactional and fail-closed per dependency: a dep now counts as redirected — Cargo.lock repoint,.cargo/config.tomlregistry block, ledger record,redirectedcount, VEX attestation — only if its Cargo.toml pin fully landed across every occurrence. If any occurrence cannot be pinned, the dep is skipped entirely with one clear warning and nothing is written for it.Rewriter changes (
socket-patch-core/src/patch/redirect/mod.rs):[dependencies],[dev-dependencies],[build-dependencies], target-specific tables, and[workspace.dependencies]— not just the first regex match. A first-match-only rewrite gave the same dep different sources per section, which cargo rejects at manifest-parse time (bricking every cargo command; reproduced with real cargo in the audit).[dependencies.<name>]tables gain an insertedregistry = "…"line instead of being reported not-found while the lock was still repointed (which broke--lockedbuilds and silently dropped the patch on re-resolve).package = "<other>"is not the patched crate (pinning it pointed a foreign package at the single-crate socket registry — hard resolution failure); the patched crate consumed under an alias key (iffy = { package = "cfg-if" }) is found and pinned.registry = "socket-patch-<uuid>"pin (canonical-uuid grammar) is ours and is replaced in place when a newer patch uuid is selected — previously it was classified as a foreign registry, leaving Cargo.toml on the old uuid while Cargo.lock moved to the new one (lock out of sync under--locked; superseded patch resolved otherwise; VEX attested the new one). A pin to any other registry remains a refusal.registry-index, unsatisfiableworkspace = trueinheritance, unsupported spellings) skip the dep entirely: no lock edit, no config block, no record, no VEX — oneredirect_cargo_toml_dep_unrewritablewarning. ACargo.lockthat lacks the dep's[[package]]block also skips the dep (manifest-only rewrites desync manifest and lock).workspace = trueinheritors are satisfied by pinning the[workspace.dependencies]entry in the same manifest.[registries.…]presence check now requires an uncommented header with an uncommented, exactindex = "…"line. A commented-out or degraded managed block is restored/regenerated on re-run instead of being "detected" by raw substring and left broken while the run reported success.\s*$regex consumed the entry's trailing newline and deleted the following blank line, violating the byte-preservation invariant. Trailing# commentson plain-version entries are now preserved too.patch_uuidmust match the canonical lowercase uuid grammar (reusespath_safety::is_canonical_uuid, same as the vendored flow),index_urlmust be a quote/backslash/control-freesparse+http(s)://URL, and the cksum must be exactly 64 lowercase hex chars. A hostile value (e.g. a]+newline uuid injecting[source.crates-io] replace-with = …) is refused with a warning.cargoCksumSha256: ""is treated as missing (matching the TS twin's falsy check) — the dep is skipped withredirect_cargo_missing_cksuminstead of writingchecksum = ""into Cargo.lock, which hard-fails the nextcargo fetch --locked.kindis notcargo-sparsenow warns (redirect_cargo_missing_override, matching the TS twin) instead of vanishing silently.CLI changes (
socket-patch-cli/src/commands/scan/hosted.rs):RewriteResultgainsconfirmed_cargo_uuids, populated only for deps whose pin fully landed (or was already in place). The hosted confirmed-gate uses that set for cargo purls instead oftext.contains(index_url)— the config block contains the index URL while pinning nothing, so a config-block-only rewrite could previously confirm, persist a ledger record, and emit anassume_appliedVEX statement for a patch no build would ever use (the cargo crawler surfaces machine-wide$CARGO_HOMEcrates, so this was reachable for crates the project doesn't even depend on).Why
The bug class is silent broken success:
scan --mode hostedexited 0, printedRedirected 1 package(s), recorded ledger entries, and attested patches via--vex assume_appliedwhile the project was left unbuildable (duplicate-source manifests, manifest/lock splits under--locked), silently unpatched (locks re-resolving to crates.io), or falsely attested (config-block-only "redirects"). All audit findings were confirmed and most reproduced with real cargo. Fixes findings A1, A2, A3, A3b, A4, A5, A6, A7, A8, A9 of the cargo hosted-rewriter audit.Fail-closed refusals implemented instead of rewrites (each yields a warning, a skipped dep, and zero writes — never a broken success):
<crate>.workspace = true(e.g.serde.version = "1").workspace = trueinheritance when the (root) manifest has no[workspace.dependencies]entry for the crate — covers virtual-workspace roots whose deps live in member manifests the hosted flow does not rewrite today.registry-indexkey — insertingregistry = "…"next to it makes cargo reject the dependency specification as ambiguous at manifest-parse time.Known deferrals (documented, not silent):
[registries.socket-patch-<old-uuid>]config blocks are left in place when a newer uuid takes over (inert once nothing references them; removing user-edited config regions wholesale felt riskier than the cosmetic leftover). The manifest/lock/ledger all move to the new uuid.Testing
crates/socket-patch-core/tests/fixtures/redirect/cargo/cargo/:two-sections(A1 + the A7 blank-line case),table-form(A2),renamed(A5),supersede(A4),rerun(idempotent no-op),commented-config(A6 restore). The existingbasicfixture is byte-identical to before.patch/redirect/mod.rscovering every finding, including the hostile-input refusals (A8), empty-cksum skip (A9), transactional skips, the table-formregistry-indexrefusal, workspace inheritance both ways, target-specific tables, and rename-awareness. Existing cargo tests updated to use a canonical uuid (the rewriter now validates the grammar).in_process_redirect.rs, adapted from the audit repro probes and flipped red→green:cargo_granted_but_nothing_pinned_is_not_confirmed_or_attested— the cargo analogue ofno_lockfile_redirect_is_not_attested: granted patch for a crate the project doesn't declare → no config block, no ledger, no VEX, exit 1 on requested attestation (previously: inert config block +redirected: 1+ ledger + VEX, exit 0).cargo_table_form_without_lock_is_pinned_and_attested— the audit's config-only repro scenario now lands a real manifest pin and legitimately attests.cargo test -p socket-patch-core --all-features redirect,cargo test -p socket-patch-cli --all-features --test in_process_redirect --test e2e_redirect_cargo_build(includes the realcargo fetch --locked/build e2e),cargo clippy --workspace --all-features -- -D warnings, and the fullcargo test --workspace --all-features— all green.Cross-repo note: the depscan TS byte-twin (
workspaces/app/src/patches/registry-rewrite/cargo.ts) shares these golden fixtures and is ported in a companion depscan PR; the depscan submodule pin will need a bump after this merges so the TS golden suite consumes the new fixtures.Note
High Risk
Changes hosted redirect confirmation, ledger, and VEX attestation for cargo—security-sensitive supply-chain behavior—and rewrites manifest/lock/config with fail-closed validation; incorrect logic could falsely attest patches or break builds.
Overview
Makes hosted cargo redirects transactional per dependency: a patch counts as redirected, ledgered, and VEX-attested only when its Cargo.toml pin fully lands (all sections, all occurrences) together with lock/config updates—or when that pin is already in place. Any unrewritable occurrence, missing lock entry, or undeclared dep skips the entire dep with warnings and zero writes (no inert
[registries.…]block).The cargo rewriter is reworked around plan-then-commit: validate uuid/index URL/checksum before TOML interpolation; pin every
[dependencies]/ dev/build / target / workspace table (including[dependencies.name]table form and rename-awarepackage =); supersede priorsocket-patch-<uuid>pins; restore commented or degraded registry config blocks; refuse path/git, foreign registry, and hostile inputs.Hosted confirmation for
pkg:cargo/now usesRewriteResult.confirmed_cargo_uuidsinstead of substring matches on index URLs—fixing falseredirected: 1, ledger, andassume_appliedVEX when nothing in the build actually pins the patch.Tests: new golden fixtures, many unit tests, and in-process CLI tests for “granted but not pinned” vs table-form without lock.
Reviewed by Cursor Bugbot for commit 157f32d. Configure here.