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
mikolalysenko commentedon Oct 28, 2026
[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
vendoreject (#1648) is blocked too. On the same two-gem merged lock (Bundler 2.1.4, Ruby 3.1.6, converged bybundle install), bothvendor --dry-runandvendorexit 1 withhosted_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 execloads both patched gems, andvexreportsnot_affectedwith both subcomponents.2. Upgrading Bundler splits the lock. I bumped
BUNDLED WITHto 2.6.9 on that merged lock and ran an unfrozenbundle install(Bundler 2.6.9). Bundler re-resolved and rewrote the lock into oneGEMsection 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.vendor --dry-run/vendorhosted_wiring_contested/ same (exit 1, nothing written)bundle installwith 2.6.9eject_planned(exit 0)eject_planned/ ejected, frozen install +vexpassGenerated by Claude Code
Test eject of two hosted gems on merged locks
mikolalysenko commentedon Oct 28, 2026
[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_attestnow also runsvendor --dry-run(plans both gems, writes nothing) andvendor --yes(no patch-registry remote left). It then does a frozen install of a cold checkout, andbundle execloads both patched gems. The test passes on 2.1.4 and 4.0.18, along with the fulle2e_redirect_gem_build(34/34) ande2e_vendor_gem_build(9/9) suites. Data point 2 (2.6.9 splits the lock) needs no change.Generated by Claude Code