Repository navigation
Gem hosted and vendored lock pins ignore a gemspec development dependency on the same gem, so on Bundler 2.7+ the rewritten Gemfile.lock fails every frozen install ((= v)! vs Bundler's merged (>= 0, = v)!) #985
Description
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
In a gem/library project the Gemfile has gemspec, the .gemspec has add_development_dependency "x", and the Gemfile also declares gem "x". This is common: dev tools such as rake and rspec are often listed in both places. Bundler 2.7+ merges the two requirements into one Gemfile dependency, so its lockfile DEPENDENCIES entry is the union of both requirements.
Both the hosted and the vendored lock rewriters write the patched gem's entry as the bare x (= <ver>)!. On Bundler 2.7+ the Gemfile they leave behind resolves to x (>= 0, = <ver>)!. Bundler therefore treats the committed pair as out of sync:
- Every frozen or deployment install (
BUNDLE_FROZEN=true,BUNDLE_DEPLOYMENT=true, i.e. CI) fails with exit 16. bundle execunder frozen raisesBundler::ProductionError.
The hosted scan reports success and emits no redirect_gem_frozen_install caveat, because the lock has CHECKSUMS and is treated as converged. Vendored mode documents a frozen install as its guarantee.
Impact
- Hosted (CHECKSUMS lock, the Bundler 4 default): the committed redirect breaks CI and deployment installs for any gem project that also lists the patched gem in its gemspec's development dependencies. A developer's unfrozen
bundle installsilently re-locks to(>= 0, = 1.0.0)!and installs the patched gem. So the failure only shows up in frozen environments, after the patch was committed. - Vendored: the same shape fails the frozen
bundle installthat CLI_CONTRACT.md promises ("bundle install(normal andBUNDLE_FROZEN=true), byte-stable lock"). - Nothing installs unpatched. The failure is loud, but it breaks the documented frozen-install contract, and the scan gives no warning.
Repro (hosted; uses the repo's own harness)
In a scratch copy of crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs, set the fixture project to:
# Gemfile
source "<mock>/upstream"
gemspec
group :development do
gem "vuln-gem"
end# mylib.gemspec
Gem::Specification.new do |s|
s.name = "mylib"; s.version = "0.1.0"; s.summary = "x"; s.authors = ["x"]
s.files = ["lib/mylib.rb"]
s.add_development_dependency "vuln-gem"
endThen run the CHECKSUMS arm (scan --mode hosted --json --yes --vex …), copy the committable files (Gemfile, lock, gemspec, lib, .bundle) to a fresh dir, and run BUNDLE_FROZEN=true bundle install there.
Lock after the scan:
DEPENDENCIES
mylib!
vuln-gem (= 1.0.0)!
Fresh-checkout frozen install (Bundler 4.0.22):
Some dependencies were deleted from your gemfile, but the lockfile can't be
updated because frozen mode is set
You have added to the Gemfile:
* vuln-gem (>= 0, = 1.0.0)
You have deleted from the Gemfile:
* vuln-gem (= 1.0.0)
(exit 16). An unfrozen install on the same checkout succeeds, installs the patched bytes, and re-locks to vuln-gem (>= 0, = 1.0.0)!.
The scan output's redirect.warnings is empty, and redirected: 1.
Vendored (a scratch copy of e2e_vendor_gem_build.rs, real rubygems.org rack; Gemfile gemspec + gem "rack", "~> 3.1", gemspec add_development_dependency "rack", ">= 3"): the vendored lock has rack (= 3.2.7)!, and the fresh-checkout frozen install fails:
The gemspecs for path gems changed, but the lockfile can't be updated because
frozen mode is set
You have added to the Gemfile:
* rack (>= 3, = 3.2.7)
You have deleted from the Gemfile:
* rack (= 3.2.7)
Expected vs actual
- Expected: CLI_CONTRACT.md's vendored table says the DEPENDENCIES entry becomes
<name> (= <ver>)!"in bundler's exact canonical form", and thatbundle installworks both normally and withBUNDLE_FROZEN=true, with a byte-stable lock. For hosted mode, a CHECKSUMS lock is treated as converged and frozen-installable (noredirect_gem_frozen_install). Either the pin should carry Bundler's merged requirement (the gemspec dev dep's requirements plus= <ver>, which Bundler 2.7+ writes), or the run should refuse or warn. - Actual: the pin omits the gemspec requirement, so the pair is frozen-uninstallable on Bundler 2.7+, and no warning is emitted.
OS × version (Linux, Ruby 3.3.6; the logic is OS-independent text rewriting)
| Bundler | Hosted, CHECKSUMS lock: frozen fresh install | Vendored: frozen fresh install |
|---|---|---|
| 2.6.9 | pass | pass |
| 2.7.2 | fail | fail |
| 4.0.0 | fail | untested |
| 4.0.10 | fail | untested |
| 4.0.17 | fail | untested |
| 4.0.22 | fail (×3, incl. a gem "vuln-gem", "~> 1.0", group: :development variant) |
fail (×2) |
Controls:
- Gemspec runtime dependency (
add_dependency) plus a Gemfilegemline, 4.0.22 hosted: pass (frozen install OK). Runtime deps of agemspecaren't merged into the Gemfile dependency. - Gemspec dev dependency with no Gemfile line: hosted fails closed with
redirect_gem_declaration_not_visible(fine).
First bad: not a socket-patch regression. The published v4.0.0 release writes the same vuln-gem (= 1.0.0)! pin. The trigger is Bundler 2.7's requirement merge (2.6.9 still writes the plain Gemfile requirement).
Suspect code
- Hosted:
crates/socket-patch-core/src/formats/gem/hosted.rs:136(let target = format!(" {} (= {})!", dep.name, dep.version);) - Vendored:
crates/socket-patch-core/src/vendor/gem.rs:1884(let new_dep_line = format!(" {name} (= {version})!");)
Neither looks at the gemspec directive's development dependencies, which Bundler 2.7+ folds into the same DEPENDENCIES entry.
No probe branches: the rewrite is OS-independent text.
Backlog review — 2026-10-08
Priority: P1 → P2. A gemspec development-dependency spelling breaks frozen Bundler installs visibly; retain the compatibility fix.
Activity
Bundler bug-hunt run 27: #985 (gemspec dev-dep lock pin)
mikolalysenko commentedon Oct 7, 2026
[agent] Triaged as priority:p1 (Bundler). Confirmed on
main(9c43dfc): both pin writers emit the bare exact requirement with no knowledge of thegemspecdirective's development dependencies, atcrates/socket-patch-core/src/formats/gem/hosted.rs:136(format!(" {} (= {})!", …)) andcrates/socket-patch-core/src/vendor/gem.rs:1884. Nothing incrates/socket-patch-core/srcreadsadd_development_dependency. Not a duplicate, and no open PR covers it. It is a sibling of #780 (shared DEPENDENCIES reader), but the cause is a different one: Bundler 2.7+'s requirement merge is not modelled.Generated by Claude Code