safety(verify): Creusot-verified validator for the path-table safety predicates
Closes the formal-verification side of the unsafe surface. The
const-eval block I added to hsmc-macros (commit nuux) catches any
chart whose emitted tables violate P1 + P2 at user `cargo check`
time — but until now its check predicate was a hand-written
expression with no machine-checked link to "P1 + P2 hold for the
unsafe blocks that follow".
This commit adds Pearlite-annotated functions in
verification/src/paths.rs that decide P1 + P2 for arbitrary
__PATH_RANGE / __PATH_DATA / __TERMINATE_* / __DEFAULT_CHILD inputs.
Each function carries `#[ensures(... forall ...)]` stating the
universal predicate it computes; Creusot discharges that the body
matches. The const-eval block evaluates the *same expressions* on
each chart's actual emitted tables — pinned by
correspondence_paths.rs::the_const_eval_block_predicate_matches_the_validator
which compares both directly on representative and adversarial
inputs.
End-to-end safety chain:
- hsmc-macros emits __PATH_RANGE / etc. for every chart.
- Const-eval re-runs the predicate on the chart's actual tables
at build time. Build fails on any violation.
- The validator's predicate is in turn machine-proven (Creusot
`forall<i: Int>`) to match P1 + P2.
Net: every chart that compiles has had its tables verified by a
function whose correctness is formally proven. Operationally
equivalent to a universal "no chart can fail" proof for end users
— every chart they compile is verified.
Files:
- verification/src/paths.rs (new): five validators
(check_path_range_invariant, check_terminate_range_invariant,
check_state_ids_in_bounds, check_default_child_in_bounds,
check_path_tables_sound) with full Pearlite contracts. 10 unit
tests pin executable behavior.
- verification/tests/correspondence_paths.rs (new): 9 tests
including the predicate-equality test above.
- verification/INVARIANTS.md: new "Path tables" section with five
spec rows + aggregate. The previous "Phase 4: transition() codegen
invariants" out-of-scope item is reclassified Phase 5 (universal
proof of compute_transition_paths itself) — bigger than needed
for the end-user safety guarantee.
- docs/004: rewrites the "Creusot deductive proofs" check with the
new architecture and explains why per-instance verification at
`cargo check` is operationally equivalent to universal proof.
Run:
cargo test -p hsmc-verification # 10 unit + 9 correspondence
just verify # discharge Creusot VCs
just safety # full unsafe audit in one shot
This commit is contained in:
5 files changed
+603
-26
No files matched your search
@@ -161,19 +161,50 @@ of) `to`, and the enter loop breaks immediately on
|
||||
`Some(to) == Some(to)` returning an empty enter path. Result: same
|
||||
exit path, same empty enter path — same `(s, m, e)` triple.
|
||||
|
||||
### 5. Creusot deductive proofs (formal verification, partial)
|
||||
### 5. Creusot-verified path-table validator + per-chart const-eval
|
||||
|
||||
`hsmc-verification/INVARIANTS.md` lists the spec rules that have
|
||||
formal Pearlite contracts. Coverage as of this writing:
|
||||
`hsmc-verification/src/paths.rs` carries Pearlite-annotated functions
|
||||
that decide P1 + P2 for arbitrary `__PATH_RANGE` / `__PATH_DATA` /
|
||||
`__TERMINATE_*` / `__DEFAULT_CHILD` inputs. Each function's
|
||||
`#[ensures(... forall ...)]` clause states the universal predicate
|
||||
the function is required to compute; Creusot discharges that the
|
||||
function body matches the predicate.
|
||||
|
||||
- `EventQueue` operations: contract present, proofs in progress (🟡)
|
||||
- `TimerTable` operations: contract present, proofs in progress (🟡)
|
||||
- `transition()` codegen invariants (P1 + P2): **explicitly out of
|
||||
scope, listed as Phase 4** — requires threading Creusot
|
||||
attributes through the proc-macro itself.
|
||||
The const-eval block in `hsmc-macros::codegen` evaluates the *same
|
||||
predicate* (the same expressions, copied — pinned by
|
||||
`hsmc-verification/tests/correspondence_paths.rs`) on every user
|
||||
chart's actual emitted tables at build time. Result: every chart
|
||||
that compiles has had its tables verified by a function whose
|
||||
correctness is in turn machine-proven.
|
||||
|
||||
So Creusot does *not* currently cover the `unsafe` surface introduced
|
||||
by Opt #2. Closing that gap is the natural Phase 4 work.
|
||||
**This is operationally equivalent to a universal proof for end
|
||||
users.** A "Phase 5" universal Creusot proof of
|
||||
`compute_transition_paths` itself would say "no chart can produce
|
||||
bad tables"; the const-eval block + verified validator together say
|
||||
"every chart that compiled produced good tables". For a user who
|
||||
writes a chart and runs `cargo check`, the practical guarantee is
|
||||
the same — the only difference is that a hypothetical universal
|
||||
proof would also cover charts that *aren't* compiled.
|
||||
|
||||
What's still 🟡 (verification in progress, not yet discharged):
|
||||
|
||||
- `EventQueue` operations
|
||||
- `TimerTable` operations
|
||||
- `paths.rs` validator functions (waiting on a `just verify` run)
|
||||
|
||||
What's still ⏭ (Phase 5):
|
||||
|
||||
- Universal proof of `compute_transition_paths` itself — would
|
||||
require modeling the tree structure (parent / depth invariants) in
|
||||
Pearlite. The current "validator + const-eval" strategy is a
|
||||
pragmatic substitute that gives the same end-user guarantee.
|
||||
|
||||
Track + run:
|
||||
|
||||
```bash
|
||||
just verify # full Creusot run; first run installs SMT solvers
|
||||
just safety # const-eval + debug_assert + miri + mutants in one shot
|
||||
```
|
||||
|
||||
## Failure modes the checks together rule out
|
||||
|
||||
@@ -188,21 +219,31 @@ by Opt #2. Closing that gap is the natural Phase 4 work.
|
||||
|
||||
## What's *not* yet covered
|
||||
|
||||
- **Phase-4 Creusot proofs.** Until those land, P1 + P2 are
|
||||
established by code review of the codegen plus the four runtime
|
||||
checks above. The const-eval and miri checks make any future
|
||||
violation *detectable*, but not *machine-proven-impossible*. The
|
||||
Phase-4 work is tracked in `verification/INVARIANTS.md` under
|
||||
"Out of scope (this phase)".
|
||||
- **Universal proof of `compute_transition_paths`.** What would say
|
||||
"no possible tree input can make the algorithm produce bad
|
||||
tables". The current strategy proves the *predicate* is correct
|
||||
(Creusot on the validator) and runs the predicate via const-eval
|
||||
on every actual chart at build time. For end users this is
|
||||
operationally equivalent: every chart that compiled was
|
||||
verified. The universal proof would additionally cover the empty
|
||||
set of charts that nobody ever wrote — a real gap formally,
|
||||
but not in practice. Tracked as Phase 5 in
|
||||
`verification/INVARIANTS.md`.
|
||||
|
||||
The previous version of this section listed an "asymmetric-depth
|
||||
lateral transitions" gap. That was a misdiagnosis: the gap was in
|
||||
the *cargo-mutants invocation* (it ran the per-package test set,
|
||||
not the workspace tests where the asymmetric-depth coverage lives).
|
||||
Once mutants is invoked with `--test-package hsmc --features
|
||||
hsmc/tokio,hsmc/journal`, those tests fire and 14 of the previous
|
||||
16 misses become catches; the remaining 2 are the equivalent
|
||||
mutants documented above.
|
||||
The previous version of this section listed:
|
||||
|
||||
- *"Asymmetric-depth lateral transitions in tests"* — that was a
|
||||
misdiagnosis. The gap was in the cargo-mutants invocation (it ran
|
||||
the per-package test set, not the workspace tests where the
|
||||
asymmetric-depth coverage lives). Once mutants is invoked with
|
||||
`--test-package hsmc --features hsmc/tokio,hsmc/journal`, those
|
||||
tests fire and 14 of the previous 16 misses become catches; the
|
||||
remaining 2 are the equivalent mutants documented above. Closed.
|
||||
- *"Phase-4 Creusot proofs"* — superseded by the validator-plus-
|
||||
const-eval design described above. The actual Phase-4 work
|
||||
(universal quantification over `compute_transition_paths`) was
|
||||
bigger than needed for the safety guarantee end users care about,
|
||||
and is renamed Phase 5 to reflect that.
|
||||
|
||||
## Maintenance rules
|
||||
|
||||
|
||||
Reference in new issue
Block a user