[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
scan --mode hosted / get --mode hosted rewrite a direct gem's declaration into a source "<patch-registry>" do … end block. The recognizer (gem_line_re) and gem_line_trailing_options only look at the rest of the one physical line after the gem name. Two common Gemfile shapes come out wrong:
- A declaration that continues on the next line (
gem "x", ↵ require: false): the block is spliced over the first line only. The continuation line ends up orphaned after end, so the Gemfile no longer parses. bundle install fails with exit 4 (syntax error, unexpected ':'). The scan still exits 0 with status: success and redirected: 1, and the same run's --vex writes a not_affected statement.
- A trailing modifier (
gem "x" if ENV[...], … unless …): the modifier isn't an option, so it's silently dropped. The gem becomes unconditional on every machine.
The vendored backend refuses both forms (vendor/gem.rs rest_blocks_edit: "the declaration continues on the next line", "conditional declaration"). The hosted rewriter has no equivalent guard.
Impact
- (1) Every
bundle command in the project fails after the hosted scan, and CI goes red on a commit the CLI called a success. The embedded VEX attests a CVE as not_affected for a project that can't install at all. Wrapping long gem lines is common in Rails Gemfiles (RuboCop's default line length pushes people to do it).
- (2) The dependency graph changes silently: a gem the user excluded per environment is now always required.
Repro
This uses the hermetic fixture from crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs: a wiremock upstream compact index, a patch registry and the patches API. The only change is the fixture Gemfile.
# Gemfile (before)
source "http://127.0.0.1:PORT/upstream"
gem "vuln-gem",
require: false
$ bundle config set --local path vendor/bundle && bundle install # ok
$ rm -rf vendor
$ socket-patch scan --mode hosted --json --yes --api-url $API --org test-org --api-token fake --vex out.vex.json --vex-product pkg:gem/[email protected]
-> exit 0, status "success", redirected 1, rewrittenFiles ["Gemfile"(, "Gemfile.lock")], vex statements 1 (not_affected)
$ cat Gemfile
source "http://127.0.0.1:PORT/upstream"
source "http://127.0.0.1:PORT/patch-registry/gem/<token>/<uuid>/" do
gem "vuln-gem", "1.0.0"
end
require: false
$ bundle install # fresh checkout of Gemfile + Gemfile.lock + .socket + .bundle
[!] There was an error parsing `Gemfile`: syntax error, unexpected ':', expecting end-of-input - require: false
exit 4
The same happens with gem "vuln-gem", "~> 1.0", ↵ require: false, group: :test, where both options are lost and the Gemfile breaks.
Modifier variant:
gem "vuln-gem" if ENV["WITH_VULN"] != "0"
# after scan --mode hosted:
source "…/patch-registry/gem/<token>/<uuid>/" do
gem "vuln-gem", "1.0.0"
end
Expected vs actual
- Expected: a declaration the rewriter can't move without changing it is refused before any write, the way the vendored twin already refuses, with a warning such as the existing
redirect_gem_unrecognized_declaration ("in a form the rewriter cannot safely edit; redirect skipped"). Otherwise the move must carry the whole call. CLI_CONTRACT.md / docs/ecosystems.md describe the hosted gem redirect as a per-dep source block that keeps the declaration's options (the code comment at redirect/mod.rs:6302 notes "Trailing options … must survive the move"). A redirect that leaves an unparseable Gemfile must not count as redirected or be attested by VEX.
- Actual: the Gemfile is corrupted (1), or the condition is silently dropped (2). Exit 0,
redirected: 1, and a not_affected VEX statement.
A minor, cosmetic effect of the same regex: its ^\s* prefix also consumes the preceding blank line(s) and the line's indentation (see the missing blank line after source above, and the dedented block inside group … do).
Matrix (Linux; the rewrite is pure text, so no OS dependence is expected)
| OS |
Ruby |
Bundler |
lock |
multi-line decl |
if modifier |
| Linux |
3.3.6 |
4.0.9 |
CHECKSUMS (converged) |
fail (exit 4) |
not run |
| Linux |
3.3.6 |
4.0.9 |
no CHECKSUMS |
fail (exit 4) |
fail (dropped) |
| Linux |
3.3.6 |
2.4.22 |
no CHECKSUMS |
fail (exit 4) |
not run |
| Linux |
3.3.6 |
4.0.9 |
group :x do + single-line decl (control) |
pass (installs patched bytes) |
— |
Each fail reproduced at least twice. macOS/Windows weren't probed, because the defect is in OS-independent string handling.
First bad
Not bisected. Present on main f6b7fb9 (4.0.0).
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:6215 (gem_line_re: ^\s*gem…["']name["']([^\n]*)$, a single-line tail)
crates/socket-patch-core/src/patch/redirect/mod.rs:6305 → :5523 gem_line_trailing_options: returns "" for a tail of , or if …, so the continuation or modifier is lost
- Compare
crates/socket-patch-core/src/vendor/gem.rs:1969 rest_blocks_edit, which refuses ends_with(',') and if / unless
[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).
Summary
scan --mode hosted/get --mode hostedrewrite a direct gem's declaration into asource "<patch-registry>" do … endblock. The recognizer (gem_line_re) andgem_line_trailing_optionsonly look at the rest of the one physical line after the gem name. Two common Gemfile shapes come out wrong:gem "x",↵require: false): the block is spliced over the first line only. The continuation line ends up orphaned afterend, so the Gemfile no longer parses.bundle installfails with exit 4 (syntax error, unexpected ':'). The scan still exits 0 withstatus: successandredirected: 1, and the same run's--vexwrites anot_affectedstatement.gem "x" if ENV[...],… unless …): the modifier isn't an option, so it's silently dropped. The gem becomes unconditional on every machine.The vendored backend refuses both forms (
vendor/gem.rsrest_blocks_edit: "the declaration continues on the next line", "conditional declaration"). The hosted rewriter has no equivalent guard.Impact
bundlecommand in the project fails after the hosted scan, and CI goes red on a commit the CLI called a success. The embedded VEX attests a CVE asnot_affectedfor a project that can't install at all. Wrapping longgemlines is common in Rails Gemfiles (RuboCop's default line length pushes people to do it).Repro
This uses the hermetic fixture from
crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs: a wiremock upstream compact index, a patch registry and the patches API. The only change is the fixture Gemfile.The same happens with
gem "vuln-gem", "~> 1.0",↵require: false, group: :test, where both options are lost and the Gemfile breaks.Modifier variant:
Expected vs actual
redirect_gem_unrecognized_declaration("in a form the rewriter cannot safely edit; redirect skipped"). Otherwise the move must carry the whole call. CLI_CONTRACT.md / docs/ecosystems.md describe the hosted gem redirect as a per-depsourceblock that keeps the declaration's options (the code comment atredirect/mod.rs:6302notes "Trailing options … must survive the move"). A redirect that leaves an unparseable Gemfile must not count asredirectedor be attested by VEX.redirected: 1, and anot_affectedVEX statement.A minor, cosmetic effect of the same regex: its
^\s*prefix also consumes the preceding blank line(s) and the line's indentation (see the missing blank line aftersourceabove, and the dedented block insidegroup … do).Matrix (Linux; the rewrite is pure text, so no OS dependence is expected)
ifmodifiergroup :x do+ single-line decl (control)Each fail reproduced at least twice. macOS/Windows weren't probed, because the defect is in OS-independent string handling.
First bad
Not bisected. Present on main
f6b7fb9(4.0.0).Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:6215(gem_line_re:^\s*gem…["']name["']([^\n]*)$, a single-line tail)crates/socket-patch-core/src/patch/redirect/mod.rs:6305→:5523gem_line_trailing_options: returns""for a tail of,orif …, so the continuation or modifier is lostcrates/socket-patch-core/src/vendor/gem.rs:1969rest_blocks_edit, which refusesends_with(',')andif/unless