Skip to content

Hosted gem redirect appends a second declaration when the gem is declared through eval_gemfile or a loop, so every bundle install fails with "You cannot specify the same gem twice" #482

Description

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

Summary

scan --mode hosted (and get --mode hosted) only looks for a gem's declaration inside the Gemfile text, using the gem_line_re / declared_re regexes. If the gem is a direct dependency declared somewhere those regexes can't see, the rewriter treats it as transitive. Two common shapes hit this:

  • the declaration is in a file pulled in with eval_gemfile "Gemfile.shared" (shared or plugin Gemfiles, Gemfile.local)
  • the declaration is generated, e.g. %w[a b].each { |g| gem g }

In both cases the rewriter appends a top-level source "<patch registry>" do gem "x", "<v>" end block. The gem is now declared twice, with different requirements or sources. Bundler refuses to parse the Gemfile, so every bundle install, frozen or unfrozen, exits 4. The scan itself exits 0 with redirected: 1 and no warning.

Impact

  • Every install of the project is broken: CI, a fresh checkout, and bundle check in the original tree.
  • The scan says it succeeded, so nothing tells the user the rewrite is what broke the build.

Repro

This uses the hermetic fixture from crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs: a mock upstream with vuln-gem 1.0.0 → tiny-dep, and a patch registry and API serving the patched vuln-gem. I kept that mock server running and drove the steps below by hand.

mkdir proj && cd proj
bundle config set --local path vendor/bundle
printf 'source "%s/upstream"\n\neval_gemfile "Gemfile.shared"\n' "$MOCK" > Gemfile
printf 'gem "vuln-gem"\n' > Gemfile.shared
bundle install                       # ok
socket-patch scan --mode hosted --json --yes --cwd "$PWD" \
  --api-url "$MOCK" --org test-org --api-token fake    # exit 0, redirected: 1
tail -3 Gemfile
# source "<MOCK>/patch-registry/gem/<token>/<uuid>/" do
#   gem "vuln-gem", "1.0.0"
# end
bundle install
# [!] There was an error parsing `Gemfile`: You cannot specify the same gem twice with
#     different version requirements.
# You specified: vuln-gem (>= 0) and vuln-gem (= 1.0.0). Bundler cannot continue.   (exit 4)

If Gemfile.shared already says gem "vuln-gem", "1.0.0", or "= 1.0.0", require: false, the error becomes You cannot specify the same gem twice coming from different sources. A Gemfile containing %w[vuln-gem].each { |g| gem g } fails the same way.

Expected vs actual

  • Expected: the redirect either wires the existing declaration or fails closed with redirect_gem_unrecognized_declaration. The rewriter already does this for declarations it can't parse; see the comment on declared_re: "appending next to a declaration the recognizer above cannot parse would leave the gem declared twice (bundler hard-fails on the duplicate)". The resolved lock tells you the gem is a direct dependency: it's listed under DEPENDENCIES, which an appended transitive gem never is before the rewrite. That signal could gate the append.
  • Actual: the append branch runs on what is really a direct dependency, and the Gemfile no longer parses.

Matrix (Linux, Ruby 3.3.6, socket-patch main 6e7ef74)

Bundler eval_gemfile, gem "x" eval_gemfile, gem "x", "1.0.0" eval_gemfile, "= 1.0.0", require: false %w[x].each { gem g }
4.0.17 (CHECKSUMS lock) fail (exit 4) fail (exit 4) fail (exit 4) fail (exit 4)
2.6.9 (no CHECKSUMS) fail (exit 4) untested untested untested
2.4.22 fail (exit 4) untested untested untested

Control: a real transitive dependency (gem "parent-gem" → vuln-gem), with a CRLF Gemfile or a Gemfile with no trailing newline, appends correctly and installs the patched bytes with a frozen install.

The rewrite doesn't depend on the OS, so macOS and Windows weren't probed.

First bad release: v4.0.0 (the published npm binary) behaves the same, so this isn't a v5 regression.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:5105: the "Genuinely undeclared (a transitive dep): append a block" branch, which is reached whenever declared_re (:4986) misses.
  • Vendored mode has the same shape: crates/socket-patch-core/src/vendor/gem.rs:1304 appends a managed gem "x", "<v>", path: … block when no line in the Gemfile declares the gem. I didn't verify this end to end. v5 vendoring needs a service artifact my sandbox can't serve.

No probe runs: everything above was reproduced on Linux with the real bundler.

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