[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
In a Bundler project that installs to system gems (the default, no path set), the gem crawler lists every gem in the shared gem env home, including versions that other projects on the machine installed. scan --mode hosted hands those copies to the gem redirect as candidates. The redirect never checks that the project's lock resolves that version. So if a patch exists for colorize 0.8.1, which another project installed, and this project locks colorize 1.1.0 under gem "colorize", "~> 1.0", the scan:
- rewrites the declaration to
source "<patch registry>" do gem "colorize", "0.8.1" end. That drops the user's own ~> 1.0 constraint and pins a different, older version.
- adds a second
colorize (0.8.1) CHECKSUMS line next to the locked colorize (1.1.0) one, and leaves the GEM spec at 1.1.0 (a mixed pair).
- warns
redirect_gem_frozen_install and tells the user to run bundle install (unfrozen) once.
Following that advice downgrades the project: Using colorize 0.8.1 (was 1.1.0). In this repro the downgraded 0.8.1 even comes from the shared home's unpatched copy (the stale-install warning fires too). rollback can't undo it (Manifest not found), so the Gemfile and lock stay broken for frozen installs until the user fixes them by hand.
Impact
- A silent dependency downgrade, past a major version and outside the Gemfile's own constraint. The patch targets a version the project doesn't use, which may be older and vulnerable.
- Every frozen or deployment install (
BUNDLE_FROZEN, CI) fails with exit 16 right after the scan.
rollback refuses the mixed pair, so the scan has no undo.
- This is the default dev-machine setup (rbenv, chruby or asdf with no
bundle config path), where the gem home always holds many projects' versions.
Repro (Linux, Ruby 3.3.6, Bundler 4.0.22 or 2.6.9)
Patch API and patch registry mocked on loopback (the run-13 mock described in ledger #316); rubygems.org is the real upstream.
gem install colorize -v 0.8.1 # "another project" installed 0.8.1 into the shared home
mkdir app && cd app
printf 'source "https://rubygems.org"\n\ngem "rake"\ngem "colorize", "~> 1.0"\n' > Gemfile
bundle install # locks and installs colorize 1.1.0 (system gems)
socket-patch scan --mode hosted --yes --api-url $MOCK --org org --api-token fake
cat Gemfile # gem "colorize", "0.8.1" inside a patch-registry source block
BUNDLE_FROZEN=true bundle install # exit 16 "Your lockfile needs to be updated"
bundle install # "Using colorize 0.8.1 (was 1.1.0)"
socket-patch rollback ... # error: Manifest not found
Gemfile diff from the scan:
gem "rake"
-gem "colorize", "~> 1.0"
+source "http://127.0.0.1:18765/patch-registry/gem/tok/<uuid>/" do
+ gem "colorize", "0.8.1"
+end
Lock diff (only this):
CHECKSUMS
+ colorize (0.8.1) sha256=096e2e…
bundler (4.0.22) sha256=…
colorize (1.1.0) sha256=30b523…
Expected vs actual
- Expected: hosted mode pins the version the project's lock resolves. CLI_CONTRACT.md's gem rows describe the redirect as re-pointing the locked spec (GEM section, CHECKSUMS,
DEPENDENCIES !), and its "Installed-version narrowing" section keeps only versions "present here". A version present only in a shared gem home that the lock doesn't resolve should be skipped (package_not_installed, or a gem-specific refusal) and never pinned. A declared constraint the patched version doesn't satisfy (~> 1.0 vs 0.8.1) should never be overwritten.
- Actual: the declaration is rewritten to the foreign version, and the next install downgrades.
Matrix
| OS |
Ruby |
Bundler |
Result |
| Linux |
3.3.6 |
4.0.22 |
reproduces (×2) |
| Linux |
3.3.6 |
2.6.9 |
reproduces (no CHECKSUMS: redirect_gem_no_checksums_section + redirect_gem_frozen_install; the unfrozen install downgrades) |
| Linux |
3.3.6 |
4.0.22, project with path vendor/bundle |
doesn't reproduce (the crawl skips the system home) |
macOS and Windows weren't probed. The crawl and rewrite logic is OS-independent.
First bad version
Also reproduces on the published v4.0.0 binary (--mode hosted), so it's not a recent regression. Current main is d47eab3.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:6073 (rewrite_gem): the in-place rewrite (around :6418) writes dep.version over the declaration's constraint without checking that the lock's GEM specs resolve dep.name (dep.version) or that the declared requirement admits it. The CHECKSUMS branch (around :6547) then adds an entry for a version the lock doesn't hold.
crates/socket-patch-core/src/crawlers/ruby_crawler.rs:43 (get_gem_paths): in project mode with no bundle store, the crawl falls back to the whole shared gem env home, so other projects' versions become hosted candidates. Gem scan discovery isn't filtered by the project's lock (crates/socket-patch-cli/src/commands/scan/discovery.rs adds lock-only entries but never removes installed entries that aren't locked).
No probe runs; Linux sandbox only.
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
In a Bundler project that installs to system gems (the default, no
pathset), the gem crawler lists every gem in the sharedgem envhome, including versions that other projects on the machine installed.scan --mode hostedhands those copies to the gem redirect as candidates. The redirect never checks that the project's lock resolves that version. So if a patch exists forcolorize 0.8.1, which another project installed, and this project lockscolorize 1.1.0undergem "colorize", "~> 1.0", the scan:source "<patch registry>" do gem "colorize", "0.8.1" end. That drops the user's own~> 1.0constraint and pins a different, older version.colorize (0.8.1)CHECKSUMS line next to the lockedcolorize (1.1.0)one, and leaves the GEM spec at 1.1.0 (a mixed pair).redirect_gem_frozen_installand tells the user to runbundle install(unfrozen) once.Following that advice downgrades the project:
Using colorize 0.8.1 (was 1.1.0). In this repro the downgraded 0.8.1 even comes from the shared home's unpatched copy (the stale-install warning fires too).rollbackcan't undo it (Manifest not found), so the Gemfile and lock stay broken for frozen installs until the user fixes them by hand.Impact
BUNDLE_FROZEN, CI) fails with exit 16 right after the scan.rollbackrefuses the mixed pair, so the scan has no undo.bundle config path), where the gem home always holds many projects' versions.Repro (Linux, Ruby 3.3.6, Bundler 4.0.22 or 2.6.9)
Patch API and patch registry mocked on loopback (the run-13 mock described in ledger #316); rubygems.org is the real upstream.
Gemfile diff from the scan:
Lock diff (only this):
CHECKSUMS + colorize (0.8.1) sha256=096e2e… bundler (4.0.22) sha256=… colorize (1.1.0) sha256=30b523…Expected vs actual
DEPENDENCIES!), and its "Installed-version narrowing" section keeps only versions "present here". A version present only in a shared gem home that the lock doesn't resolve should be skipped (package_not_installed, or a gem-specific refusal) and never pinned. A declared constraint the patched version doesn't satisfy (~> 1.0vs0.8.1) should never be overwritten.Matrix
redirect_gem_no_checksums_section+redirect_gem_frozen_install; the unfrozen install downgrades)path vendor/bundlemacOS and Windows weren't probed. The crawl and rewrite logic is OS-independent.
First bad version
Also reproduces on the published v4.0.0 binary (
--mode hosted), so it's not a recent regression. Current main isd47eab3.Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:6073(rewrite_gem): the in-place rewrite (around:6418) writesdep.versionover the declaration's constraint without checking that the lock's GEM specs resolvedep.name (dep.version)or that the declared requirement admits it. The CHECKSUMS branch (around:6547) then adds an entry for a version the lock doesn't hold.crates/socket-patch-core/src/crawlers/ruby_crawler.rs:43(get_gem_paths): in project mode with no bundle store, the crawl falls back to the whole sharedgem envhome, so other projects' versions become hosted candidates. Gem scan discovery isn't filtered by the project's lock (crates/socket-patch-cli/src/commands/scan/discovery.rsadds lock-only entries but never removes installed entries that aren't locked).No probe runs; Linux sandbox only.