Skip to content

fix(gem): bundler vendored+hosted audit — 13 confirmed bugs fixed, tests hardened - #175

Open
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/gem-bundler-audit
Open

fix(gem): bundler vendored+hosted audit — 13 confirmed bugs fixed, tests hardened#175
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/gem-bundler-audit

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Full ULTRACODE review + test matrix over the gem/bundler ecosystem (vendored and hosted modes, all configurations). 24 findings survived adversarial verification; the 13 well-scoped ones are fixed here, each pinned by a test that fails without its fix. Four follow-up PRs (plugin bootstrap, apply/rollback policy, vendor integrity, coverage hardening) will stack on this branch.

Hosted redirect (patch/redirect/mod.rs, gem section)

  • Commented-duplicate corruption (P1): the Gemfile edit replaced the first substring occurrence of the matched line, so a commented-out copy above the live line got rewritten, corrupting the Gemfile. Now splices by regex byte range.
  • Re-run nesting on grant rotation (P1): the idempotency guard keyed on the exact index URL, but the grant token rotates per request — a re-run nested a second source block. Guard is now grant-agnostic; a rotated grant refreshes the URL in place (new edit kind redirect_gemfile_source_url).
  • Attested no-op redirects (P1): gem-level git:/github:/path:/source: options survive into the Socket source block and override it in bundler's DSL — the redirect did nothing yet was confirmed and VEX-attested. Now fails closed with redirect_gem_source_option.
  • Platform-suffixed CHECKSUMS siblings now fail closed (redirect_gem_platform_unsupported) instead of inserting a duplicate bare-coordinate pin (P2).
  • Paren/tab/multi-space gem declarations are recognized; the append branch is gated on the gem being genuinely undeclared — no more duplicate declarations bundler rejects (P2).
  • The lock CHECKSUMS pin is gated on the Gemfile source redirect actually landing (a mixed state guaranteed a checksum failure) (P2).
  • New warning that a redirected pair breaks BUNDLE_FROZEN/--deployment installs (pnpm-warning parity) (P2).
  • The CHECKSUMS edit now records the upstream sha as original so a future revert can restore it (golden fixture updated). Note: the depscan TS twin (registry-rewrite gem rewriter) may need the same original field for cross-language contract parity (P2).

Vendored backend (vendor/gem.rs, registry_fetch.rs)

  • Lock churn with 2+ vendored gems (P1): PATH sections were appended in vendor-invocation order; bundler regenerates them sorted by source identifier. Insert position now matches bundler's sort — verified against a real bundle lock under 4.0.15.
  • Re-vendor on a patch update was refused (P1): a new patch uuid for an already-vendored gem hit gemfile_declaration_not_editable — the backend rejected its own wiring, breaking the documented automatic re-vendor. Now rewires in place (direct + transitive shapes).
  • Gem lockfile auto-fetch was entirely dead (P1): fetched gems staged into a dir literally named gem, which the platform guard refused as platform_gem_unsupported. Now stages <name>-<version> (with a defense-in-depth coordinate re-check before any I/O).
  • Platform-suffixed GEM-specs siblings on pre-2.6 (no-CHECKSUMS) locks now fail closed, mirroring the CHECKSUMS guard (P2).

Crawler + plumbing

  • parse_dir_name_version prefers the last dotted-version boundary: http-2-1.0.1 parses as http-2@1.0.1, not the ghost http@2 — real gems (http-2, http-2-next) were never discovered (P1). Known residual: ancient dotted platform suffixes (e.g. sparc-solaris-2.8) mis-split; modern platforms are unaffected.
  • vendor/bundle discovery enumerates engine dirs (jruby, truffleruby) instead of hardcoding ruby/ (P2).
  • run_nested_apply now threads --ecosystems: scan --ecosystems gem --sync no longer applies (or mutates) other ecosystems' patches (P2).
  • scan --vendor --dry-run --vex no longer writes the VEX file nor exits 1 on not-yet-vendored state (P2).

Test hardening

  • e2e_gem lifecycle harness: BUNDLE_PATH replaces bundle install --path (removed in bundler 3+); all 3 lifecycle tests green under host bundler 4.0.15.
  • The hosted production gem leg now asserts the CHECKSUMS digest actually changed after redirect (client-verifiable today) and, in the success arm, verifies installed bytes against the published afterHashes — an inert gem patch can no longer stay green (the npm minimist blindspot, closed for gem).
  • docker_e2e_gem serves the true git-blob beforeHash so the container chain exercises the default non-forced apply path, not just --force.
  • setup_matrix_gem module doc corrected: the with-setup Docker cases ARE still a baseline gap (bootstrap deadlock; fix coming in the follow-up plugin PR).

