Repository navigation
Hosted gem scan ignores a Gemfile lockfile "custom.lock" DSL call (Bundler 4), so it pins the unused Gemfile.lock, leaves the real lock untouched, and every frozen install fails #1477
Description
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
Bundler 4 has four ways to name a custom lockfile: BUNDLE_LOCKFILE in the env, the app config or the global config, plus a lockfile "<path>" call in the Gemfile itself. #768 (closing #749) refuses the three config/env spellings with redirect_gem_bundle_lockfile_unsupported. Nothing reads the Gemfile DSL spelling. With a Gemfile that starts with lockfile "custom.lock", scan --mode hosted:
- rewrites the
Gemfile(adds the patch-registrysource … doblock), - pins the leftover
Gemfile.lock, which Bundler no longer reads, - leaves
custom.lock, the lock Bundler actually reads, byte-untouched, - reports no
redirect_gem_bundle_lockfile_unsupported(or any lockfile) warning.
The next BUNDLE_FROZEN=true / deployment bundle install then fails: "The list of sources changed, but the lockfile can't be updated because frozen mode is set". The same project installs frozen without trouble before the scan.
The previous bug-hunt run posted this gap as a comment on the closed #749 (2026-10-08, main 823810a). It still reproduces on main 15c2c83, so this issue tracks it on its own.
Impact
CI and production installs (frozen or deployment) of a Bundler 4 project that uses the lockfile DSL break after a hosted scan. In the #749 comment's shape (no pre-existing install), the scan also exits 0 and the in-run --vex attests not_affected for a lock that was never pinned.
Repro
I used a scratch copy of crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs, gem_hosted_bundler4_custom_lockfile_redirects_nothing. In the ScanVexCustomLockfile setup, I replaced bundle config set --local lockfile custom.lock with prepending lockfile "custom.lock"\n to the Gemfile. custom.lock is a copy of the pristine lock, and Gemfile.lock is left behind, exactly as in the existing test. I replaced the assertions with prints, then ran:
SOCKET_PATCH_BUNDLER_E2E_REQUIRED=1 cargo test -p socket-patch-cli --test <scratch copy> -- \
gem_hosted_bundler4_custom_lockfile_redirects_nothing --include-ignored --nocapture
- Pristine project with the DSL line:
BUNDLE_FROZEN=true bundle installexits 0. scan --mode hosted --json --vex …producessummary.applied: 1. Its warnings are onlyredirect_gem_stale_installandvex_omitted(this harness pre-installs intovendor/bundle), and there is noredirect_gem_bundle_lockfile_unsupported.GemfileandGemfile.lockare rewritten (Gemfile.locknow has the patch-registryGEMsection,vuln-gem (= 1.0.0)!and the patched sha256).custom.lockis unchanged (upstream remote,DEPENDENCIES vuln-gem, upstream sha256).- The frozen install afterwards fails:
The list of sources changed, but the lockfile can't be updated because frozen
mode is set
You have added to the Gemfile:
* vuln-gem (= 1.0.0)
You have deleted from the Gemfile:
* vuln-gem
You have changed in the Gemfile:
* vuln-gem from `no specified source` to `rubygems repository http://127.0.0.1:…/patch-registry/gem/…/`
Expected vs actual
- Expected: docs/ecosystems.md (RubyGems row) says "bundler 4's
BUNDLE_LOCKFILE… naming any other lock is refused withredirect_gem_bundle_lockfile_unsupported", and Hosted gem redirect ignores Bundler 4's custom lockfile (lockfilesetting /BUNDLE_LOCKFILE), so it never pins the lock Bundler uses and frozen installs fail with no warning #749's contract is that the scan refuses, writes nothing and still installs frozen. Bundler'sDsl#to_definitionlets the Gemfilelockfilecall override the CLI/config lock (lockfile = @lockfile unless @lockfile.nil?, bundler 4.0.18lib/bundler/dsl.rb:193), so the DSL spelling should get the same refusal, or the scan should pin the lock it names. - Actual: the wrong lock is pinned with no warning, and frozen installs break.
Matrix (Linux, Ruby 3.3.6)
| Bundler | main | Lockfile spelling | Refused | custom.lock pinned |
Frozen install after scan |
|---|---|---|---|---|---|
| 4.0.22 | 15c2c83 |
Gemfile DSL lockfile "custom.lock" |
no (×2) | no | fails |
| 4.0.18 | 15c2c83 |
Gemfile DSL | no | no | fails |
| 4.0.18 | 823810a |
Gemfile DSL, no prior install (#749 comment) | no | no | fails, exit 16; in-run VEX attests |
| 4.0.x | 15c2c83 |
.bundle/config / env / global BUNDLE_LOCKFILE |
yes (existing test) | n/a | passes |
| < 4.0 | — | n/a (the DSL method doesn't exist) | — | — | — |
macOS and Windows weren't probed. The logic is OS-independent.
Suspect code
LoadedManifest::with_lockfile (crates/socket-patch-core/src/formats/gem/manifest.rs:183) only receives BUNDLE_LOCKFILE from env, app config and global config (manifest.rs:262 reads the config key). No gem reader parses a top-level lockfile call in the Gemfile / gems.rb, so the hosted engine falls through to the default Gemfile.lock pair (LoadedManifest::pair). The vendored path (crates/socket-patch-core/src/vendor/gem.rs, a_bundler4_custom_lockfile_is_refused) is probably affected the same way. I didn't verify it this run. lockfile false (Bundler writes no lock) is a related, untested shape.
Activity
mikolalysenko commentedon Oct 13, 2026
[agent] Triage:
priority:p1(Bundler). Not a duplicate: #749/#768 covered only theBUNDLE_LOCKFILEenv/app-config/global-config spellings. Confirmed onmain(15c2c83):LoadedManifest::with_lockfile(crates/socket-patch-core/src/formats/gem/manifest.rs) is fed only from env and the two config files, and no gem reader parses a top-levellockfilecall in the Gemfile /gems.rb. No open PR covers it, and it does not share a root cause with the other open gem issues (#985 is about gemspec dev deps, #1476 about the source-block scanner).Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Claiming this issue (shared root cause: the gem lock resolver never reads Bundler 4's Gemfile
lockfileDSL call). Branch: agent/fix-gem-lockfile-dsl. Claim-ID: 2026-10-13T11:21:28Z-9dc049Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Draft PR: #1479
Generated by Claude Code
Refuse a Gemfile lockfile call naming another lock
Read gem lockfile calls the way bundler runs them