[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).
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
bundle config set --local path.system true(orBUNDLE_PATH__SYSTEM=truein 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 intovendor/bundleusually still has that directory, since it's gitignored and nobody deletes it. In that state:Gem.loaded_specs[...].full_gem_path=/…/lib/ruby/gems/3.3.0/gems/colorize-0.8.1).<cwd>/vendor/bundle. Because that root holds stores, it skips thegem envhomes (ruby_crawler.rs:132).applytherefore patches only the stalevendor/bundlecopy, reportssuccess, andsocket-patch vexemitsnot_affectedfor 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 defaultvendor/bundleroot, 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 defaultvendor/bundleroot holds no store, the gem homesgem envreports 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_existfor a vulnerability whose code is still the one being loaded. Agentapplyalso reports success without patching the copy that runs.Repro (Linux, Ruby 3.3.6, Bundler 4.0.22; no network beyond rubygems.org)
The env variant is the same with
export BUNDLE_PATH__SYSTEM=trueinstead of the local config.Expected vs actual
BUNDLE_PATH__SYSTEM: "true"is honored "as bundler itself ignores it". Withpath.systemtrue in the winning tier (Bundler::Settings#path→Path#use_system_gems?istrue), Bundler's install root is thegem envhome. Soapplyshould patch that copy (or at least also patch it), andvexshould verify the copy Bundler loads and refuse withnot_appliedwhile it's unpatched.vendor/bundleis crawled.applysucceeds on the unused copy, the loaded copy stays unpatched, andvexattestsnot_affected.Matrix (agent mode, Linux, Ruby 3.3.6, main
9c43dfc)path.system: true+ leftovervendor/bundleBUNDLE_PATH__SYSTEM=true+ leftovervendor/bundlepath.system: true, novendor/bundlevexcorrect)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. Itsvexomits the patch for an unrelated reason (the oldsetupgating), 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 setspath.systemtrue.crates/socket-patch-core/src/crawlers/ruby_crawler.rs:132:!discovery.default_root_has_storesthen suppresses thegem envhomes, the only place the loaded copy lives.crates/socket-patch-core/src/crawlers/ruby_crawler.rs:1474(parse_bundle_config_path):path.systemonly drops the recorded path. The envBUNDLE_PATH__SYSTEMonly drops the global tier (global_path_config_unless_env_path_settings,:1293).