Verification

  • socket-patch-core 2460/0, socket-patch-cli lib 350/0, clippy + fmt clean
  • e2e_gem 11/11 (incl. live lifecycle under bundler 4.0.15), e2e_vendor_gem_build 6/6 (incl. real-bundler capstone), docker_e2e_gem 2/2, docker_e2e_vendor_gem 1/1 (fresh image)
  • hosted production gem leg green: redirect verified against live prod; install still blocked by the known depscan#23630 compact-index 404 (not_built) — server-side, re-verified 2026-08-13

🤖 Generated with Claude Code


Note

Medium Risk
Changes touch gem redirect, vendored lock surgery, and nested apply scoping—high user impact for bundler workflows but bounded to gem ecosystem with extensive new regression tests; hosted redirect fail-closed paths may skip redirects users previously thought applied.

Overview
This PR tightens gem/bundler behavior across hosted registry redirects, vendored lock editing, discovery, and CLI plumbing, with tests that fail without each fix.

Hosted Gemfile/Gemfile.lock redirect now splices Gemfile edits by byte range (avoids corrupting commented duplicates), recognizes rotated grant tokens without nesting source blocks, and fails closed on git:/path:/source: options, platform CHECKSUMS siblings, and lock pins without a matching Gemfile redirect. CHECKSUMS edits record upstream sha256 as original for revert; a new warning flags frozen/deployment breakage after redirect.

Vendored gem backend inserts PATH sections in bundler’s sorted order, rewires in place on patch uuid updates (direct and transitive), and stages lockfile auto-fetch as {name}-{version} instead of a generic gem dir. Platform-suffixed GEM specs fail closed on older locks without CHECKSUMS.

Ruby crawler prefers the last dotted version boundary for names like http-2, and discovers vendor/bundle/jruby/truffleruby engine layouts—not only ruby/.

CLI: nested apply after download/scan forwards --ecosystems so scoped sync does not apply other ecosystems; scan --vendor --dry-run --vex skips VEX generation and returns a JSON skipped marker instead of writing files or failing on not-yet-vendored projects.

Tests: BUNDLE_PATH for bundler 4+, hosted gem leg asserts CHECKSUMS change and on-disk afterHashes; docker gem e2e uses real beforeHash without --force; in-process tests cover ecosystem scoping and vendor dry-run VEX.

Reviewed by Cursor Bugbot for commit 10c0079. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CRLF re-run nests Gemfile sources
    • Changed regex pattern from do\n to do\r?\n to match both Unix and Windows line endings, preventing the idempotency guard from failing on CRLF files.

Create PR

Or push these changes by commenting:

