Skip to content

Hosted gem redirect breaks a multi-line gem declaration (the Gemfile stops parsing) and drops a trailing if/unless modifier #340

Description

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

  1. 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.
  2. 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

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