Repository navigation
Description
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
Bundler 4.1 (4.1.0.beta3, released 2026-10-28) lets a Gemfile replace one gem's dependency with another: override "other-dep", with: "vuln-gem", in: "parent". The lock records that in a new OVERRIDES section, but it keeps parent's original dependency edge (parent (1.0.0) → other-dep). So no spec line in the lock names vuln-gem as a dependency.
The hosted redirect treats vuln-gem as transitive (it isn't in DEPENDENCIES) and appends a Socket source … do block, with its blank line. That part is correct. Then rollback / remove decide between "drop the appended block" and "restore an exact pin" with is_subdependency, which looks only for a 6-space spec sub-line naming the gem. No such line exists, so the restore takes the Decl::Direct arm. It writes a brand-new top-level gem "vuln-gem", "1.0.0" into the Gemfile and leaves vuln-gem (= 1.0.0) in DEPENDENCIES.
Impact
- The restored pair isn't the pre-redirect pair. The Gemfile gains a dependency the user never declared, which contradicts the contract.
- The new line pins the vulnerable version exactly. With real Bundler 4.1.0.beta3, after
vuln-gem 1.0.1is published,bundle lock --update vuln-gemmoves the pristine project to 1.0.1 but leaves the rolled-back project on 1.0.0. Unwinding the Socket patch silently blocks the project from taking the upstream fix. - The pair is self-consistent (a frozen install passes), so nothing flags it.
Expected vs actual
crates/socket-patch-cli/CLI_CONTRACT.md (rollback/remove, gem) says: "A transitive gem (one the manifest never declared) gets an appended block with a blank line before it; the restore removes that block, its blank line and the DEPENDENCIES entry, so the pair comes back byte for byte."
- Expected:
rollback/removegive back the original Gemfile and Gemfile.lock byte for byte, as they do whenparentdepends onvuln-gemnormally (control below). - Actual: see the run below.
Pristine Gemfile:
source "http://127.0.0.1:40863/upstream"
gem "parent"
override "other-dep", with: "vuln-gem", in: "parent"Pristine lock (Bundler 4.1.0.beta3):
GEM
remote: http://127.0.0.1:40863/upstream/
specs:
parent (1.0.0)
other-dep
tiny-dep (1.0.0)
vuln-gem (1.0.0)
tiny-dep
...
DEPENDENCIES
parent
OVERRIDES
"other-dep", with: "vuln-gem", in: "parent"
After scan --mode hosted, the Socket block is appended with a blank line (correct). The converging bundle install and a frozen fresh install both pass. Then rollback (or remove pkg:gem/vuln-gem@1.0.0 --yes) exits 0, and the Gemfile ends:
override "other-dep", with: "vuln-gem", in: "parent"
gem "vuln-gem", "1.0.0"and the lock's DEPENDENCIES:
DEPENDENCIES
parent
vuln-gem (= 1.0.0)
Repro
This uses a scratch variant of crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs (mock upstream + patch registry, real gem build fixtures, real bundle). Set ZZ_PARENT_MODE=override and redirect_scanned_project adds two upstream gems, other-dep 1.0.0 and parent 1.0.0 (which depends on other-dep), and writes the Gemfile above. With ZZ_PARENT_MODE=plain, parent depends on vuln-gem directly and the Gemfile has only gem "parent". The scratch test zz_parent_unwind then does: hosted scan → bundle install → frozen fresh install → rollback / remove → compare against the pristine pair → frozen fresh install.
gem install bundler:4.1.0.beta3
BUNDLER_VERSION=4.1.0.beta3 SOCKET_PATCH_BUNDLER_E2E_VERSION=4.1.0.beta3 SOCKET_PATCH_BUNDLER_E2E_REQUIRED=1 \
ZZ_PARENT_MODE=override \
cargo test --release -p socket-patch-cli --all-features --test e2e_redirect_gem_build zz_parent_unwind -- --ignored --nocaptureBundler-only impact check (a local gem generate_index repo with vuln-gem 1.0.0 and 1.0.1 published):
== pristine before: vuln-gem (1.0.0)
== pristine after 'bundle lock --update vuln-gem': vuln-gem (1.0.1)
== restored before: vuln-gem (1.0.0)
== restored after 'bundle lock --update vuln-gem': vuln-gem (1.0.0)
Matrix
| OS | Ruby | Bundler | Shape | rollback | remove |
|---|---|---|---|---|---|
| Linux | 3.3.6 | 4.1.0.beta3 | override …, with: "vuln-gem", in: "parent" |
fail (×2) | fail (×2) |
| Linux | 3.3.6 | 4.1.0.beta3 | control: parent depends on vuln-gem |
pass (byte-exact) | pass (byte-exact) |
| Linux | 3.3.6 | ≤ 4.0.23 | n/a: no override DSL |
n/a | n/a |
| macOS / Windows | any | 4.1.0.beta3 | untested (OS-independent text logic) | — | — |
4.1.0.beta2 spelled the keys to: / from: and wasn't tested. Bundler 4.1 isn't final yet, so this hits early adopters first, and every 4.1 project once it ships.
First bad version
This isn't a regression in socket-patch. It's a new Bundler lock shape that the restore heuristic (from the #457 fix, #460) doesn't account for. Tested on main cbbbdb9.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:1543-1547: thetransitivedecision requiresis_subdependency(&lines, &gem.name).crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:177-181:is_subdependencyreads only 6-space spec sub-lines and ignores theOVERRIDESsection, wherewith: "<name>"is what makes the gem a dependency.
Possible directions (not prescriptive): treat a gem named by an OVERRIDES with: value as a subdependency, or base the decision on the absence of a pre-redirect declaration rather than on the lock's dependency graph.
No probe runs: push-triggered probe workflows aren't currently running for this routine.
Activity
bundler: run 66 ledger (#1989)
mikolalysenko commentedon Nov 2, 2026
[agent] Triaged as
priority:p1(Bundler). Confirmed onmain(cbbbdb9):is_subdependencyincrates/socket-patch-core/src/patch/redirect/upstream/gem.rsonly reads 6-space spec sub-lines and never consults the lock'sOVERRIDESsection, so a gem reached only throughoverride …, with:falls into theDecl::Directrestore arm. No duplicate or open PR found; no shared root cause with other open gem issues (#1931, #1932, #1935 are about settings/gemspec readers).Generated by Claude Code
mikolalysenko commentedon Nov 2, 2026
[agent] Claiming this issue (shared root cause: the gem restore decides "transitive" from lock spec sub-lines only and ignores Bundler 4.1
OVERRIDESwith:targets). Branch: agent/fix-gem-override-transitive-restore. Claim-ID: 2026-11-02T15:24:52Z-836802Generated by Claude Code
mikolalysenko commentedon Nov 2, 2026
[agent] Draft PR: #1991
Generated by Claude Code
Keep Bundler override targets transitive on undo