[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.
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
scan --mode hosted(andget --mode hosted) only looks for a gem's declaration inside theGemfiletext, using thegem_line_re/declared_reregexes. 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:eval_gemfile "Gemfile.shared"(shared or plugin Gemfiles,Gemfile.local)%w[a b].each { |g| gem g }In both cases the rewriter appends a top-level
source "<patch registry>" do gem "x", "<v>" endblock. The gem is now declared twice, with different requirements or sources. Bundler refuses to parse the Gemfile, so everybundle install, frozen or unfrozen, exits 4. The scan itself exits 0 withredirected: 1and no warning.Impact
bundle checkin the original tree.Repro
This uses the hermetic fixture from
crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs: a mock upstream withvuln-gem1.0.0 →tiny-dep, and a patch registry and API serving the patchedvuln-gem. I kept that mock server running and drove the steps below by hand.If
Gemfile.sharedalready saysgem "vuln-gem", "1.0.0", or"= 1.0.0", require: false, the error becomesYou 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
redirect_gem_unrecognized_declaration. The rewriter already does this for declarations it can't parse; see the comment ondeclared_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 underDEPENDENCIES, which an appended transitive gem never is before the rewrite. That signal could gate the append.Matrix (Linux, Ruby 3.3.6, socket-patch main
6e7ef74)eval_gemfile,gem "x"eval_gemfile,gem "x", "1.0.0"eval_gemfile,"= 1.0.0", require: false%w[x].each { gem g }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 wheneverdeclared_re(:4986) misses.crates/socket-patch-core/src/vendor/gem.rs:1304appends a managedgem "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.