Skip to content

Hosted gem redirect ignores Bundler 4's custom lockfile (lockfile setting / BUNDLE_LOCKFILE), so it never pins the lock Bundler uses and frozen installs fail with no warning #749

Description

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

Summary

Bundler 4.0 added custom lockfiles: bundle config set lockfile <path> (BUNDLE_LOCKFILE: in .bundle/config), the BUNDLE_LOCKFILE env var, bundle install --lockfile, and a lockfile "<path>" Gemfile DSL method (rubygems #9059, #9111, #9146). socket-patch has no reference to any of these. The gem manifest/lock pair is always Gemfile+Gemfile.lock or gems.rb+gems.locked (LoadedManifest::pair, crates/socket-patch-core/src/formats/gem/manifest.rs:76).

So when a project sets lockfile custom.lock, scan --mode hosted:

  • rewrites only the Gemfile and never pins the CHECKSUMS-era lock Bundler actually reads;
  • emits no warning: neither redirect_gem_frozen_install nor anything else. It exits 0 with status: success, and the in-run --vex attests not_affected;
  • when a leftover Gemfile.lock also exists, rewrites that lock, which Bundler ignores.

The next BUNDLE_FROZEN=true or deployment bundle install then fails with exit 16 ("Run bundle install elsewhere and add the updated custom.lock to version control"). The same project installs frozen without trouble before the scan, and a default-lock project installs frozen without trouble after it.

Impact

On Bundler 4, the converged CHECKSUMS rewrite is what makes a hosted redirect frozen-installable. With a custom lockfile, production/CI installs (frozen or deployment) break after a scan that reported success with no caveat. An unfrozen install does end up patched, because the Gemfile source block wins. Post-install vex also can't find the hosted pin, because it only reads Gemfile.lock/gems.locked (BUNDLER_LOCKS, vex/discover/gem.rs:135). Without a leftover Gemfile.lock it fails with "nothing to attest" (exit 2), even though the install is patched.

Repro (Linux, Ruby 3.3.6, Bundler 4.0.17; patch API and registry mocked on loopback, real rubygems.org upstream)

printf 'source "https://rubygems.org"\n\ngem "rake"\ngem "colorize", "~> 0.8.1"\n' > Gemfile
bundle config set --local path vendor/bundle
bundle install && mv Gemfile.lock custom.lock && rm -rf vendor   # default 4.x lock has CHECKSUMS
bundle config set --local lockfile custom.lock                    # or: export BUNDLE_LOCKFILE=custom.lock
BUNDLE_FROZEN=true bundle install; echo $?                         # control: 0
rm -rf vendor
socket-patch scan --mode hosted --json --yes --cwd . --vex out.vex.json --vex-product pkg:generic/app@1 ...
#   status=success, rewrittenFiles=["Gemfile"], redirect.warnings=[], vex statements=1
grep -c patch-registry custom.lock                                 # 0: the lock Bundler uses is untouched
BUNDLE_FROZEN=true bundle install; echo $?                         # 16

Expected vs actual

  • Expected: docs/ecosystems.md (RubyGems row) and CLI_CONTRACT.md describe hosted gem mode as editing the manifest/lock pair Bundler loads. A converged CHECKSUMS lock is frozen-installable "as written — no caveat", and a pair that frozen mode would reject gets redirect_gem_frozen_install. A lock Bundler is configured to use should be the one that gets pinned. If that isn't supported, it should be refused in the same way redirect_gem_bundle_gemfile_unsupported refuses a foreign BUNDLE_GEMFILE.
  • Actual: the configured lock is invisible. The scan reports success with no warning, and the next frozen install fails.

Matrix

OS Ruby Bundler Lock selector Leftover Gemfile.lock Scan warnings Lock pinned Frozen install after scan Unfrozen install
Linux 3.3.6 4.0.17 .bundle/config lockfile no none none (Gemfile only) exit 16 (control 0) patched
Linux 3.3.6 4.0.17 .bundle/config lockfile yes none Gemfile.lock (ignored by Bundler) exit 16 patched
Linux 3.3.6 4.0.17 env BUNDLE_LOCKFILE yes none Gemfile.lock (ignored) exit 16 patched
Linux 3.3.6 4.0.17 Gemfile lockfile "custom.lock" DSL no none none exit 16 (Bundler 4.0.17 also fails frozen here before the scan, so this is Bundler's own limit) patched
Linux 3.3.6 4.0.17 default Gemfile.lock (control) n/a none Gemfile.lock 0, patched
Linux 3.3.6 ≤ 2.7 n/a (feature introduced in Bundler 4.0.0)

The config path is OS-independent, so no macOS/Windows probe was run. Vendored mode (vendor), which also hard-codes Gemfile.lock, is untested with a custom lock.

Suspect code

  • crates/socket-patch-core/src/formats/gem/manifest.rs:76 (LoadedManifest::pair): the lock is derived only from the manifest name. Bundler 4's SharedHelpers.default_lockfile checks BUNDLE_LOCKFILE first, and CLI promotes options[:lockfile] || ENV["BUNDLE_LOCKFILE"] || Bundler.settings[:lockfile].
  • crates/socket-patch-core/src/vex/discover/gem.rs:135: the BUNDLER_LOCKS loop.
  • The settings layer that reads BUNDLE_GEMFILE / BUNDLE_CACHE_PATH from .bundle/config and env could read BUNDLE_LOCKFILE the same way. This is related to the reader-side pair issue Lock inventory reads only Gemfile.lock, so a gems.rb project's gems.locked is invisible and a stale Gemfile.lock is read instead #736, but that one is about gems.locked vs Gemfile.lock, not a configured lock.

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