Repository navigation
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
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:
vex(no manifest) exits 2 withpatched_ref_unattributable→manifest_not_found, although Bundler installs and loads both patched gems.- An in-sync re-scan (nothing new) skips both gems as
redirect_unattributable, and still reportsstatus: success, exit 0. - A superseding patch for either gem is skipped the same way on every run, so the project stays on the older patch for good. This is Hosted gem: on Bundler < 2.2 locks a superseding patch is never applied (scan skips it as redirect_unattributable, exits 0) #1688's symptom, back for any project with two or more hosted gems.
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).
- Fixture
Driver::ScanVex, no CHECKSUMS (Bundler 2.1.4). - Mount a generation serving
vuln-gem(uuid10000000-…) andtiny-dep(uuid80000000-…).scan --mode hosted, thenbundle 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
- Expected: CLI_CONTRACT.md's lockfile-discovery table (gem row) cross-checks a merged multi-remote
GEMsection against the Gemfile so hosted refs stay attributable. Each Socket remote here is tied to exactly one gem by a!DEPENDENCIES pin plus asource … doblock for that URL, so both gems are attributable. Hosted gem: on Bundler < 2.2 locks a superseding patch is never applied (scan skips it as redirect_unattributable, exits 0) #1688's fix promises that a superseding patch lands on merged locks. - Actual: attribution only works when the merged section has exactly one Socket remote.
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
mikolalysenko commentedon Oct 27, 2026
[agent] Triaged as
priority:p1(Bundler). Confirmed onmain(b787454):hosted_wiringincrates/socket-patch-core/src/vex/discover/gem.rstakes only the first Socket remote of a mergedGEMsection (socket.into_iter().next()?), and attributes gems only whensocket_count == 1, so a merged section with two hosted remotes is reportedpatched_ref_unattributable. Not a duplicate: #1688/#1690 fixed the one-remote case only.Generated by Claude Code
mikolalysenko commentedon Oct 27, 2026
[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
mikolalysenko commentedon Oct 27, 2026
[agent] Draft PR: #1750
Generated by Claude Code
Attribute every hosted gem in merged locks