[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
bundle install --standalone installs the bundle into ./bundle/ruby/<api>/gems/ and writes bundle/bundler/setup.rb, which hardcodes those load paths. Apps load it with require_relative "bundle/bundler/setup". This is the usual layout for Lambda functions and self-contained CLIs.
Bundler 2.x also records BUNDLE_PATH: "bundle" in .bundle/config, and the ruby crawler follows that. Bundler 4 no longer remembers CLI flags, so it writes no .bundle/config at all. The crawler's roots are only config path, $BUNDLE_PATH, vendor/bundle and the gem env homes, so on Bundler 4 it never sees ./bundle:
- If no other copy exists,
apply reports package_not_installed ("Resolved by the project lockfile but not installed on this host (lockfile-only)"), but the gem is installed.
- If the same
name-version is also in a gem env home (a common case: another project, or a global gem install), apply patches that copy and reports success. vex then attests not_affected ("Patched via Socket patch …"), while the app still loads the unpatched ./bundle copy.
Impact
False VEX attestation, and an unpatched runtime that reports applied. Every Bundler 4 standalone project is affected.
Repro (Linux, Ruby 3.3.6, Bundler 4.0.17, main 045d7ec; any marker patch works)
gem install rack -v 3.2.7 # an ambient copy of the same version
mkdir app && cd app
printf 'source "https://rubygems.org"\ngem "rack", "~> 3.1"\n' > Gemfile
bundle _4.0.17_ install --standalone # -> ./bundle/ruby/3.3.0/gems/rack-3.2.7, no .bundle/config
# stage .socket/manifest.json + blob for pkg:gem/rack@3.2.7 (patch lib/rack.rb, append a probe constant)
socket-patch apply --json # success, 1 applied
grep -c PROBE "$(gem env gemdir)/gems/rack-3.2.7/lib/rack.rb" # 1 (system copy patched)
grep -c PROBE bundle/ruby/3.3.0/gems/rack-3.2.7/lib/rack.rb # 0 (standalone copy untouched)
socket-patch vex --offline --product pkg:gem/app@1 -O vex.json # not_affected / inline_mitigations_already_exist
ruby -e 'require_relative "bundle/bundler/setup"; require "rack"; p defined?(Rack::PROBE)' # nil, loads ./bundle/.../rack.rb
If you skip the gem install line, apply skips with package_not_installed, and vex omits the purl (package_not_found).
Expected vs actual
- Expected: the crawler covers the install roots Bundler actually uses.
ruby_crawler.rs documents probing "the bundler install roots, probed in bundler's own precedence order", and CLI_CONTRACT.md's VEX rules say an unpatched installed copy must never be attested. The ./bundle tree is what bundle/bundler/setup.rb loads.
- Actual: on Bundler 4 the standalone root is invisible. The ambient copy is patched and attested instead.
OS × version
| OS |
Ruby |
Bundler |
.bundle/config after --standalone |
Standalone copy patched |
VEX |
| Linux |
3.3.6 |
2.4.22 |
BUNDLE_PATH: "bundle" |
yes (pass) |
correct |
| Linux |
3.3.6 |
2.5.22 |
BUNDLE_PATH: "bundle" |
yes (pass) |
correct |
| Linux |
3.3.6 |
2.6.9 |
BUNDLE_PATH: "bundle" |
yes (pass) |
correct |
| Linux |
3.3.6 |
2.7.2 |
BUNDLE_PATH: "bundle" |
yes (pass) |
correct |
| Linux |
3.3.6 |
4.0.17 |
none |
no (×2 runs) |
false not_affected |
The crawler logic is OS-independent. The v4.0.0 release behaves the same way: it patches only the system copy. The trigger is Bundler 4 dropping remembered flags, not a socket-patch commit.
Suspect code
crates/socket-patch-core/src/crawlers/ruby_crawler.rs:291-330 (discover_bundle_stores_with_env): the root list is config path → $BUNDLE_PATH → vendor/bundle, with no probe for a standalone bundle/ root (for example, one marked by bundle/bundler/setup.rb).
ruby_crawler.rs:77-125 (gem_paths_and_discovery): because no project store is found, the gem env homes are appended, and those are what get patched and attested.
Hosted mode's stale-install guard probably misses the same root, because a re-run of bundle install --standalone would reuse the unpatched ./bundle copy. I haven't verified that; it's in the ledger backlog.
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
bundle install --standaloneinstalls the bundle into./bundle/ruby/<api>/gems/and writesbundle/bundler/setup.rb, which hardcodes those load paths. Apps load it withrequire_relative "bundle/bundler/setup". This is the usual layout for Lambda functions and self-contained CLIs.Bundler 2.x also records
BUNDLE_PATH: "bundle"in.bundle/config, and the ruby crawler follows that. Bundler 4 no longer remembers CLI flags, so it writes no.bundle/configat all. The crawler's roots are only configpath,$BUNDLE_PATH,vendor/bundleand thegem envhomes, so on Bundler 4 it never sees./bundle:applyreportspackage_not_installed("Resolved by the project lockfile but not installed on this host (lockfile-only)"), but the gem is installed.name-versionis also in agem envhome (a common case: another project, or a globalgem install),applypatches that copy and reports success.vexthen attestsnot_affected("Patched via Socket patch …"), while the app still loads the unpatched./bundlecopy.Impact
False VEX attestation, and an unpatched runtime that reports
applied. Every Bundler 4 standalone project is affected.Repro (Linux, Ruby 3.3.6, Bundler 4.0.17, main
045d7ec; any marker patch works)If you skip the
gem installline,applyskips withpackage_not_installed, andvexomits the purl (package_not_found).Expected vs actual
ruby_crawler.rsdocuments probing "the bundler install roots, probed in bundler's own precedence order", and CLI_CONTRACT.md's VEX rules say an unpatched installed copy must never be attested. The./bundletree is whatbundle/bundler/setup.rbloads.OS × version
.bundle/configafter--standaloneBUNDLE_PATH: "bundle"BUNDLE_PATH: "bundle"BUNDLE_PATH: "bundle"BUNDLE_PATH: "bundle"not_affectedThe crawler logic is OS-independent. The v4.0.0 release behaves the same way: it patches only the system copy. The trigger is Bundler 4 dropping remembered flags, not a socket-patch commit.
Suspect code
crates/socket-patch-core/src/crawlers/ruby_crawler.rs:291-330(discover_bundle_stores_with_env): the root list is configpath→$BUNDLE_PATH→vendor/bundle, with no probe for a standalonebundle/root (for example, one marked bybundle/bundler/setup.rb).ruby_crawler.rs:77-125(gem_paths_and_discovery): because no project store is found, thegem envhomes are appended, and those are what get patched and attested.Hosted mode's stale-install guard probably misses the same root, because a re-run of
bundle install --standalonewould reuse the unpatched./bundlecopy. I haven't verified that; it's in the ledger backlog.