fix(gem): warn when the hosted redirect leaves a stale unpatched install materialized - #219
Merged
Mikola Lysenko (mikolalysenko) merged 2 commits intoAug 19, 2026
Conversation
…materialized Live-verified defect (2026-08-19 gem matrix, D2): `scan --mode hosted` for gems is a pure Gemfile/Gemfile.lock text rewrite with no stale-install handling. On the natural warm path — `bundle install` first, redirect second — the next `bundle install` exits 0, prints `Using <gem>`, and never refetches: the installed files and cached .gem stay UPSTREAM while the Gemfile + lock claim the patch registry. Re-verified UNIVERSAL in fresh containers: bundler 1.17.3, 2.7.2, AND 4.0.18 all stay inert (bundler 4's CHECKSUMS verify at download time only — nothing is downloaded), and `bundle install --force`/`--redownload` do NOT heal it (they reinstall from the stale cached .gem: bundler 1 silently, bundler 4 with an exit-37 refusal that still leaves upstream bytes installed). The only verified remedy is removing the installed dir + cache .gem + specifications entry, then `bundle install` (verified 3/3 majors). After the rewrite, the hosted flow now probes the installed-gem discovery paths (RubyCrawler — the same APIs apply uses) for each confirmed gem redirect and hash-compares the materialization against the patch record's afterHash file map. All-files-at-afterHash = already patched, never warned (an agent→hosted migration stays quiet by construction — its cache .gem is upstream, which is why a cache-sha check was rejected). Anything else emits a loud, machine-readable `redirect_gem_stale_install` warning (JSON redirect.warnings[] + code-tagged stderr) naming the three stale paths and prescribing the verified remedy. Read-only: nothing is ever deleted. Fix verified end-to-end in the bundler-4 container against the real prod patch (pkg:gem/activestorage@6.0.3): warning fires naming the exact paths; following the prescription installs the patched gem (cache sha 52d9c99a…). Bundler 1's FLAT BUNDLE_PATH layout is not yet discovered by the crawler — the in-flight flat-layout crawler extension makes this probe cover it automatically (no duplication here by design). redirect_golden: green untouched (no rewriter changes — the probe is CLI-side); no re-bless, no depscan gem.ts lockstep needed for this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
enabled auto-merge (squash)
August 19, 2026 17:29
Adversarial-review + Windows-CI fix round on the redirect_gem_stale_install guard: - Windows CI RED fixed: every path assertion (unit + e2e) now builds its expected string with the same component-wise joins production uses — no embedded a/b/c literals whose display is substring-matched against backslash-joined output. - The probe threads --global/--global-prefix into CrawlerOptions like every sibling discovery site (a stale gem in the run's own discovery home was invisible). - Same-run --vex consistency: stale-flagged purls are returned structurally (GemStaleOutcome.stale_purls) and EXCLUDED from assume_applied — an envelope can no longer attest a CVE its own warning says is live; excluded purls fall back to installed-tree verification. - Re-fire guarantee: record lookup is uuid-keyed and falls back to the redirect ledger's persisted records, so a transient /patches/view failure cannot silently retire the warning. - One shared oracle: core's vex::verify::verify_patch_record is now pub and the probe's duplicate judgment (gem_install_matches_record) is deleted. - False-positive hardening: staleness now needs POSITIVE evidence — readable bytes hashing to neither expectation; missing/unreadable files (verify_file_patch folds IO errors into NotFound) never warn. Variant purls resolving to one installed dir stay quiet when ANY record judges it patched. - vendor/cache coverage: a committed bundle-cache archive joins a project-local delete list (bundler installs from it first), and a sha256-mismatched committed archive warns standalone — a fresh checkout with a committed stale cache no longer stays silent. - Blast-radius wording split: shared gem-env homes get a prefer-project-local-bundle-path remedy instead of an unconditional delete prescription; non-UTF8 dir leaves are skipped, the unreachable parent arm is an expect() invariant. - Early return before any crawler work / gem env spawn when no judgment is possible; prose deduped to one canonical CLI_CONTRACT narrative; repair's hosted-skip message acknowledges the stale-install re-check. - New tests: two-gem-home multiplicity, global-prefix parity, ledger re-fire (view 5xx), same-run --vex exclusion, positive-evidence matrix, variant suppression, vendor/cache folded + standalone + patched-quiet; e2e migrated onto tests/common helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Aug 19, 2026
Mikola Lysenko (mikolalysenko)
deleted the
fix/gem-hosted-stale-install-warning
branch
August 19, 2026 19:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defect (D2, 2026-08-19 gem live-matrix — verified 3/3 in fresh containers)
scan --mode hostedfor gems is a pure Gemfile/Gemfile.lock text rewrite with no stale-install handling. The natural user chain —bundle installfirst (gem materialized under BUNDLE_PATH), hosted redirect second — leaves the nextbundle installinert: exit 0,Using activestorage 6.0.3, no refetch. The installed files and the cached.gemstay UPSTREAM/UNPATCHED while Gemfile + lock claim the patched registry. Silent, zero warnings, CVE stays live.Empirical scoping (this PR's campaign, fresh containers, real prod patch
pkg:gem/activestorage@6.0.3, uuid15e960b5-f432-4b6c-b8aa-534a2b419323)bundle installafter redirect--force/--redownload.gem+ specifications entry, thenbundle install8ed4f462…)--redownloadis an alias) but does NOT heal — exit 0, reinstalls from the stale cache52d9c99a…, marker presentVerdict: the defect is universal — the warning is emitted on every bundler major, and the remedy prescribes removal +
bundle install, never--force/--redownload.Fix (CLI-side, read-only — no rewriter changes)
After the gem hosted rewrite,
run_redirectprobes the project's installed-gem discovery paths — the existingRubyCrawler::get_gem_paths+find_by_purlsAPIs the apply flow uses (vendor/bundle/<engine>/<ver>/gemsdeployment layouts, or thegem envhomes for non-deployment installs) — for each confirmed gem redirect, and hash-compares the materialization against the patch record'safterHashfile map (verify_file_patch, the same primitivevexverification uses):afterHash→ already patched, never warns — this is the one check that cannot false-positive: an agent-modeapplypatches the installed tree in place while the cached.gemstays upstream, so the (also-considered) cache-sha comparison would cry wolf on every agent→hosted migration and was rejected;redirect_gem_stale_installwarning in both the JSON envelope (redirect.warnings[]) and text output (code-tagged stderr line), naming the installed dir, the cache.gem, and thespecificationsentry, and prescribing the verified remedy;--dry-run(nothing rewritten, no records fetched) and when the patch record could not be fetched (record_fetch_failedalready fired — noafterHashmap, no sound judgment); re-fires on idempotent re-scans until the stale materialization is gone.Live verification of the FIX (fixed binary in the containers, real prod patch)
52d9c99a5f87…,Socket Community Patchmarker present).BUNDLE_PATHlayout (vendor/bundle/gems/..., noruby/<ver>scoping) is not discovered by today's crawler. Synergy with the in-flight crawler PR: detection goes throughRubyCrawler::get_gem_paths, so the parallel flat-BUNDLE_PATH crawler extension makes this probe cover bundler 1 automatically — deliberately not duplicated here.Tests (test-first, RED→GREEN)
tests/e2e_redirect_gem_stale_install.rs(real binary + wiremock, hand-laid deployment layout, no ruby/gem/bundler needed): stale materialization → exactly one warning naming all three paths + the remedy, tree untouched, stderr mirror on the human path (RED before the fix — verified failing with onlyredirect_gem_no_checksums_section/redirect_gem_frozen_installpresent); already-patched materialization → quiet; fresh checkout → quiet.scan/hosted.rs: warning text (paths + verified remedy +--force/--redownloadcalled out as non-remedies),afterHashjudgment (patched/upstream/tampered/missing), probe end-to-end over a temp deployment layout (stale/patched/missing-record/zero-file-record/non-gem), uuid-fallback record lookup.Plus
in_process_redirect,in_process_redirect_pnpm,e2e_vex_redirectall green;cargo fmt --checkclean on touched files;cargo clippy --lib --test e2e_redirect_gem_stale_install -- -D warningsclean.Golden suite / depscan twin
cargo test -p socket-patch-core --test redirect_golden— green, untouched. The probe is CLI-side only;rewrite_gemand every fixture byte are unchanged, so no re-bless and no depscangem.tslockstep change is required for this PR. (The depscan hosted PR flow has no install-side context to probe; if the backend ever wants the equivalent guard, it is a separate design.)Contract
CLI_CONTRACT.md: new "Gem stale-install guard" paragraph in the hosted-mode contract + aredirect_gem_stale_installrow in the stable-tags vocabulary table (additive = MINOR per the table's own note).docs/ecosystems.md: gem hosted column notes the guard.🤖 Generated with Claude Code
Note
Low Risk
Warning-only CLI behavior with no lockfile rewriter changes; read-only detection and additive JSON/stderr contract (MINOR semver).
Overview
Hosted gem redirects only rewrite Gemfile/lock text; if a gem was already installed from upstream, later
bundle installkeeps the vulnerable bytes (verified across bundler 1.17 / 2.7 / 4.0).--force/--redownloaddo not fix it—they reinstall from the stale cached.gem.After a successful hosted rewrite,
run_redirectnow runs a read-only probe for each confirmedpkg:gem/redirect:RubyCrawlerfinds installed gems, compares files to the patch record’safterHashmap (same check as apply/vex). Already-patched trees stay quiet; stale upstream materializations emitredirect_gem_stale_installinredirect.warnings[]and on stderr (code-tagged), naming the gems dir, cache.gem, and specifications entry and prescribing removal plusbundle install. Skipped on--dry-runand when no record/afterHashexists; warnings repeat on re-scan until the stale install is gone. Exit code and status unchanged.Contract updates:
CLI_CONTRACT.md(gem stale-install guard + stable tag table),docs/ecosystems.md(hosted gem column). Unit tests inhosted.rsand hermetic e2ee2e_redirect_gem_stale_install.rs.Reviewed by Cursor Bugbot for commit 47a23bc. Configure here.
Round 2 — adversarial review + Windows CI (commit 97771c8)
CI was RED on
test (windows-latest)exactly as the review predicted: the round-1 unit tests substring-matched forward-slash/mixed-separator literals againstPath::joinoutput (backslash-joined on Windows). All fixed, plus the review's triaged findings:MUST-FIX, all done:
display()of identically-constructedPathBufs. The only remaining slash literal is a URL assert.--global/--global-prefixthreading — the probe'sCrawlerOptionsmirrors scan's own discovery (was hardcodedfalse/None); pinned bygem_stale_probe_honors_global_prefix.--vexcontradiction — the probe returnsGemStaleOutcome { warnings, stale_purls }; stale purls are excluded fromassume_applied, falling back to installed-tree verification (patched installs still attest with hash evidence; stale ones are omitted, and an all-stale--vexrun fails per the embedded-VEX contract instead of attesting a live CVE). Pinned bygem_hosted_stale_purl_is_not_vex_attested_in_the_same_run./patches/view5xx can no longer silently retire the warning. Pinned bygem_hosted_stale_warning_refires_when_record_fetch_fails(view 200 once, 500 after) + a unit twin.vex::verify::verify_patch_recordis nowpub; the probe's duplicategem_install_matches_recordis deleted.current_hashpresent) and hash to neither expectation; missing/unreadable files (verify_file_patchfolds IO errors intoNotFound) never warn. Variant purls resolving to one installed dir stay quiet when ANY record judges it patched. Both pinned (gem_stale_positive_evidence_requires_readable_mismatched_bytes,gem_stale_probe_variant_records_stay_quiet_when_any_judges_patched).SHOULD-FIX, all done: (7) committed
vendor/cache/<leaf>.gemjoins a project-local delete list (bundler installs from it first — a remedy leaving it behind silently reinstates stale bytes), and a sha256-mismatched committed archive warns standalone so a fresh checkout with a committed stale cache is no longer silent (sha from the registry override'sgemChecksumSha256, hashed via core'sfile_sha256_hex; unreadable → no evidence → quiet); (8) shared gem-env homes get a prefer-project-local-bundle-path remedy instead of an unconditional delete prescription; (9) non-UTF8 dir leaves are skipped, the unreachable no-parent arm is anexpect()invariant (per #198 convention); (10) record availability folds into the candidate filter with an early return before any crawler work orgem envspawn — the--dry-rungate at the call site stays deliberately explicit (review asked for "derived", but the round's own ledger fallback (4) breaks the derivation: a dry-run over an already-redirected project would otherwise warn about state the run didn't create — documented at the gate); (11) OR-assert split (code tag AND path asserted separately), two-gem-home multiplicity e2e added, e2e migrated ontotests/common/mod.rsrun_with_env/parse_json_envelope(SOCKET_NO_UPDATE_CHECK now covered); (12) uuid-only lookup replaces purl-then-uuid (the fallback-only test became the uuid-keyed lookup test); (13) prose deduped — CLI_CONTRACT's "Gem stale-install guard" is the one canonical narrative, the table row / ecosystems.md / fn docs / e2e header are sentence + pointer, and the warning details keep only the load-bearing remedy text.Skipped per triage (follow-ups): the pypi/composer sibling stale hazards (same structural issue — hosted rewrite is pure lockfile text with no stale-install handling; needs its own campaign), loop-inversion batching of
locate_gem_dir, repair.rs integration beyond the one-liner (its hosted-skip message now notes the re-scan "also re-checks for stale pre-redirect installs").Round-2 gates (all green):