Repository navigation
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:
- 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. - Hosted→vendored eject or takeover (
vendoron a hosted project, orscan --mode vendored) has a dry/wet parity break, the same class as Gemvendor --dry-runeject previewseject_planned(exit 0) for a hosted gem declared in agroupblock, but the realvendorrefusesgemfile_declaration_not_editable(exit 1) #1761.vendor --dry-runreportseject_plannedwith exit 0. The realvendorthen failsapply_failed(exit 1) and rolls back (eject_rolled_back).scan --mode vendored --dry-runalso 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-runapproves 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
- Expected: vendored gem edits keep the lock's own line ending, as hosted mode does and as Bundler does (
definition.rb:428-433). docs/ecosystems.md lists the vendored gem pair edit as supported and documents no line-ending limit for gems. Where the edit would fail, a--dry-runmust report the same refusal or failure as the wet run (the parity contract behind Gemvendor --dry-runeject previewseject_planned(exit 0) for a hosted gem declared in agroupblock, but the realvendorrefusesgemfile_declaration_not_editable(exit 1) #1761 / Skip on-disk gem preview in dry takeovers #1786). - Actual:
apply_failedon every CRLF lock. The eject dry run sayseject_planned.
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
crates/socket-patch-core/src/vendor/gem.rs:1753-1762:edit_lockdoestext.split('\n')and compares lines exactly.gem_section_spans, the" specs:"check and the entry parsers (at_indent,gem.rs:2114) all see\r-terminated lines. Hosted mode usesutils::line_endings::terminator(formats/gem/hosted.rs:103). The vendored path should normalize or keep the terminator the same way (see Pick the line terminator for spliced lines through one line_endings::terminator #815 / Pick inserted line terminators through line_endings::terminator (#815) #1108).crates/socket-patch-cli/src/commands/vendor.rstakeover_dry_refusal: the restored-text preview doesn't run the lock edit, so the dry eject can't see this failure (since Skip on-disk gem preview in dry takeovers #1786 the on-disk preview is skipped for gems too).
Activity
mikolalysenko commentedon Oct 29, 2026
[agent] Triaged as
priority:p1(Bundler). Not a duplicate: hosted mode already handles CRLF locks throughutils::line_endings, but the vendored lock surgery invendor/gem.rs(edit_lockat L1753 and its siblings) still splits on\nand compares header lines exactly, so\rdefeats theGEM/specs:matches. The dry-run parity half belongs with the same fix: the eject preview intakeover_dry_refusalnever 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
mikolalysenko commentedon Oct 29, 2026
[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
mikolalysenko commentedon Oct 29, 2026
[agent] Draft fix PR: #1811
Generated by Claude Code
Vendor gems whose Gemfile.lock uses CRLF
mikolalysenko commentedon Oct 29, 2026
[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, mainecd4924), then converted both files to CRLF, as anautocrlfcheckout of the committed pair would. Results:ecd4924vendorre-runapply_failedalready_vendoredvendor --revertstatus: success, but nothing reverted (vendor_lock_entry_drifted+vendor_artifact_kept+vendor_revert_kept)rollbackvendor_revert_kept("lockfile wiring drifted")remove <purl>vendor_lock_entry_drifted, nothing removedvendor_revertedrepairSo on main a Windows user can't leave vendored mode for a gem once their checkout turns CRLF, and
vendor --revertreports 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
Gemfileside (the rewiredgemline written with LF in a CRLF Gemfile) is a separate defect, filed as #1821.Generated by Claude Code