[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
Dependencies
None blocks it, but coordinate with open PRs #768 and #776, which also edit vendor/gem.rs.
[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.locksection 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
GEMsection ofGemfile.lock.edit_lockcallssection_span(&lines, "GEM"), which returns the first line equal toGEM. When the spec isn't there, it falls back to "our previous PATH section" and otherwise fails withGemfile.lock GEM specs has no entry `<name> (<version>)`.Bundler 2 writes one
GEMsection per rubygems source, sorted by remote. Any project with a second source (a private gem server in asource "…" doblock) 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 hostedconverge_gem_lock_sourcewalks everyGEMsection.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:Bundler wrote the private source first:
A unit probe in
vendor/gem.rs(not committed) on that exact lock:edit_lock(lock, "rack", "2.2.8", rel)→Err("Gemfile.lock GEM specs has no entry `rack (2.2.8)`");GEMsections swapped →Ok;formats::gem::parse(lock).entries()→pkg:gem/aaa-internal@1.0.0andpkg:gem/rack@2.2.8(resolvedhttps://rubygems.org/downloads/rack-2.2.8.gem), so scan and VEX see rack;converge_gem_lock_sourceon the same lock →ok=true, editsredirect_gemfile_lock_dependency_pin+redirect_gemfile_lock_gem_source.So
scanoffers a patch forrack@2.2.8,scan --mode hostedwires it, andvendor/scan --mode vendoredfails 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
GEMsections, using the sharedformats::gem::parsesection list rather thansection_span(…, "GEM"):specs:-stanza checks to that section;path_source_identifieracross all sections);find_path_section/ the spec move back at#L2239-L2270) restore the block into the section recorded at vendor time. Record the section'sremote: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.rsonly, est. +60 / −20 production lines plus tests. Out of scope: platform-specific gems (still refused) andgems.locked(#736 / PR #750).Acceptance criteria
GEMsections produces Bundler's canonical lock, and revert restores the original bytes exactly.e2e_vendor_gem_buildcase with a two-source Gemfile (a localfile://gem repo as the second source, as above):vendor, thenbundle install --local/bundle exec ruby -e 'require "rack"'loads the patched copy, andvendor --revertrestores the lock byte for byte.vendor/gem.rstests ande2e_vendor_gem_buildstay green.Dependencies
None blocks it, but coordinate with open PRs #768 and #776, which also edit
vendor/gem.rs.