Skip to content

Hosted gem rollback / remove turn a gem reached only through a Bundler 4.1 override …, with:, in: into a new top-level exact pin, so the project can no longer update past the vulnerable version #1989

Closed

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.1 is published, bundle lock --update vuln-gem moves 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 / remove give back the original Gemfile and Gemfile.lock byte for byte, as they do when parent depends on vuln-gem normally (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 --nocapture

Bundler-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: the transitive decision requires is_subdependency(&lines, &gem.name).
  • crates/socket-patch-core/src/patch/redirect/upstream/gem.rs:177-181: is_subdependency reads only 6-space spec sub-lines and ignores the OVERRIDES section, where with: "<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

  1. mikolalysenko commented on Nov 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Bundler). Confirmed on main (cbbbdb9): is_subdependency in crates/socket-patch-core/src/patch/redirect/upstream/gem.rs only reads 6-space spec sub-lines and never consults the lock's OVERRIDES section, so a gem reached only through override …, with: falls into the Decl::Direct restore 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

  2. mikolalysenko commented on Nov 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the gem restore decides "transitive" from lock spec sub-lines only and ignores Bundler 4.1 OVERRIDES with: targets). Branch: agent/fix-gem-override-transitive-restore. Claim-ID: 2026-11-02T15:24:52Z-836802


    Generated by Claude Code

  3. mikolalysenko commented on Nov 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1991


    Generated by Claude Code

  4. added a commit that references this issue on Nov 2, 2026
    cb98e0e
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