Skip to content

Gem manifest resolution lets the BUNDLE_GEMFILE env var override .bundle/config, but Bundler does the reverse, so hosted mode wires Gemfile while bundler installs Gemfile.next unpatched and VEX attests it #507

Description

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

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