Skip to content

Gem crawler ignores Bundler's path.system: true when a leftover vendor/bundle exists, so agent apply patches the unused copy and vex attests not_affected while Bundler loads the unpatched system gem #915

Description

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

Summary

bundle config set --local path.system true (or BUNDLE_PATH__SYSTEM=true in the environment) is Bundler's documented way to stop using a bundle path and go back to system gems. A project that used to install into vendor/bundle usually still has that directory, since it's gitignored and nobody deletes it. In that state:

  • Bundler installs into and loads from the system gem home (Gem.loaded_specs[...].full_gem_path = /…/lib/ruby/gems/3.3.0/gems/colorize-0.8.1).
  • The gem crawler always probes the default <cwd>/vendor/bundle. Because that root holds stores, it skips the gem env homes (ruby_crawler.rs:132).
  • Agent apply therefore patches only the stale vendor/bundle copy, reports success, and socket-patch vex emits not_affected for the CVE while the app runs the unpatched system copy.

The crawler already parses BUNDLE_PATH__SYSTEM (parse_bundle_config_path, and CLI_CONTRACT.md says it "drops the recorded path, as bundler itself ignores it"). But it only drops the recorded path. It keeps the default vendor/bundle root, which Bundler ignores just as much, and lets that root suppress the system homes. The env form only drops the global-config tier (global_path_config_unless_env_path_settings).

This is narrower than the plain "leftover vendor/bundle, no config at all" heuristic that CLI_CONTRACT.md documents ("When the default vendor/bundle root holds no store, the gem homes gem env reports are appended"). Here Bundler has been told explicitly that the bundle path is not in use, and the crawler reads that setting but doesn't act on it.

Impact

A false VEX attestation: not_affected / inline_mitigations_already_exist for a vulnerability whose code is still the one being loaded. Agent apply also reports success without patching the copy that runs.

Repro (Linux, Ruby 3.3.6, Bundler 4.0.22; no network beyond rubygems.org)

mkdir app && cd app && git init -q
printf 'source "https://rubygems.org"\ngem "colorize", "0.8.1"\n' > Gemfile
bundle config set --local path vendor/bundle && bundle install      # old setup
bundle config unset --local path
bundle config set --local path.system true && bundle install         # back to system gems
# hand-written .socket/manifest.json + blobs: pkg:gem/colorize@0.8.1,
#   package/lib/colorize.rb  beforeHash = upstream, afterHash = upstream + "$SOCKET_PATCHED_COLORIZE = true"
socket-patch apply --offline --json        # status success, 1 applied
bundle exec ruby -e 'require "colorize"; p defined?($SOCKET_PATCHED_COLORIZE) ? :PATCHED : :UNPATCHED; puts Gem.loaded_specs["colorize"].full_gem_path'
#  => :UNPATCHED  /opt/rbenv/versions/3.3.6/lib/ruby/gems/3.3.0/gems/colorize-0.8.1
tail -1 vendor/bundle/ruby/3.3.0/gems/colorize-0.8.1/lib/colorize.rb   # => $SOCKET_PATCHED_COLORIZE = true (the unused copy)
socket-patch vex --product pkg:gem/app@1.0.0   # exit 0, "status":"not_affected"

The env variant is the same with export BUNDLE_PATH__SYSTEM=true instead of the local config.

Expected vs actual

  • Expected: CLI_CONTRACT.md ("Gem install roots") says the crawler probes "the project's Bundler install roots in bundler's own precedence order", and that BUNDLE_PATH__SYSTEM: "true" is honored "as bundler itself ignores it". With path.system true in the winning tier (Bundler::Settings#path → Path#use_system_gems? is true), Bundler's install root is the gem env home. So apply should patch that copy (or at least also patch it), and vex should verify the copy Bundler loads and refuse with not_applied while it's unpatched.
  • Actual: only vendor/bundle is crawled. apply succeeds on the unused copy, the loaded copy stays unpatched, and vex attests not_affected.

Matrix (agent mode, Linux, Ruby 3.3.6, main 9c43dfc)

Bundler local path.system: true + leftover vendor/bundle env BUNDLE_PATH__SYSTEM=true + leftover vendor/bundle control: local path.system: true, no vendor/bundle
4.0.22 fail (2/2) fail (2/2) pass (system copy patched, vex correct)
2.6.9 fail fail pass
2.4.22 fail fail —

The logic is OS-independent (pure path selection), so I didn't run macOS/Windows probes. Published v4.0.0 also patches only vendor/bundle. Its vex omits the patch for an unrelated reason (the old setup gating), so the false attestation shows up on v5 main.

Suspect code

  • crates/socket-patch-core/src/crawlers/ruby_crawler.rs:417: roots.push(default_root.clone()) runs unconditionally, even when the effective Bundler tier sets path.system true.
  • crates/socket-patch-core/src/crawlers/ruby_crawler.rs:132: !discovery.default_root_has_stores then suppresses the gem env homes, the only place the loaded copy lives.
  • crates/socket-patch-core/src/crawlers/ruby_crawler.rs:1474 (parse_bundle_config_path): path.system only drops the recorded path. The env BUNDLE_PATH__SYSTEM only drops the global tier (global_path_config_unless_env_path_settings, :1293).

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions