Skip to content

fix(registry): actually read the plugin pins and fail on unreachable or unhashed pins - #124

Merged
Xuepoo merged 3 commits into
mainfrom
audit/registry-pins
Nov 6, 2026
Merged

Xuepoo merged 3 commits into
mainfrom
audit/registry-pins

Conversation

@Xuepoo

@Xuepoo Xuepoo commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Why

The pin-reachability check was a no-op: readSubmodulePins() ran git ls-tree HEAD plugins, which returns the tree entry (040000 tree ... plugins), not the 11 gitlinks. Every official entry logged "no recorded pin; skipping reachability", so just registry-validate exited 0 even if a plugin was pinned to an unreachable commit or the manifest at the pin did not match the registry hash.

Proof:

$ git ls-tree HEAD plugins
040000 tree a156278...    plugins
$ git ls-tree HEAD plugins/
160000 commit 36b1b31...  plugins/activity
...

Fix

  • readSubmodulePins() reads git ls-tree HEAD plugins/ and is exported/testable.
  • planPinChecks():
    • a declared submodule with no recorded pin is an error (was a silently skipped warning);
    • a shipping pin without manifest_hash is an error;
    • a non-sha256:<64 lowercase hex> digest is an error in both modes, including --skip-network;
    • a named NON_SHIPPING_PIN_EXEMPTIONS entry is a notice with its reason, but only when the entry has no submodule and no recorded gitlink; an exempt name that still ships is an error;
    • a recorded pin requests reachability plus manifest-hash binding.
  • checkManifestHashes() verifies sha256:<64 hex> against the manifest at the pin (local object first, pinned raw.githubusercontent.com fallback); a mismatch or 404 is an error.
  • Both manifest sources are bounded to 256 KiB: the raw fetch uses a Content-Length pre-check plus a streaming cap, and the local git object is size-checked with cat-file -s before git show. An oversized manifest is an error, and a rejecting cancel() cannot mask that error.
  • Local git spawn failures (missing checkout, missing git binary, timeout) fall back to the raw URL instead of throwing, and readPinnedManifest() keeps local failures out of the "network unavailable" classification.
  • git ls-tree failure now fails closed.
  • --skip-network still enforces pin presence, exemptions, and the sha256:<64 hex> shape; only the network phases are skipped.
  • checkSubmoduleConsistency() honors the same exemptions, so the two gates cannot contradict each other.
  • Canonical integrity documentation now distinguishes scope: official shipping pins require and verify the digest at the pin; community entries may omit manifest_hash; signatures remain optional and advisory. registry/schema.json, the README, and all 11 registry/official/*.toml headers were updated, and the schema regex is ^sha256:[0-9a-f]{64}$.

Before / after

before: 0 error(s), 23 warning(s)   (11 × "no recorded pin; skipping reachability")
after:  0 error(s), 12 warning(s)   (0 pin-skip; 11 signature advisory + 1 aggregate)

Tests

tests/registry.test.ts: 11-gitlink fixture, missing-pin failure for a shipping plugin, unreachable (diverged) pin failure, exemption with/without reason and exemption-vs-shipping conflict, missing/non-sha256 manifest_hash offline, manifest hash match/mismatch/missing, oversized local and raw manifests, rejecting stream cancel(), local git spawn failures, local-first/raw fallback, and --skip-network pin planning with a derived count.

186 pass, 0 fail across both suites (142 registry tests); tsc exit 0.

just check passes in the worktree (fmt, lint, type-check, tests, validator, index freshness, app build). Validator result: 0 error(s), 11 reachability checks and 11 manifest bindings planned. Remaining warnings are the 11 signature advisories plus environmental skips: the audit worktree has no submodules checked out, and the unauthenticated GitHub compare API returns 403 once its hourly quota is exhausted.

Honour a non-shipping exemption only when the entry has no submodule or
recorded gitlink; an exempt name that still ships is now a hard error instead
of skipping reachability and manifest verification. Require a sha256
manifest_hash for every shipping official pin.

Bound both manifest sources to 256 KiB: the raw fetch uses the existing
Content-Length/streaming cap, and local git objects are size-checked with
cat-file before git show. Oversized manifests are errors.

Wrap local git spawns in try/catch so a missing checkout or git binary returns
the local fallback path instead of throwing; readPinnedManifest keeps local
failures out of the network-error classification. Add regression tests for
the exemption conflict, oversized local and raw manifests, spawn failures,
local-first/raw fallback, and --skip-network pin planning.
Enforce the official `sha256:<64 lowercase hex>` manifest_hash shape in
planPinChecks, so --skip-network rejects a wrong algorithm or digest length
instead of deferring it to the online phase. Tighten the shared integrity
pattern and schema to the same notation.

Update the canonical integrity claims: official shipping pins require and
verify the digest at the pin, while community entries may omit it and
signatures stay advisory. Refresh the 11 official entry headers and the
README/schema descriptions accordingly.

Keep the oversized-manifest error authoritative when Response.cancel() or
ReadableStreamDefaultReader.cancel() rejects, and derive the --skip-network
pin count in the regression test instead of hardcoding 11.
@Xuepoo
Xuepoo merged commit 14c2b7a into main Nov 6, 2026
6 checks passed
@Xuepoo
Xuepoo deleted the audit/registry-pins branch November 6, 2026 05:01
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