Skip to content

Commit 5339111

Browse files
fix(vendor): fall back to local build when the served gem stub gemspec is invalid (#221)
* test(gem): restore production e2e after the 2026-08-18 catalog republish Production republished the rubygems patch catalog on 2026-08-18: 15 free activestorage patches (5.2.0-6.0.3, GHSA-m42x-37p3-fv5w / CVE-2020-8162). The old pin activestorage@7.0.2.2 (2535d43d-…) no longer exists, and the hosted registry's compact index now serves the full dependency list — the server defect that motivated the temporary gem shutdown is fixed. - e2e_hosted_production: delete the GEM_E2E_DISABLED kill switch (all 3 sites) and the probe-based install tolerance per its own auto-retire design (gem_registry_base/http_probe helpers now dead, removed); re-pin to activestorage@6.0.3 / 15e960b5-…; the leg is renamed gem_bundler_hosted_install_proof and asserts the full chain unconditionally (redirect rewrite, CHECKSUMS pin swap to the patched sha, real install through patch.socket.dev, per-file afterHash verification). Verified live: 3/3 green incl. preflight + canary. - e2e_vendored_production: re-pin; the platform_gem_unsupported tolerance is vestigial (gap fixed in #172) and the leg's doc comment now says so. The full delivery-proof upgrade is deferred to a stacked fix PR: the served gem-stub-gemspec artifact is currently invalid (missing summary/authors; bundler rejects the path source — discovered 2026-08-19). Verified live: preflight + gem leg green. - e2e_gem: re-pin the 5.2.0 lifecycle tests to the republished patch (efc8d8ca-…, CVE-2020-8162); GEM_PURL now carries ?platform=ruby because `get` keys the manifest by the view response's qualified purl. Verified live: 3/3 green. - in_process_vendor: new test pinning the qualified-purl (?platform=ruby) path through scan --vendor — the regression that would silently re-open the #172 gap (ledger keyed by qualified purl, basePurl bare). - docs/testing/{hosted,vendored}-production-e2e.md: catalogs re-pinned, defect sections converted to FIXED-history style, new OPEN section for the invalid served stub gemspec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gem): adversarial-review hardening — self-adapting UUID pin, frozen converged-lock install proof, hermeticity Round-2 fixes on the gem e2e restoration: * hosted gem leg: GEM_UUIDS any-of set (PYPI_UUIDS precedent) — parse the UUID actually wired into the rewritten Gemfile's patch-registry URL, assert membership, and afterHash-verify against that exact patch's /patch/view; hard-assert the BUNDLE_PATH wipe; reinstall under BUNDLE_FROZEN=true with converged-lock assertions (patch-registry GEM remote + `activestorage (= 6.0.3)!` DEPENDENCIES pin), verified live * mimemagic hazard: USE_FREEDESKTOP_PLACEHOLDER=true on every gem-installing leg in e2e_hosted_production.rs and e2e_gem.rs * published_patch_advisory_counts: HTTP-status guard like its siblings * truthing: qualifiers are normalized by the SERVER, nothing client-side strips (consts comments + hosted doc coverage row + vendored doc REPLACED-pin history keeping the withdrawn 7.0.2.2/2535d43d ids findable) * e2e_redirect_gem_build.rs: module doc reframed historical, dangling is_known_defect pointer fixed to the docs history section * e2e_gem.rs: GHSA-or-CVE advisory assertion, full SOCKET_* scrub + SOCKET_NO_CONFIG, assert_after_hashes delete-entry branch * in_process_vendor.rs: qualified gem test at full parity with its bare twin (PATH remote, stub gemspec, downloaded==1, positive applied event), exact percent-encoded by-package mock paths, new detached-qualified revert test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vendor): fall back to local build when the served gem stub gemspec is invalid Defense-in-depth for defect D4 from the 2026-08-19 gem live-matrix campaign (verified 6/6 live across bundler 1.17/2.7/4.0): production's gem-stub-gemspec secondary artifact omits the rubygems-required `summary` and `authors` attributes (and `licenses`). The CLI sha512-verified the stub and wrote it verbatim as the vendored path-source `<name>.gemspec`, and every bundler major validates path-source gemspecs, so any post-vendor or fresh-checkout `bundle install` exited 1 with `missing value for attribute summary`. A depscan-side fix for the stub generator is in flight, but every currently-published gem stub is invalid, so the CLI hardens now. An INVALID served stub now follows the existing MISSING-stub policy, under its own code (additive/MINOR): - `--vendor-source auto`: loud `vendor_prebuilt_stub_invalid` warning naming the missing attributes, then fall back to the local build (installed gem + locally derived stub); - `--vendor-source service` (explicit): refuse with `vendor_prebuilt_stub_invalid`, naming the attributes and the remedy, before anything is written (no partial artifacts). Validation is a conservative textual heuristic (assignment-line presence for `summary` / `authors`|`author`, obviously-empty spellings rejected — no ruby parsing); a legitimate stub always passes and is still written byte-verbatim. A missing `licenses` is only mentioned in the message (rubygems warns, not fails). CLI_CONTRACT.md documents the new code in the fallback ladder and the PatchAction vocabulary. Tests: - hermetic (wiremock, RED->GREEN): auto+invalid-stub falls back to the local build with the loud warning and the LOCAL stub on disk; service+invalid-stub refuses with `vendor_prebuilt_stub_invalid` leaving no partial artifacts and an untouched lock; the valid-stub byte-verbatim write is pinned (the service-success fixture now carries the required attributes); plus a unit table for the heuristic's spellings. - live regression leg: `e2e_vendored_production.rs`'s gem leg is upgraded to `gem_bundler_vendored_install_proof` — a full fresh-dir frozen `bundle install` delivery proof with the failure tolerance and the `SOCKET_PATCH_VENDORED_E2E_GEM_STRICT` knob deleted. It passes against real production TODAY via the auto fallback (the served stub is still invalid), and exercises the service artifact directly once the depscan fix deploys and the artifacts rebuild. The leg installs in bundler's deployment layout (`vendor/bundle` inside the project) so the crawler-visible install can feed the local-build fallback its stub gemspec. Verified live against production: scan --mode vendored applied=1 via the fallback with the `vendor_prebuilt_stub_invalid` event, vendored gemspec carries real summary/authors; --vendor-source service refuses with the new code and leaves no .socket/vendor. Stacked on #217 (test/gem-e2e-restore). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(vendor): review round — gem-home guard, empirical rubygems bar, both-arm stub validation, truthful dead-ends, on-disk heal Adversarial-review fix round for the D4 stub-hardening PR. SECURITY (1): the local-stub derivation walked two parents up from installed_dir unconditionally. For a registry auto-fetch staging dir (<private tempdir>/<name>-<version>) that escapes into the SHARED temp root, making $TMPDIR/specifications/<leaf>.gemspec a predictable, attacker-plantable path whose contents would be committed and eval'd as Ruby by every later `bundle install`. The derivation now requires installed_dir's parent to be a literal `gems/` dir (a real gem-home layout); staging dirs have no local stub. Test plants a valid spec at the old derivation target and proves it is never consumed. Scanner rewrite (2): comment-stripping at `#` truncated inside string literals (s.summary = "#1 Ruby web server" judged missing → valid stubs refused under service). The scanner now examines raw lines anchored to the line start (receiver ident + .attr + assignment), where a preceding comment marker is impossible. The emptiness policy is now EMPIRICAL, verified against rubygems 3.3/3.5/3.6 in the bundler 1.17/2.7/4.0 era images: summary hard-fails only when never assigned (nil/"" are writer-coerced, warning at most); authors hard-fails when never assigned or collapsing to no String elements ([], nil, [nil], %w[] — while [""] passes). The old code refused rubygems-tolerated stubs and passed %w[] which hard-fails. Both-arm validation (3): the local-build arm wrote the local stub verbatim; it now validates at the same write choke point and refuses `gem_spec_invalid` naming the file (new CLI_CONTRACT vocabulary row). The GEMSPEC/GEMSPEC_318/GEMSPEC_PUMA and CLI-suite fixtures now carry summary+authors like every healthy rubygems-written stub. Truthful dead-end (4): auto + invalid served stub + gem not installed used to refuse gem_spec_missing with circular advice ("use --vendor-source=service" <-> service says "use auto") and the D4 diagnostic never reached the envelope (Refused carries no warnings). The FallBack variant now carries the served-stub defect; the refusal is `vendor_prebuilt_stub_invalid`, names the defect, and advises installing the gem. Tests cover auto and service, both not-installed. Heal existing victims (5): the idempotent hot path only checked the vendored gemspec EXISTS, silently re-blessing pre-fix invalid stubs. copy_ok now re-validates the on-disk stub; invalid routes into the existing artifact-only rebuild (test: pre-seeded invalid stub on disk → re-scan rewrites a valid one, pair edit + ledger untouched). Dedupe (11 + addendum B): one shared attr_mention line-scanner under both gemspec_declares_extensions and the attr checks; the miss closure widened with the (hard code, remedy) pair instead of a re-implemented branch; licenses/license alias fan-out folded into the alias-list helper; stub_text bound once. e2e leg robustness (6-10): bundler invocations scrub ambient BUNDLE_*/GEM_*/RUBYOPT (sibling-suite pattern) and set USE_FREEDESKTOP_PLACEHOLDER=true (mimemagic shared-mime-info hazard, mirrors #217 round 2); delivery proof asserts canonicalized starts_with(fresh-dir) provenance and compares installed bytes against captured pristine bytes; a route-attribution assertion requires exactly one of {vendor_prebuilt_downloaded, vendor_prebuilt_stub_invalid} so the leg auto-retires the fallback expectation when the depscan stub fix deploys; applied/failed/idempotency/revert assertions are scoped to the activestorage purl so future catalog additions cannot red the leg; stale doc comments fixed. Contract (addendum A): documented why the service-mode refusal on an invalid stub rides a MINOR — the prior exit-0 wrote a stub bundler rejects (an uninstallable project); the refusal is the bug fix. Rebased on test/gem-e2e-restore @ 63531d9 (PR #217 review round 2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ed7e700 commit 5339111

6 files changed

Lines changed: 1086 additions & 161 deletions

File tree

crates/socket-patch-cli/CLI_CONTRACT.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ per service outcome:
397397
| integrity mismatch | cargo/maven/nuget: **refuse** (`vendor_prebuilt_integrity_mismatch`) — tampered bytes never fall back; other ecosystems (to be aligned): local build + `vendor_prebuilt_integrity_mismatch` | refuse (cargo/maven/nuget: `vendor_prebuilt_integrity_mismatch`; others: `vendor_prebuilt_required`) |
398398
| still building (`pending_build` / serve 408) | local build + `vendor_prebuilt_pending` | refuse |
399399
| not built / withdrawn / not found / no usable artifact | local build (quiet) | refuse |
400+
| gem stub gemspec missing / invalid | local build + `vendor_prebuilt_stub_missing` / `vendor_prebuilt_stub_invalid` (invalid + gem not installed: refuse `vendor_prebuilt_stub_invalid` — no stub source exists) | refuse (`vendor_prebuilt_required` / `vendor_prebuilt_stub_invalid`) |
400401
| 401 / 403 grant / 5xx / network error | local build + `vendor_prebuilt_unavailable` | refuse |
401402
| `--offline` | local build | refuse (`vendor_service_offline_conflict`) |
402403

@@ -416,7 +417,24 @@ needs an eval-able stub gemspec that the `.gem` archive doesn't carry in bundler
416417
alongside the `.gem`, and the gem backend downloads + integrity-verifies both. A served gem whose
417418
stub is missing (a native-extension gem, for which the converter emits no stub, or a patch built
418419
before the stub rollout) is treated as a service miss — `auto` falls back to the local build,
419-
`service` refuses (`vendor_prebuilt_required`). For any ecosystem with no service path at all
420+
`service` refuses (`vendor_prebuilt_required`). A served stub that is present but INVALID — it
421+
fails the rubygems `summary`/`authors` bar, so every bundler major would reject the vendored
422+
path source at install time (a defect the 2026-08-19 live matrix found in every then-published
423+
gem stub) — follows the same miss policy under its own code (additive/MINOR): `auto` falls back
424+
to the local build with a loud `vendor_prebuilt_stub_invalid` warning naming the missing
425+
attributes, `service` refuses with `vendor_prebuilt_stub_invalid`. (Semver note: before the
426+
hardening, `service` mode exited 0 here while writing a stub bundler rejects — an UNINSTALLABLE
427+
project. The refusal is the bug fix; the exit-0 was the defect, so this rides a MINOR.) When the
428+
invalid-stub fallback finds the gem is ALSO not installed locally (no `specifications/` stub to
429+
derive), the vendor refuses with the same `vendor_prebuilt_stub_invalid` code, naming the served
430+
defect and the install-the-gem remedy. The locally-derived stub is validated at the same write
431+
choke point: a corrupted local `specifications/` stub failing the bar refuses with
432+
`gem_spec_invalid` naming the file. The bar is a conservative textual heuristic matched to what
433+
rubygems 3.3–3.6 actually hard-fails (no assignment of `summary`; no `authors`/`author`
434+
assignment, or one that collapses to no String elements — `[]`/`nil`/`[nil]`/`%w[]`; nil/empty
435+
strings are rubygems-tolerated and pass); a valid stub is still written byte-verbatim, and the
436+
idempotent re-vendor path re-checks the ON-DISK stub, routing a pre-hardening invalid one into
437+
the artifact rebuild. For any ecosystem with no service path at all
420438
`auto`/`build` build locally as before, and `service` refuses with
421439
`vendor_service_unsupported_ecosystem`. A successful service vend emits `vendor_prebuilt_downloaded`.
422440
Unrelated to `--download-mode` (which selects the patch-CONTENT format for the local build).
@@ -894,6 +912,8 @@ Every `--json` invocation emits a single JSON object that follows the **unified
894912
| `vendor_lock_checksums_unsupported` / `vendor_stale_lock_checksum` | `failed` | vendor (gem): an ambiguous/platform CHECKSUMS entry, or a v1-wired lock whose stale token blocks the hot path (run `vendor --revert` + re-vendor). |
895913
| `redirect_gem_stale_install` | `redirect.warnings[]` (warning) | scan `--mode hosted` (gem): a stale UNPATCHED materialization (installed gem, or committed `vendor/cache` archive) that `bundle install` will reuse instead of fetching the redirected patch; the detail carries the verified remedy. Full rules and flavors: the "Gem stale-install guard" section. |
896914
| `pypi_{poetry,pdm,pipenv}_no_lockfile` | `failed` | vendor (pypi): a lock-less tool marker with no `requirements.txt` fallback — run `<tool> lock`. |
915+
| `vendor_prebuilt_stub_invalid` | `failed` / `skipped` (warning) | vendor (gem, `--vendor-source`): the served stub gemspec fails the rubygems `summary`/`authors` bar, so bundler would refuse the vendored path source at install time. `service`: refusal naming the missing attributes; `auto`: loud warning + local-build fallback — or, when the gem is also not installed locally (no stub to derive), a refusal naming the served defect and the install-the-gem remedy. |
916+
| `gem_spec_invalid` | `failed` | vendor (gem): the LOCAL `specifications/` stub gemspec fails the same rubygems `summary`/`authors` bar (a corrupted or hand-edited gem home); the refusal names the file — reinstall the gem (`gem pristine <name>` / fresh `bundle install`). |
897917
| `vendor_*` / `pypi_*` / `gemfile_*` / `lock_*` / `locked_version_mismatch` / `user_authored_*` / `native_extensions_unsupported` / `platform_gem_unsupported` | `failed`/`skipped` | vendor: per-ecosystem refusal + drift vocabulary; see the Vendor command contract section. New tags are additive (MINOR). |
898918

899919
### Top-level `EnvelopeError` codes

0 commit comments

Comments
 (0)