[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
#431 (the fix for #341 / #390) added formats/gem/manifest.rs::classify, which decides which manifest Bundler loads. When BUNDLE_GEMFILE is set in both the environment and the project's .bundle/config, it lets the environment win. Its unit test env_wins_over_config_and_is_anchored_at_the_project_root even asserts that. Real Bundler does the opposite. In Bundler::Settings, local app config has a higher priority than ENV, and Bundler::CLI#initialize re-exports Bundler.settings[:gemfile] into ENV["BUNDLE_GEMFILE"]. So the .bundle/config value is what bundle install / bundle lock / bundle exec load.
When the two disagree, socket-patch rewrites the manifest Bundler ignores, reports redirected: 1, and the in-run VEX attests not_affected. Meanwhile bundle install resolves the configured manifest straight from upstream and installs the unpatched gem. This is the #390 failure mode, now reached through the env + config combination. vendor/gem.rs:149 calls the same bundler_loaded_manifest, so vendored mode most likely has the same issue (not verified separately).
Impact
The dangerous direction: a project committed bundle config set --local gemfile Gemfile.next (dual boot), and CI or a developer shell exports BUNDLE_GEMFILE=Gemfile. socket-patch then follows the env, sees a supported Configured { Gemfile }, rewrites Gemfile, and attests. Bundler loads Gemfile.next anyway. The result is silently unpatched code plus a false VEX attestation. The reverse combination (env gems.rb with config Gemfile, or env Gemfile with config gems.rb on a twin project) wires the wrong one of the two supported pairs in the same way.
Bundler's actual precedence (real bundler, Ruby 3.3.6)
mkdir p && cd p && mkdir .bundle
printf 'source "https://rubygems.org"\ngem "rake"\n' > Gemfile
printf 'source "https://rubygems.org"\ngem "rack"\n' > Gemfile.next
printf -- '---\nBUNDLE_GEMFILE: "Gemfile.next"\n' > .bundle/config
BUNDLE_GEMFILE=Gemfile bundle lock # writes Gemfile.next.lock, never Gemfile.lock
BUNDLE_GEMFILE=Gemfile bundle config get gemfile
# Settings for `gemfile` in order of priority. The top value will be used
# Set for your local app (.../.bundle/config): "Gemfile.next"
# Set via BUNDLE_GEMFILE: "Gemfile"
I got the same result on 4.0.17, 2.6.9 and 2.4.22, and with gems.rb (config) vs Gemfile (env) on a twin project (gems.locked written).
socket-patch repro
I used a copy of e2e_redirect_gem_build.rs's ScanVexDualBoot setup (it commits .bundle/config BUNDLE_GEMFILE: Gemfile.next with a Gemfile.next / Gemfile.next.lock copy of the pair), with one change: the CLI runs with BUNDLE_GEMFILE=Gemfile in its environment.
BUNDLE_GEMFILE=Gemfile socket-patch scan --mode hosted --json --yes --cwd proj \
--api-url <mock> --org org --api-token fake --vex out.vex.json --vex-product <p>
BUNDLE_GEMFILE=Gemfile bundle install # in proj, BUNDLE_PATH=vendor/bundle
Actual:
scan exits 0 with "redirected": 1, "rewrittenFiles": ["Gemfile"], "vex": {"statements": 1}, and no redirect_gem_bundle_gemfile_unsupported.
Gemfile gains the patch-registry source … do block, and Gemfile.next / Gemfile.next.lock are untouched.
bundle install fetches vuln-gem 1.0.0 from the upstream index only. The installed lib/vuln_gem.rb is byte-identical to the unpatched original, so installed_is_patched=false.
Expected: socket-patch resolves BUNDLE_GEMFILE the way Bundler does: .bundle/config (or $BUNDLE_APP_CONFIG/config) first, then the environment. With Gemfile.next configured, the run then refuses with redirect_gem_bundle_gemfile_unsupported and attests nothing. That's the contract CLI_CONTRACT.md documents for that code: "BUNDLE_GEMFILE … names a manifest other than the project's Gemfile / gems.rb, so no gem is redirected or attested". docs/ecosystems.md also says modes must wire "only the manifest Bundler loads".
Matrix
| OS |
Ruby |
Bundler |
Reproduces |
| Linux |
3.3.6 |
4.0.17 |
yes (twice) |
| Linux |
3.3.6 |
2.6.9 |
yes |
| Linux |
3.3.6 |
2.4.22 |
yes |
| macOS / Windows |
— |
— |
untested (the precedence logic is OS-independent) |
First bad commit: 9d718cf (#431). Before it, BUNDLE_GEMFILE was ignored entirely (#390).
Suspect code
crates/socket-patch-core/src/formats/gem/manifest.rs:127 (classify): gemfile_env is consulted before config_value.
crates/socket-patch-core/src/formats/gem/manifest.rs:213 (the unit test that pins the inverted order).
- Callers:
crates/socket-patch-core/src/hosted/engine.rs:497 and crates/socket-patch-core/src/vendor/gem.rs:149.
A minor related case: when the env and the config both name supported but different spellings, the refusal and remedy text should name the setting Bundler actually uses (bundle config unset --local gemfile).
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
#431 (the fix for #341 / #390) added
formats/gem/manifest.rs::classify, which decides which manifest Bundler loads. WhenBUNDLE_GEMFILEis set in both the environment and the project's.bundle/config, it lets the environment win. Its unit testenv_wins_over_config_and_is_anchored_at_the_project_rooteven asserts that. Real Bundler does the opposite. InBundler::Settings, local app config has a higher priority thanENV, andBundler::CLI#initializere-exportsBundler.settings[:gemfile]intoENV["BUNDLE_GEMFILE"]. So the.bundle/configvalue is whatbundle install/bundle lock/bundle execload.When the two disagree, socket-patch rewrites the manifest Bundler ignores, reports
redirected: 1, and the in-run VEX attestsnot_affected. Meanwhilebundle installresolves the configured manifest straight from upstream and installs the unpatched gem. This is the #390 failure mode, now reached through the env + config combination.vendor/gem.rs:149calls the samebundler_loaded_manifest, so vendored mode most likely has the same issue (not verified separately).Impact
The dangerous direction: a project committed
bundle config set --local gemfile Gemfile.next(dual boot), and CI or a developer shell exportsBUNDLE_GEMFILE=Gemfile. socket-patch then follows the env, sees a supportedConfigured { Gemfile }, rewritesGemfile, and attests. Bundler loadsGemfile.nextanyway. The result is silently unpatched code plus a false VEX attestation. The reverse combination (envgems.rbwith configGemfile, or envGemfilewith configgems.rbon a twin project) wires the wrong one of the two supported pairs in the same way.Bundler's actual precedence (real bundler, Ruby 3.3.6)
I got the same result on 4.0.17, 2.6.9 and 2.4.22, and with
gems.rb(config) vsGemfile(env) on a twin project (gems.lockedwritten).socket-patch repro
I used a copy of
e2e_redirect_gem_build.rs'sScanVexDualBootsetup (it commits.bundle/configBUNDLE_GEMFILE: Gemfile.nextwith aGemfile.next/Gemfile.next.lockcopy of the pair), with one change: the CLI runs withBUNDLE_GEMFILE=Gemfilein its environment.Actual:
scanexits 0 with"redirected": 1,"rewrittenFiles": ["Gemfile"],"vex": {"statements": 1}, and noredirect_gem_bundle_gemfile_unsupported.Gemfilegains the patch-registrysource … doblock, andGemfile.next/Gemfile.next.lockare untouched.bundle installfetchesvuln-gem 1.0.0from the upstream index only. The installedlib/vuln_gem.rbis byte-identical to the unpatched original, soinstalled_is_patched=false.Expected: socket-patch resolves
BUNDLE_GEMFILEthe way Bundler does:.bundle/config(or$BUNDLE_APP_CONFIG/config) first, then the environment. WithGemfile.nextconfigured, the run then refuses withredirect_gem_bundle_gemfile_unsupportedand attests nothing. That's the contract CLI_CONTRACT.md documents for that code: "BUNDLE_GEMFILE… names a manifest other than the project'sGemfile/gems.rb, so no gem is redirected or attested". docs/ecosystems.md also says modes must wire "only the manifest Bundler loads".Matrix
First bad commit: 9d718cf (#431). Before it,
BUNDLE_GEMFILEwas ignored entirely (#390).Suspect code
crates/socket-patch-core/src/formats/gem/manifest.rs:127(classify):gemfile_envis consulted beforeconfig_value.crates/socket-patch-core/src/formats/gem/manifest.rs:213(the unit test that pins the inverted order).crates/socket-patch-core/src/hosted/engine.rs:497andcrates/socket-patch-core/src/vendor/gem.rs:149.A minor related case: when the env and the config both name supported but different spellings, the refusal and remedy text should name the setting Bundler actually uses (
bundle config unset --local gemfile).