Skip to content

Fix hosted gem redirect ignoring Bundler mirror.all (#681) - #684

Open
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
agent/fix-gem-mirror-overrides-source
Open

Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
agent/fix-gem-mirror-overrides-source

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #681

Summary

Hosted gem scans refuse redirects when the effective Bundler mirror would capture the Socket patch-registry source. The refusal preserves the manifest and lockfile and emits redirect_gem_mirror_overrides_source without exposing private mirror credentials. Embedded VEX then requires verified installed bytes for hosted gems, so a previous hosted pin cannot falsely attest a missing or upstream installation.

Root cause

Bundler can capture a per-dependency source with an all-source, exact-source, or hostname mirror. The original implementation missed hostname and source-specific environment settings, treated an empty BUNDLE_APP_CONFIG as unset, and let independent VEX lockfile inference bypass a detected refusal on later scans. Automatic warning/remedy text also copied credentials from mirror URLs.

Fix

  • Model per-key app-config-over-environment precedence, followed by all-source, exact-source, and hostname lookup. Match Bundler key normalization, case/default-port/slash behavior, legacy app-config keys, and exact-source fallback-only shadowing. A configured mirror URL with a fallback timeout is conservatively refused without probing network reachability.
  • Resolve BUNDLE_APP_CONFIG and BUNDLE_IGNORE_CONFIG consistently with the shared Ruby intake. An explicitly empty app-config path selects the project-root config file. Environment mirror keys retain their literal native interpretation.
  • Withhold gem manifests and locks from hosted rewriting when a capturing mirror is found. Emit static setting-origin and remediation text, without private URLs or key tokens; users can scope the existing mirror to rubygems.org and remove the capturing setting from its actual source.
  • Propagate mirror refusal into embedded VEX. Hosted gem pins cannot use lockfile-only, assume-applied, or no-verify inference after refusal. Normal verification of installed patched bytes remains valid. Vendor/agent evidence, other ecosystems, and standalone VEX keep their existing behavior.
  • Update CLI_CONTRACT.md and docs/ecosystems.md, and add permanent parser, config-resolution, engine, VEX, and native install/rescan regressions. Wrapper packages only dispatch the binary and require no change.

Known limits: user-global ~/.bundle/config remains outside this reader; mirror settings introduced only in a later install environment cannot be observed during the scan. A capturing mirror conservatively refuses all gem redirects in the run.

Validation of the correction

  • 450 focused repository tests passed: 328 core gem/Ruby/hosted, 88 CLI hosted/VEX, 9 gem lockfile VEX, and 25 stale-install tests.
  • Four native Bundler tests passed, with six fixture executions using Ruby 3.4.10 / Bundler 4.0.17. These exercise real patch installs and repeat scans with installed/missing trees and verification enabled/disabled.
  • 22 independent CLI/install/rescan controls passed, including fresh mirror refusal, positive patched installs, credential-free diagnostics, and empty app-config handling. A separate parser oracle matched all 78 valid native observations across Bundler 2.6.9 and 4.0.17; invalid native configurations were excluded.
  • Independent reviewers matched all nine reviewed file hashes to the committed correction. Production core/CLI Clippy passed with the pre-existing macOS unused-variable allowance. Formatting has no new deviations; pre-existing scan/hosted.rs formatting is retained. These are focused local checks, not a full workspace/platform rerun.

Author validation on the original commit 468a237

Historical evidence below is from the original implementation on Linux, Ruby 3.3.6, Bundler 4.0.17, and toolchain 1.93.1. It does not establish CI status for the correction.

  • Red without the fix (the refusal disabled): gem_hosted_bundler_mirror_all_redirects_nothing FAILED with "redirect":{"redirected":1,"rewrittenFiles":["Gemfile"],…},"vex":{"statements":1,…}, the exact symptom in Hosted gem redirect ignores Bundler's mirror.all setting, so the next bundle install fetches the redirected gem's upstream bytes from the mirror while the in-run VEX attests not_affected #681. bundler_mirror_all_redirects_nothing and bundler_mirror_for_the_patch_source_redirects_nothing also FAILED.
  • Green with the fix: all 10 new unit tests pass. cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build -- --ignored: 12/12 pass, including the new test and every existing hosted gem capstone.
  • cargo clippy --workspace --all-features -- -D warnings: clean. The new files are rustfmt-clean. main itself isn't rustfmt-clean and CI doesn't gate on it, so unrelated files were left alone.
  • cargo test --workspace --all-features --no-fail-fast: 214 suites ok. 12 tests fail only because this sandbox runs as uid 0, where chmod 0555 and unremovable-file injections can't force the write failures they test (*_state_write_failure_*, *unremovable*, relax_loop_must_not_traverse_symlinked_root, wire_write_failure_*). None of them touches gem or hosted-intake code. CI runs them as a normal user.
  • CI on 468a237: green. The macOS composer 2.2.30 job first failed on a packagist DNS timeout, and its one re-run passed.

CI

CI on cb0fd60ff09b1e624eff168d0d38f8845011ea27 is still running. Bugbot reported two additional findings under independent verification; Ready for review remains off until they are resolved and complete checks are clear.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz


Note

Medium Risk
Changes hosted-mode gem redirect and embedded VEX attestation paths; incorrect mirror detection could block valid redirects or allow false attestations, though behavior is fail-closed with broad test coverage.

Overview
Hosted gem redirects now fail closed when Bundler mirror settings (mirror.all, exact patch-source, or hostname mirrors from app config or BUNDLE_MIRROR__* env) would route the Socket patch-registry source to an upstream mirror. The new formats/gem/mirror model matches Bundler precedence and key normalization; the hosted intake drops gem manifest/lock candidates and emits redirect_gem_mirror_overrides_source with remediation text that does not leak mirror URLs or credentials.

Embedded VEX on hosted scans sets hosted_gem_mirror_check / hosted_gem_mirror_refused so rediscovered hosted gem pins cannot be attested via lockfile inference, assume_applied, or --vex-no-verify when a mirror applies; verified installed bytes still count. BUNDLE_APP_CONFIG: an empty value now selects <project>/config, aligned with Bundler.

Contract docs, ecosystem matrix, unit/engine/VEX tests, and native Bundler e2e scenarios cover mirror refusal and rescan behavior.

Reviewed by Cursor Bugbot for commit 86718e9. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Bundler's mirror.all (or a mirror for the patch-registry source)
sends the per-dep source block the hosted redirect writes to the
mirror, which serves the unpatched upstream gem. The scan reported
the gem redirected and the in-run VEX attested not_affected while
the next bundle install was unpatched or failed CHECKSUMS.

The hosted intake now reads the mirror settings from the bundler app
config and BUNDLE_MIRROR__ALL and, when one captures the patch
registry, leaves the Gemfile pair untouched, attests nothing, and
warns redirect_gem_mirror_overrides_source with the remedy (scope the
mirror to rubygems.org).

Fixes #681

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 3, 2026 11:54
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@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.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/hosted/engine.rs
The redirect_gem_mirror_overrides_source detail always advised unsetting
a local mirror.all, which never clears a BUNDLE_MIRROR__ALL from the
environment or a mirror.<source> key for the patch registry. The mirror
model now returns the remedy for the setting it detected, and a test
applies each remedy and checks the next scan passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

composer 2.2.30 / php 8.3 / macos-latest failed on 468a237, and the cause isn't this PR. Three e2e_redirect_composer_build tests failed in composer update with curl error 28 while downloading https://repo.packagist.org/packages.json: Resolving timed out after 10002 milliseconds, which is a DNS timeout on the macOS runner. This PR changes only gem and hosted-intake code, and the same job passed on the previous head, ad8d067. There's no code fix to port for a network outage, so I'm re-running the failed job once.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@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.

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at head 468a237.

  • CI: all checks green on 468a237 (355 check runs: success or skipped). The earlier composer macOS failure (see comment above) passed on re-run.
  • Bugbot: reviewed 468a237 with no new issues. The one earlier finding (hosted/engine.rs:582, on ad8d067) was fixed and its thread is resolved.
  • For the reviewer: the new Bundler mirror.* detection in hosted gem intake, and how a mirror that captures the patch-registry source is reported.

Slack announcement not sent: this run has no Slack send tool, so the next run should retry.


Generated by Claude Code

@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

Codex follow-up review of cb0fd60ff09b1e624eff168d0d38f8845011ea27: one additional VEX finding is confirmed and being fixed; not ready to merge.

The older hosted gem pin finding is independently confirmed: an empty catalog or empty grants bypass mirror detection and can still yield a false attestation. A further correction is in progress. The Windows case-handling finding was investigated and dismissed using the tagged Ruby/Bundler source chain; the thread explains why OS lookup semantics do not apply to Bundler's case-sensitive snapshot. No native Windows test is claimed. The validation below covers the correction already pushed, and does not establish safety for the newly confirmed candidate-gap case.

The original P1 had several paths to a false hosted attestation: hostname/app and per-source environment mirrors escaped detection; an empty BUNDLE_APP_CONFIG selected a different config file in Bundler; and a repeat scan could infer an attestation from an old hosted pin even after detecting mirror.all, with no installed patched tree. Real Bundler installs reproduced upstream bytes after the original CLI claimed not_affected.

The correction models effective app/environment mirror precedence and all/source/hostname matching, including Bundler key normalization and fallback-only shadowing. Empty BUNDLE_APP_CONFIG now selects the same root config as Bundler. A detected mirror refusal also blocks embedded hosted-gem lockfile/assume-applied inference, including --vex-no-verify; actual verified installed bytes remain valid evidence. Refused scans preserve the manifest and lockfile. The P2 credential disclosure is fixed by using static setting/remedy text without interpolating private URLs or keys.

Validation on the exact committed source:

  • 450 repository tests passed: 328 core gem/Ruby/hosted tests, 88 CLI hosted/VEX tests, 9 gem lockfile VEX tests, and 25 stale-install tests. New permanent regressions cover fresh mirror refusal, config resolution, and repeat-scan VEX behavior.
  • Four native Bundler tests passed using Ruby 3.4.10 / Bundler 4.0.17, with six fixture executions covering actual installs and installed/missing-tree rescans.
  • 22 independent controls passed: 14 fresh CLI cases, three real install lifecycles, four old-pin rescan states, and an empty-app-config case. These include positive patched installs, refusal without file changes, and credential-free JSON/human diagnostics. An independent parser oracle matched 78 valid native observations across Bundler 2.6.9 and 4.0.17; invalid native configurations were excluded.
  • Independent reviewers matched all nine reviewed file hashes to the commit. Production core/CLI Clippy passed with only the pre-existing macOS unused_variables allowance. There are no new rustfmt deviations; existing formatting in scan/hosted.rs is retained. Merge with main 045d7ec7 is clean.

User-global Bundler config and mirror settings introduced only in a later install environment remain documented limits. Standalone VEX behavior is unchanged. This is focused local validation, not a full workspace/platform rerun.

Ready for review remains off while these findings are verified and resolved, and until complete CI and Bugbot are clear.

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Cursor (@cursor) review

@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.

Autofix Details

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

  • ✅ Fixed: VEX misses mirrors without gem candidates
    • Added independent mirror detection that checks for capturing mirrors even when no gem candidates are present, preventing false attestations for lockfile-discovered gems.

Create PR

Or push these changes by commenting:

@cursor push d702d51261
Preview (d702d51261)
diff --git a/crates/socket-patch-cli/src/commands/scan/hosted.rs b/crates/socket-patch-cli/src/commands/scan/hosted.rs
--- a/crates/socket-patch-cli/src/commands/scan/hosted.rs
+++ b/crates/socket-patch-cli/src/commands/scan/hosted.rs
@@ -1253,10 +1253,25 @@
         // rediscovers older pins too, so a candidate-only set would miss some
         // refused hosted gems. Keep their actual installed-byte verification,
         // but do not infer applied status from the intercepted source.
-        params.hosted_gem_mirror_refused = rewrite
-            .warnings
-            .iter()
-            .any(|warning| warning.code == "redirect_gem_mirror_overrides_source");
+        // Check for mirrors independently of whether gem candidates are
+        // present: a lockfile-discovered gem pin can still be affected by a
+        // capturing mirror even when this run has no gem grants. Probe for
+        // mirror.all and hostname/exact-source mirrors that would capture the
+        // patch registry, using a representative source URL.
+        params.hosted_gem_mirror_refused = {
+            let patch_registry_sources = &["https://patch.socket.dev/gem/"];
+            let mirror_detected = socket_patch_core::crawlers::ruby_crawler::bundler_source_mirror(
+                &common.cwd,
+                patch_registry_sources,
+            )
+            .await
+            .is_some();
+            mirror_detected
+                || rewrite
+                    .warnings
+                    .iter()
+                    .any(|warning| warning.code == "redirect_gem_mirror_overrides_source")
+        };
         // Stale-flagged purls are EXCLUDED from assume_applied: the same-run
         // envelope carries a redirect_gem_stale_install warning proving the
         // installed materialization unpatched, so attesting that purl from

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

Comment thread crates/socket-patch-cli/src/commands/scan/hosted.rs
Comment thread crates/socket-patch-core/src/crawlers/ruby_crawler.rs
The mirror refusal flag for embedded VEX was derived only from the
rewrite's redirect_gem_mirror_overrides_source warning, which exists
only when this run had gem candidates. A hosted scan with an empty
catalog, a paid-only gem or a withdrawn offer still rediscovers older
hosted gem pins in its VEX plan, so lockfile inference and
--vex-no-verify could attest them while Bundler fetched unpatched bytes
through a capturing mirror.

Embedded hosted VEX now checks each hosted gem pin in the completed plan
against the project's Bundler mirror settings (using the pin's own
Socket source), on the redirect path and on the hosted scan's empty
JSON and human terminal paths. Verified installed bytes remain valid
evidence; standalone and agent/vendored VEX are unchanged.

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

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@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.

✅ Bugbot reviewed your changes and found no new issues!

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

Reviewed by Cursor Bugbot for commit 86718e9. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Ready for review at head 86718e99fecac3129659200dfd0f447d858174aa.

  • CI: 491/491 check runs green (success/skipped) on 86718e9.
  • Bugbot: re-reviewed 86718e9, no new findings.
    • VEX misses mirrors without gem candidates (High): fixed in 86718e9. Embedded hosted VEX now checks each hosted gem pin in the plan against the Bundler mirror settings itself, so empty-grant and empty-catalog scans can no longer attest a pin a mirror serves unpatched. Regression test: mirror_check_refuses_rediscovered_pins_without_gem_candidates.
    • Windows env mirrors are case-sensitive (Medium): refuted earlier on the thread (Bundler snapshots ENV case-sensitively).
  • Reviewer focus: vex.rs hosted_gem_mirror_captured and the new hosted flag passed into embed_vex_into_json / embed_vex_human in scan/mod.rs.

Slack announcement not sent this run (Slack send tool unavailable).


Generated by Claude Code

This branch has not been deployed

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

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

2 participants