Skip to content

fix(#288): wire trap_gate onto the runtime path (additive trap backstop)#289

Open
avrabe wants to merge 1 commit into
mainfrom
fix/288-wire-trap-gate
Open

fix(#288): wire trap_gate onto the runtime path (additive trap backstop)#289
avrabe wants to merge 1 commit into
mainfrom
fix/288-wire-trap-gate

Conversation

@avrabe

@avrabe avrabe commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

trap_gate (the systemic trap-equivalence gate, #279/#284) was dead code — its transform-checking entry points (check_div_transform, check_mem_transform, check_select_discard) had zero callers. Trap soundness therefore rested entirely on the per-fold static guards (#273/#274/#276/#278/#281); the gate provided zero runtime protection (#288, #279 false-closed on the belief it was the authority).

This PR wires the certificate-checked gate onto the constant_folding runtime path as an additive proof backstop, gated behind the verification feature.

What was wired

  • New loom_core::trap_backstop module. For every div/rem-on-constants fold that fires in constant_folding, ordeal must prove the op was trap-free (orig.may_trap ⇔ false, LRAT certificate re-checked) via the new trap_gate::check_div_discard. A non-accepted verdict reverts that function's fold.
  • Routed kinds: div_s, div_u, rem_u — ordeal's trap_div models their trap condition exactly (concrete constant operands ⇒ no symbolic slack).
  • The backstop runs after the fold, before the Z3 value validator — which cannot see traps over its total model, the exact reason the five miscompiles slipped through.

Additive-backstop design (no capability change)

Every existing static guard is kept untouched (the is_simple_pure_instr div/rem exclusion, the div_s(INT_MIN,-1) const-fold refusal in rewrite_pure, the vacuum may-trap-load keep, the select is_no_trap_expr guard). When a guard holds, no trapping fold fires and behavior is unchanged. The backstop only catches a trapping fold that slipped a guard. With the verification feature off, trap_backstop is compiled out entirely and behavior is byte-identical.

Documented remaining gaps (static guard stays sole authority)

Per the conservatism rule, sites that cannot be modeled soundly with the existing API + a small clearly-correct helper were left on their static guard and documented rather than modeled dubiously.

Anti-regression tests (the point of #288)

  • test_288_backstop_observes_and_accepts_safe_div_fold — the gate observes the 20/4 fold fire and accepts it.
  • test_288_backstop_blocks_trapping_div_fold — a constructed div_s(INT_MIN,-1) and div_u(x,0) fold is blocked by the gate with the static guard bypassed in-test, proving the gate (not the static guard) is the runtime authority.
  • test_288_constant_folding_pass_keeps_safe_fold — end-to-end, the pass with the backstop live still folds a trap-free const div (no over-blocking).
  • New check_div_discard unit tests: accept trap-free, reject overflow, reject ÷0, pin the rem_s over-approximation.

rivet

Adds TEST-TRAP-GATE-RUNTIME-PATH (verifies REQ-1/REQ-6, mitigates UCA-5/UCA-21, verifies CC-4/CC-20; release tag v122). rivet validatePASS.

Build / test results (macOS homebrew Z3, isolated target dir), honestly stated

  • cargo test --release -p loom-core (default = includes verification): test result: ok. 480 passed; 0 failed; 2 ignored (lib suite) — all green.
  • cargo test --release -p loom-core --features verification: test result: ok. 480 passed; 0 failed; 2 ignored — all green.
  • cargo test --release -p loom-core --no-default-features (backstop compiled out, proves byte-identical path compiles + passes): all green.
  • cargo clippy --release -p loom-core --features verification: clean; cargo fmt --all applied.

Do not merge — for user review.

Fixes #288
Refs #279

🤖 Generated with Claude Code

trap_gate (#279/#284) had ZERO callers — its transform-checking entry
points were dead code, so trap soundness rested entirely on the per-fold
STATIC guards (#273/#274/#276/#278/#281). This wires the certificate-checked
trap-equivalence gate onto the constant_folding runtime path as an ADDITIVE
proof BACKSTOP, gated behind the `verification` feature.

What is wired
- New `loom_core::trap_backstop` module: for every div/rem-on-constants fold
  that FIRES in constant_folding, ordeal must PROVE the op was trap-free
  (LRAT certificate re-checked) via the new `trap_gate::check_div_discard`.
  A non-accepted verdict reverts that function. Routed kinds: div_s, div_u,
  rem_u (ordeal models their trap condition exactly).
- constant_folding calls the backstop after the fold, before the Z3 value
  validator (which cannot see traps — the reason the miscompiles slipped).

Design: purely additive
- No static guard is relaxed. When a guard holds (no trapping fold fires),
  behavior is unchanged. The backstop only catches a trapping fold that
  slipped a guard. With `verification` off the module is compiled out and
  behavior is byte-identical.

Documented remaining gaps (static guard stays sole authority)
- rem_s: ordeal's trap_div over-approximates it (shares the INT_MIN/-1
  overflow disjunct with div_s, but WASM rem_s(INT_MIN,-1) does NOT trap).
  Routing it would falsely reject a safe fold. Pinned by a trap_gate test.
- mem-discard (#278): the vacuum load-drop fold never fires (is_pure_pusher
  excludes loads), so there is no live fold to back-check; check_mem_transform
  stays exercised by its own unit tests only.
- select-arm-discard (#281): the fold lives in loom-shared/rewrite_pure below
  the ordeal boundary; its discarded arm's trap condition is not reconstructed
  at the instruction layer. Its static is_no_trap_expr guard stays authority.

Anti-regression tests (#288's whole point)
- test_288_* assert the gate is INVOKED on the runtime path: a known-safe
  fold (20/4) is observed AND accepted; a constructed trapping fold
  (div_s(INT_MIN,-1), div_u(x,0)) is BLOCKED with the static guard bypassed
  in-test, proving the gate — not the static guard — is the runtime authority.
- New check_div_discard unit tests (accept trap-free, reject overflow, reject
  ÷0, pin the rem_s over-approximation).

rivet: adds TEST-TRAP-GATE-RUNTIME-PATH (verifies REQ-1/REQ-6, mitigates
UCA-5/UCA-21, verifies CC-4/CC-20; release tag v122). `rivet validate` PASS.

Builds (macOS homebrew Z3): default (verification) + explicit --features
verification + --no-default-features all green; clippy clean; fmt applied.

Fixes #288
Refs #279

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant