Skip to content

Vendored gem refuses a gem whose spec is not in the lock's first GEM section #779

Description

[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: discussion #560 register.

Kind: bug. Source: new finding, register E59. It is a symptom of the three Gemfile.lock section models (review Part 5.4, E19); the consolidation refactor is linked below.

Problem

Vendored gem mode looks for the gem's spec only in the first GEM section of Gemfile.lock. edit_lock calls section_span(&lines, "GEM"), which returns the first line equal to GEM. When the spec isn't there, it falls back to "our previous PATH section" and otherwise fails with Gemfile.lock GEM specs has no entry `<name> (<version>)`.

Bundler 2 writes one GEM section per rubygems source, sorted by remote. Any project with a second source (a private gem server in a source "…" do block) can therefore have rubygems.org in the second section, and every public gem in it then can't be vendored. The other modes handle this: formats::gem::parse (inventory, VEX) keeps every section, and hosted converge_gem_lock_source walks every GEM section.

Reproduction (proved by execution on 045d7ec, run twice)

A real lock from Bundler 4.0.17 / Ruby 3.3.6, generated with bundle lock. Gemfile:

source "https://rubygems.org"
gem "rack", "2.2.8"
source "file:///…/repo" do   # any second source; https://gems.example.com sorts the same way
  gem "aaa-internal"
end

Bundler wrote the private source first:

GEM
  remote: file:///…/repo/
  specs:
    aaa-internal (1.0.0)

GEM
  remote: https://rubygems.org/
  specs:
    rack (2.2.8)
…
DEPENDENCIES
  aaa-internal!
  rack (= 2.2.8)

A unit probe in vendor/gem.rs (not committed) on that exact lock:

  • vendored edit_lock(lock, "rack", "2.2.8", rel) → Err("Gemfile.lock GEM specs has no entry `rack (2.2.8)`");
  • control, the same lock with the two GEM sections swapped → Ok;
  • formats::gem::parse(lock).entries() → pkg:gem/aaa-internal@1.0.0 and pkg:gem/rack@2.2.8 (resolved https://rubygems.org/downloads/rack-2.2.8.gem), so scan and VEX see rack;
  • hosted converge_gem_lock_source on the same lock → ok=true, edits redirect_gemfile_lock_dependency_pin + redirect_gemfile_lock_gem_source.

So scan offers a patch for rack@2.2.8, scan --mode hosted wires it, and vendor / scan --mode vendored fails it with a message that says the lock has no such entry, although it does.

Symptoms and impact

I found no existing issue (searched "GEM section", "multi-source", section_span, edit_lock). It affects every vendored gem in a project whose Gemfile has more than one source, when the gem's source isn't the alphabetically first remote. That is common for teams with a private gem server. It fails closed (nothing is written), but the patch can't be applied at all in vendored mode, and the error is misleading.

Proposed change

Find the gem's spec across all GEM sections, using the shared formats::gem::parse section list rather than section_span(…, "GEM"):

  • lift the block out of the section that holds it;
  • apply the platform-sibling and specs:-stanza checks to that section;
  • keep step 3's "PATH section above the GEM section" placement correct (Bundler sorts sections by source identifier, so check the insert position against path_source_identifier across all sections);
  • make revert (find_path_section / the spec move back at #L2239-L2270) restore the block into the section recorded at vendor time. Record the section's remote: in the ledger wiring if it isn't there already.

The full model consolidation is a separate refactor (linked in a comment below). This fix should touch only the section lookup, so it can land first.

Size and scope

vendor/gem.rs only, est. +60 / −20 production lines plus tests. Out of scope: platform-specific gems (still refused) and gems.locked (#736 / PR #750).

Acceptance criteria

  • Unit tests: vendoring and reverting a gem that sits in the second of two GEM sections produces Bundler's canonical lock, and revert restores the original bytes exactly.
  • An e2e_vendor_gem_build case with a two-source Gemfile (a local file:// gem repo as the second source, as above): vendor, then bundle install --local / bundle exec ruby -e 'require "rack"' loads the patched copy, and vendor --revert restores the lock byte for byte.
  • Existing vendor/gem.rs tests and e2e_vendor_gem_build stay green.

Dependencies

None blocks it, but coordinate with open PRs #768 and #776, which also edit vendor/gem.rs.

Activity

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:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingpm:bundlerBundler (RubyGems)priority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions