Skip to content

Gem hosted and vendored rewrites delete a second gem declaration that shares the patched gem's line after ;, so the next bundle install drops that dependency #826

Description

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

Summary

When the patched gem shares a Gemfile line with another declaration, separated by ;, both gem rewriters replace the whole physical line with the patched gem's new declaration. Everything after the ; is silently deleted:

gem "colorize", "0.8.1"; gem "rainbow", "3.1.1"
  • Hosted (scan --mode hosted) rewrites it to source "<patch-registry>" do / gem "colorize", "0.8.1" / end, and gem "rainbow" is gone.
  • Vendored (vendor) rewrites it to gem "colorize", "0.8.1", path: ".socket/vendor/…", and again gem "rainbow" is gone.

The lock still lists rainbow under DEPENDENCIES. A frozen install fails with exit 16 ("You have deleted from the Gemfile: * rainbow"). The unfrozen install, which hosted mode prescribes for CHECKSUMS-less locks (redirect_gem_frozen_install), succeeds and removes rainbow from the bundle, after which require "rainbow" raises LoadError. The scan itself exits 0 with status: success and redirected: 1, and gives no warning.

Hosted rollback doesn't recover it: it restores gem "colorize", "0.8.1" and nothing else. Vendored vendor --revert should restore the ledger's verbatim original line; I haven't verified that.

Impact

A dependency that has nothing to do with the patch disappears from the project. Under frozen CI that's a red build. Otherwise it's a runtime LoadError, or a silently missing gem that is only required lazily. ;-joined declarations are unusual but valid Ruby, and Bundler accepts them.

A related shape: with options before the ; (gem "colorize", "0.8.1", require: false; gem "rainbow", "3.1.1"), gem_line_trailing_options returns the whole rest verbatim (require: false; gem "rainbow", "3.1.1"). Hosted mode then moves gem "rainbow" into the Socket source … do block. A frozen install exits 16; on Bundler 4.0.17 the unfrozen install still resolves rainbow from rubygems.org, but it is now source-pinned (rainbow (= 3.1.1)!).

Repro (hosted, real rubygems.org upstream, mocked patch API + patch registry on loopback)

cat > Gemfile <<'EOF'
source "https://rubygems.org"

gem "colorize", "0.8.1"; gem "rainbow", "3.1.1"
EOF
bundle config set --local path vendor/bundle && bundle install
socket-patch scan --mode hosted --json --yes --api-url $MOCK --patch-server-url $MOCK --api-token fake --org org
#  -> exit 0, status success, redirected 1, rewrittenFiles 2; only redirect_gem_stale_install warning
cat Gemfile
#  source "https://rubygems.org"
#  source "http://127.0.0.1:18766/patch-registry/gem/tok123/<uuid>/" do
#    gem "colorize", "0.8.1"
#  end
# fresh checkout (Gemfile + Gemfile.lock only):
BUNDLE_FROZEN=true bundle install   # exit 16: "You have deleted from the Gemfile: * rainbow (= 3.1.1)"
bundle install                      # exit 0; rainbow removed from Gemfile.lock
bundle exec ruby -e 'require "colorize"; p SOCKET_PATCHED_COLORIZE; require "rainbow"'
#  true
#  cannot load such file -- rainbow (LoadError)

Vendored (I used a scratch copy of e2e_vendor_gem_build.rs's capstone whose only change is the fixture Gemfile line gem "rack", "~> 3.1"; gem "colorize", "0.8.1"):

Gemfile after `vendor --offline`:
  source "https://rubygems.org"
  gem "rack", "3.2.7", path: ".socket/vendor/gem/<uuid>/rack-3.2.7"
Gemfile.lock still has DEPENDENCIES colorize (= 0.8.1)
fresh-checkout frozen `bundle install`: "You have deleted from the Gemfile: * colorize (= 0.8.1)"

Expected vs actual

Matrix (Linux, Ruby 3.3.6; the rewrite is pure string handling, so no OS dependence is expected)

Mode Bundler Manifest Result
hosted 4.0.17 (CHECKSUMS lock) Gemfile fail (×3): sibling deleted; frozen exit 16, unfrozen drops it
hosted 4.0.17 gems.rb fail
hosted 2.6.9 (no CHECKSUMS) Gemfile fail
hosted 2.4.22 (no CHECKSUMS) Gemfile fail: the prescribed unfrozen install drops it; LoadError
vendored 4.0.17 Gemfile fail
vendored 2.4.22 Gemfile fail
hosted, ; declaration first (gem "rainbow"…; gem "colorize"…), 4.0.17 pass: refused with redirect_gem_unrecognized_declaration
hosted, PR #637 head 464896d, 4.0.17 still fails (the new gem_line_tail_blocks_edit refuses if but not ;)

First bad

Not bisected. Present on main 045d7ec.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:5164 gem_line_trailing_options: for a tail of , "0.8.1"; gem "rainbow", "3.1.1", it consumes the quoted version, then finds no , before ; and returns "", so the replacement keeps nothing past the version. When an option precedes the ;, it returns the remainder verbatim, including the second statement.
  • Hosted call site: crates/socket-patch-core/src/patch/redirect/mod.rs:5706. The whole matched line is replaced.
  • Vendored: crates/socket-patch-core/src/vendor/gem.rs:1532 rest_blocks_edit has no ; check, and :1421 builds new_line from the same helper.
  • PR Fix hosted gem redirect breaking multi-line and conditional gem lines (#340) #637's gem_line_tail_blocks_edit tokenizes the tail but treats ; as an ordinary character. A top-level ; outside quotes should probably refuse there (and in rest_blocks_edit).

No probe runs: Linux only, because the defect is OS-independent string handling. Related: #340 (same single-line tail recognizer, different trigger).

Activity

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