Skip to content

Hosted gem rollback / remove strip the DEPENDENCIES ! when the user's source block opener spans lines or uses braces, so frozen installs fail on Bundler 4.0.19+ #1509

Closed

Description

[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).

Summary

This is a #1056 / #1476 neighbour that the #1478 fix doesn't cover. The hosted gem unwind decides whether the restored gem sits inside the user's own source block with in_source_block → block_stack (crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:545, :565). That scanner only recognises a source block when one physical line both starts with source and holds the do (gem.rs:609, source_do needs !continues). It also never counts { … } blocks at all. Two valid Bundler spellings therefore lose their ! pin on rollback / remove:

  1. A multi-line opener, for example a URL that doesn't fit on one line:
    source(
      "https://gems.example.com"
    ) do
      gem "vuln-gem", "1.0.0"
    end
  2. A brace block:
    source("https://gems.example.com") {
      gem "vuln-gem", "1.0.0"
    }

Bundler locks a gem declared in either form as vuln-gem (= 1.0.0)!, just as it does for the one-line source "…" do. The test's own premise assertion confirms it on 2.6.9 and 4.0.22.

Impact

  • After a hosted scan followed by rollback or remove, the restored Gemfile.lock has vuln-gem (= 1.0.0) without the !, so the lock isn't byte-restored.
  • On Bundler 4.0.19+, every frozen / deployment install of the restored pair fails with exit 16: "Your lockfile needs to be updated, but it can't be because frozen mode is set". Bundler 2.6.9 and 4.0.18 only warn.
  • For the brace form, the Gemfile also isn't restored byte-for-byte. The gem comes back at column 0 after an inserted blank line, because the indent is derived from the same block stack:
    source("http://127.0.0.1:41955/upstream") {
    
    gem "vuln-gem", "1.0.0"
    }

Repro

This uses the repo's own harness. Apply this scratch change to crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs: in redirect_scanned_project, add a Driver::ScanVexSourceBlockAssignment arm (gated on an env var) that writes one of these Gemfiles instead:

// paren multi-line opener
format!("source \"{up}/upstream\"\n\nsource(\n  \"{up}/upstream\"\n) do\n  gem \"{DEP}\", \"{DEP_VERSION}\"\nend\n")
// brace block
format!("source \"{up}/upstream\"\n\nsource(\"{up}/upstream\") {{\n  gem \"{DEP}\", \"{DEP_VERSION}\"\n}}\n")

Then run:

gem install bundler -v 4.0.22
BUNDLER_VERSION=4.0.22 SOCKET_PATCH_BUNDLER_E2E_VERSION=4.0.22 SOCKET_PATCH_BUNDLER_E2E_REQUIRED=1 \
  cargo test -p socket-patch-cli --test e2e_redirect_gem_build gem_hosted_unwind_keeps_a_source_block_bang -- --include-ignored --nocapture

The test fails at rollback: the lock comes back byte for byte, ! included:

  left: "...DEPENDENCIES\n  vuln-gem (= 1.0.0)\n\nBUNDLED WITH\n  4.0.22\n"
 right: "...DEPENDENCIES\n  vuln-gem (= 1.0.0)!\n\nBUNDLED WITH\n  4.0.22\n"

With the byte assertions turned into prints, the frozen install of the restored pair on a fresh checkout exits 16 for both rollback and remove.

For a direct check of the scanner, in_source_block(text, text.find("gem \"rails\"").unwrap()) returns false for both texts above. It returns true for the one-line source "…" do spelling.

Expected vs actual

Matrix (Linux, Ruby 3.3.6, main aa85c6c)

Shape Bundler 4.0.22 rollback 4.0.22 remove 2.6.9 rollback 2.6.9 remove
Paren multi-line opener source(\n "…"\n) do ! dropped, frozen exit 16 (×2) ! dropped, frozen exit 16 ! dropped, frozen warns / exit 0 ! dropped, frozen exit 0
Brace block source("…") { ! dropped, frozen exit 16, Gemfile not byte-restored (×3) same ! dropped, exit 0, Gemfile not byte-restored same
One-line source "…" do (control) pass pass pass pass

macOS and Windows weren't probed, but the scanner is OS-independent string logic.

First bad

Not bisected. These shapes were never recognised: source-block detection arrived with the #1056 fix, and #1478 extended it only to keyword constructs.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:609: source_do is only set when the line isn't a continuation, and the do must sit on that same line.
  • crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:620-650: { / } never open or close a frame, so a brace block (source or otherwise) is invisible.

A fail-closed fallback, refusing the unwind when the gem's original line sits under a source opener the scanner can't model, would also avoid the silent ! loss.

Activity

  1. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the hosted gem unwind's source-block scanner only recognises a one-line source … do opener and ignores brace blocks). Branch: agent/fix-gem-source-block-scanner. Claim-ID: 2026-10-14T04:20:38Z-016237


    Generated by Claude Code

  2. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1520


    Generated by Claude Code

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

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:bundlerBundler (RubyGems)priority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions