Skip to content

Hosted gem scan pins a version that only another project installed into the shared gem home, rewriting gem "x", "~> 1.0" to the older patched "0.8.1", so the prescribed bundle install downgrades the project's locked gem #1055

Description

[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.

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