Skip to content

Vendored gem mode fails apply_failed ("Gemfile.lock has no GEM section") on any CRLF Gemfile.lock, and the hosted→vendored vendor --dry-run eject still previews eject_planned #1808

Closed

Description

[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).

Summary

The vendored gem backend's lock surgery (edit_lock) splits Gemfile.lock on \n and compares the section headers exactly ("GEM", " specs:"). On a CRLF lock every line keeps its trailing \r, so no GEM section is found and the gem fails with:

"errorCode": "apply_failed",
"error": "failed to edit Gemfile.lock: Gemfile.lock has no GEM section"

A CRLF Gemfile.lock is a normal Bundler shape. A core.autocrlf=true checkout (the Git for Windows default) or a .gitattributes eol=crlf produces one, and Bundler keeps it as CRLF on purpose. bundler-4.0.23/lib/bundler/definition.rb:428 says: "Convert to \r\n if the existing lock has them, i.e., Windows with git autocrlf". Hosted mode already rewrites CRLF locks correctly (ledger run 12: CRLF manifest + CRLF lock cycle passes).

There are two symptoms:

  1. Plain vendored mode (vendor, scan --mode vendored) can't vendor any gem in a project whose lock is CRLF. The dry run and the wet run both fail the same way (exit 1), so this one at least fails closed and is consistent.
  2. Hosted→vendored eject or takeover (vendor on a hosted project, or scan --mode vendored) has a dry/wet parity break, the same class as Gem vendor --dry-run eject previews eject_planned (exit 0) for a hosted gem declared in a group block, but the real vendor refuses gemfile_declaration_not_editable (exit 1) #1761. vendor --dry-run reports eject_planned with exit 0. The real vendor then fails apply_failed (exit 1) and rolls back (eject_rolled_back). scan --mode vendored --dry-run also exits 0 while the wet takeover exits 1. The dry-run preview over the restored text never reaches the lock edit.

A CRLF Gemfile alone is fine: the eject and the frozen install pass. Only the lock's line endings matter. A UTF-8 BOM on the Gemfile is fine too.

Impact

  • Windows users (and anyone with autocrlf / eol=crlf) can't use vendored mode for gems at all.
  • CI that gates on --dry-run approves an eject that then fails. Nothing is corrupted, because the wet run rolls back to the hosted state.

Repro

These are scratch tests on the repo's own harnesses, with real gem build / bundle install and a wiremock patch API.

Plain vendored (built on gem_vendor_fresh_checkout_bundle_install_and_revert in e2e_vendor_gem_build.rs):

Gemfile: source "https://rubygems.org"\n\ngem "rack", "~> 3.1"\n
bundle config set --local path vendor/bundle && bundle install
# simulate an autocrlf checkout of the lock
ruby -e 'f="Gemfile.lock"; File.binwrite(f, File.binread(f).gsub("\n","\r\n"))'
# stage a patch for lib/rack.rb (stage_patch_with_vuln)
socket-patch vendor --json --offline --dry-run   # exit 1, apply_failed "has no GEM section"
socket-patch vendor --json --offline             # exit 1, same

Eject (built on gem_hosted_pin_previews_and_runs_a_clean_eject in e2e_redirect_gem_build.rs, from #1786):

redirect_scanned_project(Spelling::Gemfile, no CHECKSUMS, Driver::ScanVex); bundle install   # converged hosted pair
convert Gemfile.lock to CRLF (Gemfile LF or CRLF, it doesn't matter)
mount_vendored_grant
socket-patch vendor --json --yes --dry-run ...   # exit 0, "eject_planned"
socket-patch vendor --json --yes ...             # exit 1, apply_failed "has no GEM section" + eject_rolled_back
# same with `scan --mode vendored` [--dry-run]:  dry exit 0, wet exit 1

Expected vs actual

Matrix (Linux, Ruby 3.3.6; the logic is OS-independent text parsing)

Case 2.6.9 4.0.23 4.1.0.beta3
Plain vendored, CRLF lock (dry and wet) fail (exit 1 both) fail (exit 1 both) —
vendor eject, CRLF Gemfile + lock: dry → wet fail (0 → 1) fail (0 → 1) fail (0 → 1)
vendor eject, CRLF lock only — fail (0 → 1) —
scan --mode vendored takeover, CRLF pair: dry → wet fail (0 → 1) fail (0 → 1) fail (0 → 1)
vendor eject, CRLF Gemfile only — pass —
vendor eject, BOM Gemfile pass pass pass
vendor / scan --mode vendored eject, LF pair pass pass pass

Each failing cell reproduced at least twice. I couldn't run macOS / Windows probes (push-triggered probe runs aren't starting, see #316). The failure is in pure string handling, so it doesn't depend on the OS. In practice it hits Windows checkouts the most.

Tested on main ecd4924 (v5.0.1).

Suspect code

Activity

  1. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Bundler). Not a duplicate: hosted mode already handles CRLF locks through utils::line_endings, but the vendored lock surgery in vendor/gem.rs (edit_lock at L1753 and its siblings) still splits on \n and compares header lines exactly, so \r defeats the GEM / specs: matches. The dry-run parity half belongs with the same fix: the eject preview in takeover_dry_refusal never runs the lock edit. Related refactor: #780 (one Gemfile.lock read model) and #815 (one line-terminator rule), but this bug can be fixed on its own. Open for a fix.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: vendored gem lock surgery splits Gemfile.lock on LF and matches headers exactly, so CRLF lines never match; the eject dry run never runs that edit). Branch: agent/fix-vendored-gem-crlf-lock. Claim-ID: 2026-10-29T04:20:37Z-19c52a


    Generated by Claude Code

  3. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft fix PR: #1811


    Generated by Claude Code

  4. added a commit that references this issue on Oct 29, 2026
    2cdd480
  5. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] More evidence from the Bundler bug-hunt (run 55): the same root cause also blocks un-vendoring after a CRLF re-checkout.

    I vendored on an LF Gemfile + Gemfile.lock (gem_vendor_fresh_checkout_bundle_install_and_revert, Bundler 4.0.23, main ecd4924), then converted both files to CRLF, as an autocrlf checkout of the committed pair would. Results:

    Command main ecd4924 PR #1811 head
    vendor re-run exit 1 apply_failed exit 0 already_vendored
    vendor --revert exit 0, status: success, but nothing reverted (vendor_lock_entry_drifted + vendor_artifact_kept + vendor_revert_kept) exit 0, reverted, lock stays CRLF
    rollback exit 1 vendor_revert_kept ("lockfile wiring drifted") exit 0, reverted
    remove <purl> exit 1 vendor_lock_entry_drifted, nothing removed exit 0 vendor_reverted
    repair exit 0 exit 0

    So on main a Windows user can't leave vendored mode for a gem once their checkout turns CRLF, and vendor --revert reports success while doing nothing. #1811 fixes all of it on this shape. A regression test on the revert/remove/rollback side of the CRLF re-checkout might be worth adding there.

    The Gemfile side (the rewired gem line written with LF in a CRLF Gemfile) is a separate defect, filed as #1821.


    Generated by Claude Code

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