[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).
[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 fromgem_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 newpath:keyword argument. Ruby rejects a positional argument after a keyword argument, so the rewritten Gemfile no longer parses.vendorstill reportsstatus: success/applied: 1, and the in-runvexattestsnot_affected. But every laterbundle install/bundle execfails withThere 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", *Vinside the source block, with no keyword in front, and a frozen fresh install loads the patched gem.Impact
not_affectedfor 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 ingem_vendor_fresh_checkout_bundle_install_and_revertchange the fixture Gemfile to any of:Then run:
The
vendorenvelope issuccess, and the vex leg passes. The Gemfile becomes:and the next
bundle install(in place, or frozen on a fresh checkout) fails: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
gemfile_declaration_not_editablecontract say vendored mode either rewrites the declaration into a working exact pin pluspath:, 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 atvendor/gem.rs:1418), not break it.Matrix
*VENV.fetch(…)CONST, require: falsemacOS 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}")appendsoptsafter 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-optsordering, so neither fixes this. Possible fixes: refuse withgemfile_declaration_not_editablewhen the kept tail starts with a non-keyword argument, or drop the positional constraint expressions (the exact pin supersedes them) and keep only thekey:/:key =>options.Found on main
045d7ec(v4.0.0 is the latest tag).