@cursor push 8058d388a7
Preview (8058d388a7)
diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs
--- a/crates/socket-patch-core/src/patch/redirect/mod.rs
+++ b/crates/socket-patch-core/src/patch/redirect/mod.rs
@@ -1786,7 +1786,7 @@
             let block_re = Regex::new(
                 &(String::from(r#"(?m)^source "("#)
                     + &url_pat
-                    + r#")" do\n  gem ["']"#
+                    + r#")" do\r?\n  gem ["']"#
                     + &regex::escape(&dep.name)
                     + r#"["']"#),
             )

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 10c0079. Configure here.

// spaces after `gem`, and the parenthesized call form.
let gem_line_re = Regex::new(
&(String::from(r#"(?m)^\s*gem ["']"#)
&(String::from(r#"(?m)^\s*gem(?:[ \t]*(\()[ \t]*|[ \t]+)["']"#)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRLF re-run nests Gemfile sources

High Severity

The new grant-agnostic idempotency guard matches do\n gem, so a CRLF Gemfile (common under Windows core.autocrlf) fails to recognize an existing Socket source block. Control then falls through to gem_line_re, which matches the indented gem inside that block and splices another source block around it, nesting sources on re-run. The previous exact-URL contains check was CRLF-tolerant for same-grant re-runs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 10c0079. Configure here.

}
println!("{}", serde_json::to_string_pretty(&result).unwrap());
return final_code;
return 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dry-run VEX skip is JSON-only

Medium Severity

The new dry-run VEX skip only covers the vendor JSON path. Interactive scan --vendor --dry-run --vex still calls embed_vex_human, so it can exit 1 on a not-yet-vendored project or write an attestation during --dry-run—the same failure mode this PR claimed to close. apply already skips VEX on dry-run for both output modes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 10c0079. Configure here.

Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Aug 14, 2026
…ll scan arms

Addresses both Bugbot findings on #175:

- The grant-agnostic Gemfile idempotency guard required LF (`do\n`), so a
  core.autocrlf checkout of a previously-redirected Gemfile was not
  recognized and the indented gem line inside the block got wrapped in a
  second, nested source block on re-run. The recognizer now accepts
  `do\r?\n`; pinned by gemfile_rerun_on_crlf_checkout_never_nests
  (verified red without the fix).

- The dry-run VEX skip only covered the vendor JSON arm; the interactive
  `scan --vendor --dry-run --vex` path (embed_vex_human) and the JSON
  `scan --apply --dry-run --vex` path (embed_vex_into_json at the apply
  fall-through) still generated the document — exiting 1 on a
  not-yet-vendored project or writing the attestation during --dry-run.
  The guard now lives at the top of both embed helpers, covering every
  scan arm; pinned by scan_vendor_dry_run_with_vex_interactive_* and
  scan_apply_json_dry_run_with_vex_* (both verified red without it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Both Bugbot findings addressed in d063cb1:

  • CRLF re-run nests Gemfile sources (High): the block recognizer now accepts do\r?\n, so a core.autocrlf checkout of a previously-redirected Gemfile is recognized instead of re-wrapped. Pinned by gemfile_rerun_on_crlf_checkout_never_nests — verified red without the fix.
  • Dry-run VEX skip is JSON-only (Medium): the guard moved to the top of both embed_vex_into_json and embed_vex_human, so every scan arm skips VEX on --dry-run — including the interactive vendor path and the JSON --apply fall-through, which had the same hole. Pinned by two new in-process tests, both verified red without the guard.

Note: took the guard-relocation fix rather than the prepared one-line autofix so the interactive and JSON-apply arms are covered too.

🤖 Generated with Claude Code

…sts hardened

ULTRACODE review + full test matrix over the gem/bundler ecosystem
(vendored and hosted modes, every configuration). 24 findings survived
adversarial verification; the 13 well-scoped ones are fixed here, each
pinned by a test that fails without its fix.

Hosted redirect (patch/redirect/mod.rs, gem section):
- splice the Gemfile edit by regex byte range: a commented-out duplicate
  of the gem line no longer gets rewritten instead of the live line
- grant-agnostic idempotency guard: re-running scan --mode hosted with a
  rotated grant token refreshes the source URL in place instead of
  nesting a second source block (new edit kind redirect_gemfile_source_url)
- fail closed on gem-level git:/github:/path:/source: options (they
  override the enclosing source block, making the redirect an attested
  no-op); warn redirect_gem_source_option and skip the dep
- fail closed on platform-suffixed CHECKSUMS siblings
  (redirect_gem_platform_unsupported) instead of inserting a duplicate
  bare-coordinate pin
- recognize paren/tab/multi-space gem declarations; gate the
  append-branch on the gem being genuinely undeclared (no more duplicate
  declarations bundler rejects)
- never pin the lock CHECKSUMS when the Gemfile source redirect did not
  land (mixed state guaranteed a checksum failure)
- warn that a redirected pair breaks frozen/deployment installs
- record the upstream sha256 line as original on the CHECKSUMS edit so a
  future revert can restore it (golden fixture updated)

Vendored backend (vendor/gem.rs):
- insert new PATH sections at bundler's sorted position (identifier
  order, verified against real bundler 4.0.15 bundle lock) — two or more
  vendored gems no longer churn the committed lock
- fail closed on platform-suffixed GEM-specs siblings on no-CHECKSUMS
  locks (mirrors the existing CHECKSUMS guard)
- re-vendor on a patch UPDATE (new uuid, same purl): recognize our own
  path: wiring and rewire in place instead of refusing with
  gemfile_declaration_not_editable — the documented automatic re-vendor
  contract now actually works for gem

Auto-fetch (vendor/registry_fetch.rs):
- stage fetched gems into the canonical <name>-<version> leaf instead of
  a dir literally named "gem", which vendor_gem refused as
  platform_gem_unsupported — lockfile auto-fetch for gems was dead

Crawler (crawlers/ruby_crawler.rs):
- parse_dir_name_version prefers the last dotted-version boundary, so
  http-2-1.0.1 parses as (http-2, 1.0.1) instead of the ghost (http, 2)
- vendor/bundle discovery enumerates engine dirs (jruby, truffleruby)
  instead of hardcoding ruby/

Scan/get plumbing:
- run_nested_apply now threads --ecosystems: scan --ecosystems gem
  --sync no longer applies (or mutates) other ecosystems' patches
- scan --vendor --dry-run --vex no longer writes the VEX file nor exits
  1 on not-yet-vendored state

Test hardening:
- e2e_gem lifecycle harness: BUNDLE_PATH replaces bundle install --path
  (removed in bundler 3+; all 3 lifecycle tests green under 4.0.15)
- e2e_hosted_production gem leg now asserts the CHECKSUMS digest CHANGED
  after redirect (client-verifiable today) and, in the success arm,
  verifies installed content against the published afterHashes — an
  inert gem patch can no longer stay green (the npm minimist blindspot)
- docker_e2e_gem serves the true git-blob beforeHash so the chain
  exercises the default non-forced apply path, not just --force
- setup_matrix_gem module doc: the with-setup Docker cases ARE still a
  baseline gap (bootstrap deadlock: plugin registration evaluates
  plugins.rb before any gems land; exit-semantics twin), doc corrected

Verified: core 2460/0, cli lib 350/0, clippy+fmt clean; e2e_gem 11/11
(incl. live lifecycle under bundler 4.0.15), e2e_vendor_gem_build 6/6
(incl. real-bundler capstone), docker_e2e_gem 2/2, docker_e2e_vendor_gem
1/1, hosted production gem leg green (redirect verified; install still
blocked by the known depscan#23630 compact-index 404 — server-side).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ll scan arms

Addresses both Bugbot findings on #175:

- The grant-agnostic Gemfile idempotency guard required LF (`do\n`), so a
  core.autocrlf checkout of a previously-redirected Gemfile was not
  recognized and the indented gem line inside the block got wrapped in a
  second, nested source block on re-run. The recognizer now accepts
  `do\r?\n`; pinned by gemfile_rerun_on_crlf_checkout_never_nests
  (verified red without the fix).

- The dry-run VEX skip only covered the vendor JSON arm; the interactive
  `scan --vendor --dry-run --vex` path (embed_vex_human) and the JSON
  `scan --apply --dry-run --vex` path (embed_vex_into_json at the apply
  fall-through) still generated the document — exiting 1 on a
  not-yet-vendored project or writing the attestation during --dry-run.
  The guard now lives at the top of both embed helpers, covering every
  scan arm; pinned by scan_vendor_dry_run_with_vex_interactive_* and
  scan_apply_json_dry_run_with_vex_* (both verified red without it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Rebased fix/gem-bundler-audit onto origin/main (4e5288e, #164) — new tip 99f9cd0 (was d063cb1 on b109645; main gained 11 commits).

Conflicts resolved (1):

Auto-merges verified semantically:

Verification (all green):

  • cargo test -p socket-patch-core full: 2468 passed, 0 failed (redirect goldens included)
  • cli --features docker-e2e,setup-e2e: --lib 367 ok; in_process_scan 27 ok (both conflict-side tests run explicitly); in_process_get_manifest_path 5; in_process_get_update_count 3; e2e_gem 8 ok / 3 ignored; docker_e2e_gem 2; docker_e2e_vendor_gem 1; e2e_vendor_gem_build 5 + host capstone (--ignored, ruby 3.4.10 / bundler 4.0.15) 1 ok; in_process_gem_apply 7; in_process_gem_multi_platform 4; scan_vendor_e2e 18; in_process_vendor 27
  • cargo clippy --workspace --all-features -- -D warnings (CI-exact): clean
  • cargo fmt --check: fails locally, but the failures are pre-existing on origin/main under the pinned toolchain (rustfmt 1.8.0) — the branch's violations are a strict subset of main's (it actually fixes two get.rs ones), i.e. the rebase adds zero new fmt drift. CI has no fmt gate (ci.yml runs clippy only), so left main's files untouched rather than folding out-of-scope reformats into these commits.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant