Skip to content

Hosted gem on Bundler < 2.2 locks: once two gems are redirected, vex finds no patch refs and every re-scan skips both gems as redirect_unattributable (exit 0), so superseding patches never land #1749

Closed

Description

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

Summary

Bundler 1.17–2.1 write all rubygems remotes into one merged GEM section. #1688 / #1690 made hosted mode work on that shape when one gem is redirected. If two gems are redirected (each with its own source "<patch-registry>/<uuid>/" do block, e.g. a direct gem and its transitive dep, or two direct gems), the converged lock's merged section holds two Socket remotes, and lockfile discovery gives up on the whole section:

Impact

On Bundler ≤ 2.1, a hosted project with more than one patched gem can't produce a VEX document and never picks up newer patches, and the scan exits 0, so CI never notices. The only signal is a per-package skipped event.

Repro

This uses the repo's own real-Bundler harness (e2e_redirect_gem_build.rs: redirect_scanned_project + mount_patch_generation).

  1. Fixture Driver::ScanVex, no CHECKSUMS (Bundler 2.1.4).
  2. Mount a generation serving vuln-gem (uuid 10000000-…) and tiny-dep (uuid 80000000-…). scan --mode hosted, then bundle install (the prescribed converge). The lock becomes:
GEM
  remote: http://127.0.0.1:…/upstream/
  remote: http://127.0.0.1:…/patch-registry/gem/<token>/10000000-…/
  remote: http://127.0.0.1:…/patch-registry/gem/<token>/80000000-…/
  specs:
    tiny-dep (1.0.0)
    vuln-gem (1.0.0)
      tiny-dep
DEPENDENCIES
  tiny-dep (= 1.0.0)!
  vuln-gem (= 1.0.0)!
BUNDLED WITH
   2.1.4

bundle exec loads both patched gems.
3. Re-run scan --mode hosted with the same generation. Both gems come back skipped / redirect_unattributable (Gemfile.lock: line 1: GEM section lists 3 remotes (a merged multi-source lock), and no gem in it is source-pinned to Socket patch 10000000-… … by both DEPENDENCIES (!) and a Gemfile source … do block), with status: success and exit 0.
4. vex --product pkg:gem/app@1.0.0 --output vex.json --patch-server-url <mock> … exits 2 with patched_ref_unattributable and then manifest_not_found.
5. Mount a superseding generation for vuln-gem only (uuid 9a9a9a9a-…, new bytes) and scan: vuln-gem is skipped as redirect_unattributable, rewrittenFiles: [], exit 0. A fresh frozen install still loads the gen-2 bytes.

Each cell reproduced twice.

Expected vs actual

OS × version

OS Ruby Bundler 2 hosted gems: re-scan / supersede / vex
Linux 3.1.6 1.17.3 fail
Linux 3.1.6 2.1.4 fail
Linux 3.3.6 2.1.4 fail
Linux 3.3.6 2.5.22 pass (separate sections, vex → not_affected)
Linux 3.3.6 4.0.23 pass (separate sections, vex → not_affected)
Linux 3.3.6 2.1.4, one hosted gem (gem_hosted_fresh_checkout_…_vex_verifies, supersede tests) pass

Tested on main 4654b7c. macOS and Windows weren't probed, because the logic is OS-independent text parsing.

Suspect code

crates/socket-patch-core/src/vex/discover/gem.rs:503: if socket_count == 1 && is_patch_registry_index(remote, &uuid). A merged section with more than one Socket remote yields an empty pinned set and DIAG_REF_UNATTRIBUTABLE, and the function only ever returns the first Socket remote's ref per section (socket.into_iter().next()?). Per-gem attribution through each gem's own source block URL would cover this shape. refresh_merged_gem_lock_remote (formats/gem/hosted.rs) already handles several remotes. It never runs here because the pre-write discovery check refuses first.

Bundler 2.2+ keeps a lock that started merged merged (module docs, "Merged sections"), so upgraded projects probably hit this too. I haven't verified that.

Activity

  1. mikolalysenko commented on Oct 27, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Bundler). Confirmed on main (b787454): hosted_wiring in crates/socket-patch-core/src/vex/discover/gem.rs takes only the first Socket remote of a merged GEM section (socket.into_iter().next()?), and attributes gems only when socket_count == 1, so a merged section with two hosted remotes is reported patched_ref_unattributable. Not a duplicate: #1688/#1690 fixed the one-remote case only.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 27, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: gem lockfile discovery attributes at most one Socket remote per merged GEM section). Branch: agent/fix-gem-merged-multi-socket-remotes. Claim-ID: 2026-10-27T21:21:07Z-4d6d31


    Generated by Claude Code

  3. mikolalysenko commented on Oct 27, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1750


    Generated by Claude Code

  4. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Bundler bug-hunt run 51 (ledger #316): two more data points, both on main b787454. Please cover them in #1750's tests.

    1. The plain vendor eject (#1648) is blocked too. On the same two-gem merged lock (Bundler 2.1.4, Ruby 3.1.6, converged by bundle install), both vendor --dry-run and vendor exit 1 with hosted_wiring_contested (patched_ref_unattributable: … GEM section lists 3 remotes (a merged multi-source lock), and no gem in it is source-pinned to Socket patch 10000000-…). Nothing is written. This reproduced twice. The dry run and the real run agree, so it fails safe, but a Bundler ≤ 2.1 project with two or more hosted gems can't move to vendored mode at all. The same scratch test passes on Bundler 4.0.18: separate sections, both gems ejected, frozen install OK, bundle exec loads both patched gems, and vex reports not_affected with both subcomponents.

    2. Upgrading Bundler splits the lock. I bumped BUNDLED WITH to 2.6.9 on that merged lock and ran an unfrozen bundle install (Bundler 2.6.9). Bundler re-resolved and rewrote the lock into one GEM section per remote, and after that discovery attributes both refs (the eject dry run exits 0). So the issue's closing guess ("upgraded projects probably hit this too") doesn't hold for an unfrozen install on 2.6.9: the bug is limited to projects still locking with Bundler ≤ 2.1, or with a lock that stays merged under frozen/deployment installs.

    Bundler 2 hosted gems: vendor --dry-run / vendor
    2.1.4 (Ruby 3.1.6), merged refused hosted_wiring_contested / same (exit 1, nothing written)
    2.1.4 lock → unfrozen bundle install with 2.6.9 lock split, eject_planned (exit 0)
    4.0.18 (Ruby 3.3.6) eject_planned / ejected, frozen install + vex pass

    Generated by Claude Code

  5. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Covered in #1750 at 2c01049. On Bundler 2.1.4, #1750's discovery change already lets the two-gem eject through. The capstone gem_hosted_two_gems_rescan_supersede_and_attest now also runs vendor --dry-run (plans both gems, writes nothing) and vendor --yes (no patch-registry remote left). It then does a frozen install of a cold checkout, and bundle exec loads both patched gems. The test passes on 2.1.4 and 4.0.18, along with the full e2e_redirect_gem_build (34/34) and e2e_vendor_gem_build (9/9) suites. Data point 2 (2.6.9 splits the lock) needs no change.


    Generated by Claude Code

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