Skip to content

Vendored gem rewrite puts non-string arguments after path:, so gem "x", *V, gem "x", ENV.fetch(…) or gem "x", VERSION becomes a Gemfile syntax error and every bundle command fails #847

Description

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

Summary

In vendored mode, the gem's Gemfile line is rewritten as gem <name>, <version>, path: <rel>, <opts>. Here <opts> comes from gem_line_trailing_options, which keeps everything after the leading quoted version arguments. When the declaration's version comes from a constant, a splat or a method call, that non-string argument is treated as an "option" and lands after the new path: keyword argument. Ruby rejects a positional argument after a keyword argument, so the rewritten Gemfile no longer parses.

vendor still reports status: success / applied: 1, and the in-run vex attests not_affected. But every later bundle install / bundle exec fails with There was an error parsing Gemfile: syntax error. That includes the frozen install on a fresh checkout.

Hosted mode handles the same three shapes correctly: it writes gem "colorize", "0.8.1", *V inside the source block, with no keyword in front, and a frozen fresh install loads the patched gem.

Impact

  • The project's Gemfile is broken after a "successful" vendor. CI and deploys fail until someone edits the Gemfile by hand.
  • VEX attests not_affected for a project that can't even resolve its bundle.

Repro (real Bundler; repo harness)

Copy crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs, and in gem_vendor_fresh_checkout_bundle_install_and_revert change the fixture Gemfile to any of:

source "https://rubygems.org"

RV = ["~> 3.1"]
gem "rack", *RV
gem "rack", ENV.fetch("RV", "~> 3.1")
RACK_VERSION = "~> 3.1"
gem "rack", RACK_VERSION, require: false

Then run:

SOCKET_PATCH_BUNDLER_E2E_VERSION=4.0.17 BUNDLER_VERSION=4.0.17 \
  cargo test --release -p socket-patch-cli --test <copy> -- --include-ignored \
  gem_vendor_fresh_checkout_bundle_install_and_revert --nocapture

The vendor envelope is success, and the vex leg passes. The Gemfile becomes:

gem "rack", "3.2.7", path: ".socket/vendor/gem/<uuid>/rack-3.2.7", *RV
gem "rack", "3.2.7", path: ".socket/vendor/gem/<uuid>/rack-3.2.7", ENV.fetch("RV", "~> 3.1")
gem "rack", "3.2.7", path: ".socket/vendor/gem/<uuid>/rack-3.2.7", RACK_VERSION, require: false

and the next bundle install (in place, or frozen on a fresh checkout) fails:

[!] There was an error parsing `Gemfile`: syntax error, unexpected * - ...c2d-0123456789ab/rack-3.2.7", *RV
[!] There was an error parsing `Gemfile`: syntax error, unexpected '\n', expecting => - ...2.7", ENV.fetch("RV", "~> 3.1")
[!] There was an error parsing `Gemfile`: syntax error, unexpected ',', expecting => - ...89ab/rack-3.2.7", RACK_VERSION, require: false

The Ruby grammar alone reproduces it: ruby -e 'def gem(*a, **k); end; V=["1"]; gem "x", "1", path: "p", *V' → syntax error, unexpected *.

Expected vs actual

  • Expected: docs/ecosystems.md (RubyGems row) and the gemfile_declaration_not_editable contract say vendored mode either rewrites the declaration into a working exact pin plus path:, or refuses and writes nothing. That's how it already treats multi-line, conditional, indented and parenthesized declarations. Trailing options are meant to "survive the rewrite" (comment at vendor/gem.rs:1418), not break it.
  • Actual: the line is rewritten into invalid Ruby, the run reports success, and VEX attests it.

Matrix

OS Ruby Bundler *V ENV.fetch(…) CONST, require: false Hosted, same shapes
Linux 3.3.6 4.0.17 fail fail fail pass
Linux 3.3.6 2.4.22 fail fail fail not run (OS/version-independent string logic)

macOS and Windows weren't probed: the rewrite is pure string handling with no OS-specific branch.

Suspect code

  • crates/socket-patch-core/src/vendor/gem.rs:1421-1426: new_line = format!("gem {q}{name}{q}, {q}{version}{q}, path: {q}{rel}{q}, {opts}") appends opts after the keyword.
  • crates/socket-patch-core/src/patch/redirect/mod.rs:5164 (gem_line_trailing_options): it returns the tail from the first non-quoted argument, so positional expressions (*V, CONST, ENV.fetch(…)) come back as "options".
  • rest_blocks_edit (vendor/gem.rs) doesn't refuse a tail whose first kept argument is positional.

Open PRs #637 / #731 touch these helpers but keep the path:-then-opts ordering, so neither fixes this. Possible fixes: refuse with gemfile_declaration_not_editable when the kept tail starts with a non-keyword argument, or drop the positional constraint expressions (the exact pin supersedes them) and keep only the key: / :key => options.

Found on main 045d7ec (v4.0.0 is the latest tag).

No activity

Activity on this issue will appear here.

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: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