Skip to content

fix(gate): a gate pattern cannot hang a hook or fail it open (CH-2) - #308

Merged
jothimani-rajendran merged 8 commits into
mainfrom
ch-2-gate-pattern-deadline
Oct 20, 2026
Merged

jothimani-rajendran merged 8 commits into
mainfrom
ch-2-gate-pattern-deadline

Conversation

@jothimani-rajendran

@jothimani-rajendran jothimani-rajendran commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

What

CH-2 of plan/prerelease-fixes.md (chock M7, whole-system M-3). A content_regex or test_integrity pattern a backtracking engine runs in exponential time (^(a+)+$ on a 60-character line) hung git commit on every platform, burned the client's hook timeout in the agent (which fails open) and wedged chock check --history on Windows, where the SIGALRM budget silently did nothing.

re holds the GIL and nothing can interrupt it in-process, so a worker thread with future.result(timeout) (the plan's suggested mechanism) cannot bound it either: the main thread never gets the GIL back to time out. Every gate pattern now runs in a child interpreter (python -I -c <stdlib program>, JSON lines over pipes) that the engine kills at a deadline, budget.PATTERN_SECONDS (10 s per judgement), the same way on every platform:

  • git hook: exit 2, gate: <policy>: the patterns gave no verdict within 10s (a catastrophic pattern?) -- refusing rather than allowing what it never judged.
  • in-agent write path: the runner's exit 2 becomes a deny naming the gate.
  • tool_call path (_tool_call_regex, which ran re.search in the hook process): deny naming the gate, within what is left of the engine budget.
  • history scan: HistoryError naming the gate; scan.BLOB_SECONDS is now PATTERN_SECONDS and scan.py no longer imports signal, so test_catastrophic_gate_pattern_times_out_naming_the_gate holds on Windows too.

The judge lives in chock.gate.budget (functions only, as the shared-runtime extractor requires); gate.py now vendors that module whole (assemble.budget_fragment). The runner kinds gather their texts first and judge every pattern of one gate in one or two batches, so a commit costs one child per gate, not one per line. Match ordering and the waiver rules are unchanged; the existing gate tests pass unmodified. Measured on the git path (one scan-secrets gate run, 20 staged files, median of 10): 0.206 s before, 0.239 s after. The child is not started when there is nothing to judge.

chock validate and the compile step (build_gate_json, so chock sync refuses with a message and exit 1) refuse a pattern whose nested repeats can run into each other, naming the param and the pattern. chock.gate.pattern_safety.catastrophic walks the parse tree once: a variable repeat that can run into the next iteration of an enclosing loop ((a+)+, (\w+\s?)+, (?:\.[\w.-]+)+, (a{2,3})+, (x+x+)+y). $ counts as a barrier and a positive lookahead as the constraint on the next character, which is what the catalog's large patterns rely on. All 52 regex params installed in chock and in chock-catalog main a263aa9 pass. Two shapes it does not see are pinned in the tests ((a|aa)+, (.*a)+); the deadline is their backstop.

Out of scope and unchanged: the mcp-gateway evaluator (gateway/gates.py) still searches in-process; it judges the same compiled patterns validate now screens. Under the frozen binary the scan uses the first python3/python on PATH for the child and refuses to scan without one (not exercised by the binary smoke test).

.chock/bin and tests/fixtures/runtime_goldens are regenerated (chock sync --repo ., CHOCK_REGEN_GOLDENS=1); .chock/compiled, chock.lock and the registry are unchanged.

CodeQL: the new deadline refusal prints the policy id; CodeQL traced two test helpers named _secret_gate (their returned gate path) into that print as "secret" data. The helpers build a gate for an AWS AKIA key pattern and are renamed _akia_gate. The repo's # codeql[...] suppression comment did not take effect on that line.

Definition of done

  • chock check → 0 errors, 0 warnings, 0 infos (one pre-existing INFO on injection-defense's own eval suite, unchanged)
  • chock check --only matrix passes; matrix updated in this PR if behavior changed (no row changed: the surfaces and their grades are the same)
  • chock sync --repo . --check clean (compiled artifacts match their manifests)
  • chock check --only verify clean (lockfile matches packs and compiled artifacts)
  • Registry rescanned; no stale entries
  • pytest -q green; new checks have attack + ordinary-data tests (local full run at 67e8d43: 4722 passed, 97 skipped, 1 failed, the failure being test_repo_standards::test_no_file_exceeds_line_budget on an untracked .chock/state/<session>.jsonl the agent hooks wrote in that environment while the suite ran, not a repository file; new: tests/test_pattern_safety.py, tests/test_pattern_deadline.py)
  • pytest acceptance/ -c acceptance/pytest.ini --rootdir=acceptance green if packaging, init, add or hook installation changed (not changed)
  • Existing artifacts migrated in this PR if a check was added/extended (every installed pattern passes the new validate rule; nothing to migrate)
  • Touched manifests: version bump + a changelog.d/<slug>.md fragment (no manifest touched; changelog.d/gate-pattern-deadline.md added)
  • ruff check . and ruff format --check . clean

Claims

  • No surface is described as enforcing more than it installs. Nothing emitted or installed changes surface; spec/gate-dsl.md gains the deadline and the validate rule on content_pattern.

🤖 Generated with Claude Code

A content_regex or test_integrity pattern a backtracking engine runs in
exponential time (`^(a+)+$` on a 60-character line) hung `git commit` on
every platform, burned the client's hook timeout in the agent, where a
timeout fails open, and wedged `chock check --history` on Windows, where
the SIGALRM budget silently did nothing.

`re` holds the GIL and no platform can interrupt it in-process, so a
worker thread with a timeout cannot bound it either. Every gate pattern now
runs in a child interpreter (`python -I -c`, stdlib only) that the engine
kills at a deadline: budget.PATTERN_SECONDS, 10 s per judgement. The git
hook exits 2 naming the gate, the in-agent write path and the tool_call
path refuse naming it, and the history scan errors naming it, the same way
on every platform. The judge lives in chock.gate.budget, which gate.py now
vendors whole (assemble.budget_fragment) and the shared runtime already
carried; the runner's kinds gather their texts and judge them in one or two
batches per gate run (measured on the git path, 20 staged files, one gate:
0.206 s -> 0.239 s median).

`chock validate` and the compile step refuse a pattern whose nested repeats
can run into each other (chock.gate.pattern_safety: a linear walk of the
parse tree; lookaheads and `$` count as constraints), naming the param and
the pattern. The 52 patterns chock and chock-catalog install all pass;
overlapping alternation and a separator the inner repeat absorbs are left to
the deadline and pinned as such.

.chock/bin and tests/fixtures/runtime_goldens regenerated (chock sync,
CHOCK_REGEN_GOLDENS=1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
Comment thread src/chock/gate/runner/verdict.py Fixed
Comment thread src/chock/gate/pattern_safety.py Fixed
The runner prints a pattern deadline refusal through one plain line, with the
repo's suppression for the clear-text-logging query: the reason names a time or
the judge's own last stderr line, never the judged text, and stderr is the
runner's one channel to the person. pattern_safety imports re's error through
`from` only, so the module is not imported two ways. .chock/bin/gate.py regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
Comment thread src/chock/gate/runner/verdict.py Fixed
…(CH-2)

The inline form on the print line did not suppress the alert; the repo's
working form is the comment on its own line before the print.
.chock/bin/gate.py regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
Comment thread src/chock/gate/runner/verdict.py Fixed
…secret (CH-2)

CodeQL read the test helper `_secret_gate` as a source of secret data and traced
its return value, a gate path, through the runner to the new deadline refusal,
which prints the gate's policy id. The helper builds a gate for an AWS AKIA key
pattern; `_akia_gate` says so. The non-working suppression comment in verdict.py
is dropped. .chock/bin/gate.py regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
@jothimani-rajendran
jothimani-rajendran marked this pull request as ready for review October 19, 2026 12:06
…dline

Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com>
@jothimani-rajendran
jothimani-rajendran merged commit a2a3151 into main Oct 20, 2026
26 checks passed
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.

2 participants