Fix gem rollback pinning redirected transitive gems (#457) - #460
Mikola Lysenko (mikolalysenko) merged 3 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
When hosted mode redirected a gem the Gemfile does not declare (a transitive dependency), it appended the patch-registry source block with no blank line before it. Rollback and remove only recognize the block as that append when a blank line precedes it, so they restored the gem as a new top-level `gem "<name>", "<version>"` plus an exact DEPENDENCIES pin, freezing the vulnerable version against `bundle update` (#457). The append now always leaves one blank line before the block, and the transitive restore removes that separator with the block, so the Gemfile and lock come back byte for byte. Assisted-by: Claude Code:claude-opus-5-5
f30857d to
89dfc9e
Compare
The upstream-restore golden pinned the old behavior: the rewriter's own append for a transitive gem was kept as a direct exact pin on rollback. It now round-trips byte for byte; a legacy append with no blank line before it still comes back as the exact pin. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] On 3aaf1dc,
I've re-run the failed jobs once. If it fails again I'll treat it as real and dig into the rescan diff in the uploaded Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 3aaf1dc. Configure here.
|
[agent] Ready for review on
Slack announcement not sent this run: the Slack send tool isn't available in the agent session. The next run will retry. Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #457
Summary
Hosted
rollback/removeno longer turn a redirected transitive gem into a top-level exact pin. On a converged (CHECKSUMS) lock, the Gemfile and lock now come back byte for byte, sobundle update <gem>can move off the vulnerable version again.Root cause
When hosted mode redirects a transitive gem,
rewrite_gem(patch/redirect/mod.rs) appends asource "<patch registry>" do … endblock. If the Gemfile already ends in\n, it writes no blank separator. The upstream restore (patch/redirect/upstream/gem.rs,provably_appended) only treats the block as the rewriter's append (Decl::Transitive) when a blank line comes right before it, because an in-place rewrite swallows the blank lines before the declaration. The rewriter's own append never had that blank line, so the restore fell back toDecl::Direct. The result was a newgem "<name>", "<version>"line and a<name> (= <version>)DEPENDENCIES entry.Fix
rewrite_gem: the transitive append always writes exactly one blank line before the block, using the Gemfile's own line ending (two line breaks if the file had no final one). The append is now provably an append.restore_manifest:Decl::Transitiveremoves that blank separator together with the block, so the restore is byte-identical.registry-rewrite gem.ts) should write the same blank separator.Tests (red → green)
upstream::gem::tests::rewriter_transitive_append_restores_byte_identically: real rewriter output (LF, trailing blank, CRLF, no final newline, two appends undone in either order)upstream::gem::tests::transitive_redirect_round_trips_through_restore: fullrestore_oneover the rewriter's CHECKSUMS lock + Gemfile (upstream sha seeded through a test-only client hook)gem "rails", "7.0.0"upstream_restore_golden::gem_transitive_append_round_trips_unless_unprovable: replacesgem_transitive_without_proof_stays_declared, which pinned the old behavior; the legacy no-blank shape is still asserted to stay as the exact pinLocal runs on 3aaf1dc:
cargo clippy --workspace --all-features -- -D warnings: clean.cargo test -p socket-patch-core --all-features --lib upstream::gem: 10/10 pass.cargo test -p socket-patch-core --all-features --test upstream_restore_golden gem: 5/5 pass.cargo test --workspace --all-features --no-fail-fast: everything gem- and redirect-related passes. 15 tests fail only in this sandbox because it runs as root, which bypasses the read-only / chmod fixtures:*_write_failure_*,*unremovable*,*cleanup_failure*,vlt_healan_unremovable_hidden_lock…,copy_treerelax_loop_must_not_traverse_symlinked_root, andpypi_poetry/pypi_requirementswire-failure rollback. None of them touches gem code, and CI runs them as a non-root user.cargo fmt --all -- --check: not run as a gate. Main itself isn't rustfmt-clean under the pinned 1.93.1 toolchain (about 125 files churn), CI has no fmt job, and this PR keeps its formatting changes to the lines it edits.No wrapper change is needed:
npm/,pypi/andgem/only dispatch to the binary.Priority note: this is a p1 single-issue cluster. I picked it over the older #328 because #457 silently freezes a vulnerable version after rollback, while #328 fails closed.
Note
Medium Risk
Changes gem hosted rewrite and upstream-restore behavior for Gemfile.lock pairs; incorrect handling previously froze vulnerable versions after rollback, while new redirects gain a mandatory blank line before appended blocks.
Overview
Fixes #457: hosted gem
rollback/removeno longer leave a redirected transitive gem as a new top-levelgem "name", "version"line (and matching lockDEPENDENCIESpin) when the manifest should return unchanged.The hosted rewriter now always inserts one blank line before an appended
source "<patch>" do … endblock (respecting LF vs CRLF), so upstream restore can treat it as provably appended.restore_manifestdrops that separator together with the block forDecl::Transitive, yielding a byte-identical Gemfile/lock round-trip on converged CHECKSUMS locks. Legacy appends written without that blank line still cannot be distinguished from a last-line in-place rewrite and continue to restore as an exact pin—documented in CLI_CONTRACT.md and covered in tests.Adds
seed_rubygems_sha256on the test upstream client, expands gem unit/golden tests (including full rewriter →restore_oneround-trip), and renames the golden case to assert both the new round-trip and the legacy ambiguous behavior.Reviewed by Cursor Bugbot for commit 3aaf1dc. Configure here.
Generated by Claude Code