GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||
GeezerVison is showing github.com — fix(gem): warn when the hosted redirect leaves a stale unpatched install materialized by mikolalysenko · Pull Request #219 · SocketDev/socket-patch Conversation
|
| bundler | warm bundle install after redirect |
--force/--redownload |
remove gems dir + cache .gem + specifications entry, then bundle install |
|---|---|---|---|
| 1.17.3 | INERT (exit 0, no marker, cache sha stays upstream 8ed4f462…) |
exists (--redownload is an alias) but does NOT heal — exit 0, reinstalls from the stale cache |
HEALS — cache sha becomes patched 52d9c99a…, marker present |
| 2.7.2 | INERT (exit 0, no marker) — the campaign's earlier "warm fetch" on b2/b4 was a warm-path artifact; the clean warm chain is inert | both flags exist; same stale-cache reinstall behavior class | HEALS |
| 4.0.18 | INERT (exit 0, no marker) — converged lock + CHECKSUMS do NOT protect: checksums verify at download time only, and nothing is downloaded | does NOT heal — exit 37 "mismatched checksums" (loud, but upstream bytes stay installed; it reinstalls from cache instead of refetching) | HEALS |
Verdict: the defect is universal — the warning is emitted on every bundler major, and the remedy prescribes removal + bundle install, never --force/--redownload.
After the gem hosted rewrite, run_redirect probes the project's installed-gem discovery paths — the existing RubyCrawler::get_gem_paths + find_by_purls APIs the apply flow uses (vendor/bundle/<engine>/<ver>/gems deployment layouts, or the gem env homes for non-deployment installs) — for each confirmed gem redirect, and hash-compares the materialization against the patch record's afterHash file map (verify_file_patch, the same primitive vex verification uses):
afterHash → already patched, never warns — this is the one check that cannot false-positive: an agent-mode apply patches the installed tree in place while the cached .gem stays upstream, so the (also-considered) cache-sha comparison would cry wolf on every agent→hosted migration and was rejected;redirect_gem_stale_install warning in both the JSON envelope (redirect.warnings[]) and text output (code-tagged stderr line), naming the installed dir, the cache .gem, and the specifications entry, and prescribing the verified remedy;--dry-run (nothing rewritten, no records fetched) and when the patch record could not be fetched (record_fetch_failed already fired — no afterHash map, no sound judgment); re-fires on idempotent re-scans until the stale materialization is gone.52d9c99a5f87…, Socket Community Patch marker present).BUNDLE_PATH layout (vendor/bundle/gems/..., no ruby/<ver> scoping) is not discovered by today's crawler. Synergy with the in-flight crawler PR: detection goes through RubyCrawler::get_gem_paths, so the parallel flat-BUNDLE_PATH crawler extension makes this probe cover bundler 1 automatically — deliberately not duplicated here.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 only redirect_gem_no_checksums_section/redirect_gem_frozen_install present); already-patched materialization → quiet; fresh checkout → quiet.scan/hosted.rs: warning text (paths + verified remedy + --force/--redownload called out as non-remedies), afterHash judgment (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.test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 395 filtered out (--lib commands::scan::hosted)
test result: ok. 3 passed; 0 failed; 0 ignored (e2e_redirect_gem_stale_install)
test result: ok. 1 passed; 0 failed; 0 ignored (redirect_golden)
test result: ok. 410 passed; 0 failed; 1 ignored (socket-patch-cli --lib)
Plus in_process_redirect, in_process_redirect_pnpm, e2e_vex_redirect all green; cargo fmt --check clean on touched files; cargo clippy --lib --test e2e_redirect_gem_stale_install -- -D warnings clean.
cargo test -p socket-patch-core --test redirect_golden — green, untouched. The probe is CLI-side only; rewrite_gem and every fixture byte are unchanged, so no re-bless and no depscan gem.ts lockstep 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.)
CLI_CONTRACT.md: new "Gem stale-install guard" paragraph in the hosted-mode contract + a redirect_gem_stale_install row 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 install keeps the vulnerable bytes (verified across bundler 1.17 / 2.7 / 4.0). --force/--redownload do not fix it—they reinstall from the stale cached .gem.
After a successful hosted rewrite, run_redirect now runs a read-only probe for each confirmed pkg:gem/ redirect: RubyCrawler finds installed gems, compares files to the patch record’s afterHash map (same check as apply/vex). Already-patched trees stay quiet; stale upstream materializations emit redirect_gem_stale_install in redirect.warnings[] and on stderr (code-tagged), naming the gems dir, cache .gem, and specifications entry and prescribing removal plus bundle install. Skipped on --dry-run and when no record/afterHash exists; 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 in hosted.rs and hermetic e2e e2e_redirect_gem_stale_install.rs.
Reviewed by Cursor Bugbot for commit 47a23bc. Configure here.
CI was RED on test (windows-latest) exactly as the review predicted: the round-1 unit tests substring-matched forward-slash/mixed-separator literals against Path::join output (backslash-joined on Windows). All fixed, plus the review's triaged findings:
MUST-FIX, all done:
display() of identically-constructed PathBufs. The only remaining slash literal is a URL assert.--global/--global-prefix threading — the probe's CrawlerOptions mirrors scan's own discovery (was hardcoded false/None); pinned by gem_stale_probe_honors_global_prefix.--vex contradiction — the probe returns GemStaleOutcome { warnings, stale_purls }; stale purls are excluded from assume_applied, falling back to installed-tree verification (patched installs still attest with hash evidence; stale ones are omitted, and an all-stale --vex run fails per the embedded-VEX contract instead of attesting a live CVE). Pinned by gem_hosted_stale_purl_is_not_vex_attested_in_the_same_run./patches/view 5xx can no longer silently retire the warning. Pinned by gem_hosted_stale_warning_refires_when_record_fetch_fails (view 200 once, 500 after) + a unit twin.vex::verify::verify_patch_record is now pub; the probe's duplicate gem_install_matches_record is deleted.current_hash present) and hash 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. 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>.gem joins 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's gemChecksumSha256, hashed via core's file_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 an expect() invariant (per #198 convention); (10) record availability folds into the candidate filter with an early return before any crawler work or gem env spawn — the --dry-run gate 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 onto tests/common/mod.rs run_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):
cargo clippy --workspace --all-features -- -D warnings (the exact CI invocation): clean
cargo test -p socket-patch-cli --lib: ok. 419 passed; 0 failed; 1 ignored
e2e_redirect_gem_stale_install: ok. 19 passed; 0 failed
e2e_vex_redirect / in_process_redirect / in_process_redirect_pnpm / repair_invariants:
ok. 5 / 41 / 8 / 20 passed; 0 failed
cargo test -p socket-patch-core --test redirect_golden: ok. 1 passed (untouched, no re-bless)
cargo test -p socket-patch-core --lib vex::verify: ok. 27 passed
rustfmt --check on all touched files: clean
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…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/[email protected]): 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 <[email protected]>
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 <[email protected]>
There was an error while loading. Please reload this page.
…lass-split fallback-home failure semantics Review-round fixes for the two verified Bugbot findings on #222. Finding 1 (Medium): the containment guard's `gem_bundle_config_path_ignored` was a bare eprintln inside the crawler — it never reached any --json `warnings[]` and printed under --silent, violating the repo-wide warning conventions (#219/#220 + the #223 omnibus silent fixes). The crawler is now print-free: the refusal is RECORDED on `BundleStoreDiscovery.skipped_config_path`, and a shared `config_path_ignored_warning(value)` builder feeds the CLI channels — scan pushes it onto the same run-level channel as the PnP layout refusals (JSON `warnings[]` on both the zero-package and >=1-package envelopes; one stderr line gated on !json && !silent), and apply carries it in its Envelope `warnings[]` plus one gated stderr line. Scoped like the crawl that hit it: local mode, gem in --ecosystems/manifest scope. Finding 2 (High, with nuance): with an env/config bundle root, get_gem_paths appends the gem-env fallback homes and the multi-copy fan-out patched EVERY copy with per-copy loud-fail — so a gem present in both the bundle store and a shared home (rvm @global, root-owned system dir) failed the WHOLE run on a permission failure or variant mismatch THERE, even though the copy bundler loads patched fine. Patching a shared home's copy is not itself wrong (plain apply always patched GEM_HOME when no store existed); the defect was failure semantics crossing store classes. Fix: discovery's store list is exposed (`RubyCrawler::discover_bundle_stores`, fs-probes only) and apply's gem fan-out classes each copy — bundle-path store copies stay PRIMARY (loud-fail, unchanged); gem-env fallback-home copies become BEST-EFFORT once at least one store copy applied: a variant mismatch or write failure there is a per-copy non-fatal Skipped event (`gem_fallback_home_skipped`, detail names the path and reason; gated stderr twin), never a run failure. Parity edge kept: with NO bundle-store copy (the historic fallback-only layout, and every --global run) the home copy IS primary and keeps loud-fail exactly as pre-#218 apply. TDD evidence (red -> green on the rebased tip): scan/apply --json missing the warnings[] entry and the --silent leak (3 tests, in_process_gem_config_warning.rs); mismatched-home-copy exit 1 and Failed-event-on-strict-refusal (in_process_gem_fallback_home.rs), with fallback-only loud parity and both-copies-patched pinned green throughout. CLI_CONTRACT.md documents the copy classes and the warning channels. Gates: touched files rustfmt-clean; cargo clippy --workspace --all-features -D warnings clean; core --lib 2535, cli --lib 436; crawlers::ruby 50, crawler_ruby_e2e 25; gem+npm multicopy 2+2; apply_network 11, apply_invariants 4, cli_gem_variant_mismatch_policy 6, cli_apply_silent 2, e2e_gem hermetic 8 (6 shown +cache selftests), e2e_scan, cli_scan_silent, docker_e2e_gem — all green. Co-Authored-By: Claude Fable 5 <[email protected]>
…pstream release in place Measured on real Pipenv 11.10.4, 2018.11.26 and 2026.8.0, hosted AND vendored: with the same release already installed, `pipenv install`, `pipenv install --deploy` and `pipenv sync` all exit 0 and keep the upstream bytes (pip: "Requirement already satisfied"). The rewritten lock protects fresh installs only, and nothing said so — the same silent-CVE class the gem redirect guards against (#219). Every Pipenv install proof so far uninstalled the package before installing, which hid it. Hosted: a post-rewrite probe over the Python crawler's site-packages (VIRTUAL_ENV, ./.venv, ./venv, Pipenv's out-of-tree venv; --global honoured) judges each confirmed Pipfile.lock redirect with the shared `verify_patch_record` oracle and warns `redirect_pipenv_stale_install` on POSITIVE evidence only (readable bytes ≠ afterHash); an agent-mode-patched install, a lock-only checkout and missing/unreadable files stay silent, and stale purls are excluded from the same-run `--vex` attestation exactly like gem's. Gated off on --dry-run. `confirmed_pipenv_uuids` (previously written and never read) scopes the probe to the redirects the Pipenv rewriter made. Vendored: the pipenv wiring path pushes `pypi_pipenv_stale_install` under the same rules. Remedy, verified on 2026 and 2018 with the Pipfile byte-untouched: `pipenv run pip uninstall -y <pkg> && pipenv sync` or `pipenv --rm && pipenv sync`. `pipenv uninstall <pkg>` is NOT it — it rewrites the Pipfile and re-locks the patch away (the package vanishes); `PIP_FORCE_REINSTALL=1 pipenv sync` works on 2018 but is ignored by 2026. Co-Authored-By: Claude Fable 5.1 <[email protected]>
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.