|
1 | 1 | # P-037 Phase B0: the formal/production proof-boundary audit (pre-registered) |
2 | 2 |
|
3 | | -> Status: **PRE-REGISTERED. No manifest, checker, or audit code written.** |
4 | | -> §§A–D are committed before any audit artifact, so the result is judged |
5 | | -> against them rather than an impression afterwards. Governance: |
| 3 | +> Status: **RESULT: PASS — PROOF BOUNDARY CLOSED FOR PHASE B SHADOW (§E), |
| 4 | +> with 16 B1 entry obligations and 2 phase-C obligations. One phase-C item |
| 5 | +> (A14, static dispatch) needs an owner decision before phase C.** |
| 6 | +> §§A–D were pre-registered in `ee73dd1`, before any audit artifact, and are |
| 7 | +> left as registered; the result is judged against them rather than an |
| 8 | +> impression afterwards. Governance: |
6 | 9 | > `docs/notes/p037-formal-kernel.md` §10.1 (finding classes), §10.4 (the |
7 | 10 | > Phase-B proof boundary), §10.6 (the R ruling). Base: A2.2 closed on #371 at |
8 | 11 | > `0ae821356ae03cd185e845cc46be45b700f1e064`. Branch: |
@@ -185,3 +188,169 @@ under KILL 9. |
185 | 188 | **Budget.** One small checker (`scripts/p037_proof_boundary.py`), one manifest |
186 | 189 | (`formal/p037-kernel/proof-boundary.json`), this note, small selftests and |
187 | 190 | mutants. A new general verification framework is a STOP. |
| 191 | + |
| 192 | +## E. Result (MEASURED OBSERVATION unless tagged) |
| 193 | + |
| 194 | +```text |
| 195 | +PHASE: B0 proof-boundary audit |
| 196 | +RESULT: PASS — PROOF BOUNDARY CLOSED FOR PHASE B SHADOW |
| 197 | +SEMANTIC WIRING: NONE |
| 198 | +HARNESS INVENTORY: 23 source / 15 fast + 8 heavy / 0 missing / 0 extra / 0 overlap |
| 199 | +UNCLASSIFIED ASSUMPTIONS: 0 (16 load-bearing: WF, EWF, A1–A12, A13–A16) |
| 200 | +#368: C_BLOCKER — no production file may call a schedule-taking kernel API |
| 201 | + (executable rule); #368 stays open, a blocker before any such use |
| 202 | +R: FAIL-CLOSED — NO_GUARDED_EVIDENCE, record-absence-boundary pinned |
| 203 | +``` |
| 204 | + |
| 205 | +The artifacts: |
| 206 | +- `formal/p037-kernel/proof-boundary.json`, the manifest; |
| 207 | +- `scripts/p037_proof_boundary.py` (`--check`, `--selftest`, `--mutants`); |
| 208 | +- `tests/test_p037_proof_boundary.py` (`--check` and `--selftest` on every |
| 209 | + `tests/run_tests.py`); |
| 210 | +- a `--mutants` step in the `formal-p037` CI job; |
| 211 | +- the #368 witness `issue_368_unfair_schedule_returns_a_non_fixpoint` (a |
| 212 | + kernel test; no kernel code changed); |
| 213 | +- the probe evidence under `docs/evidence/p037-b0-probes/`. |
| 214 | + |
| 215 | +### E.1 Necessary conditions |
| 216 | + |
| 217 | +| # | result | |
| 218 | +|---|---| |
| 219 | +| N1 | holds. All three sets are derived from source: 23 `#[kani::proof]`, 15 in `ci.yml` `formal-p037`, 8 in `formal-p037-gate.yml` `heavy-kani`. Each loop really runs `cargo kani --harness "$h"`. The README's hand-kept "22 harnesses" was stale (k11b arrived in A0.5) and is replaced by a pointer to the derivation | |
| 220 | +| N2 | holds. Every harness has a claim, concrete kernel subjects (each checked to be a `fn` in `lib.rs`), its assumptions and its CI class | |
| 221 | +| N3 | holds. Every assumption has exactly one class. `PRODUCTION_GUARANTOR` is used only where the mechanism exists **today**: A1, A2, A5, A10 are producer invariants pinned by census shapes; A7 and A8 are production-tree checker rules. Every kernel-only witness is labelled `seam: kernel`, and the checker refuses to relabel one as production | |
| 222 | +| N4 | holds. `MAX_COORDS = 3` / `MAX_EDGES = 2` are checked against `lib.rs`. Production is `dynamic`, and `kani_proves_production_bound: false`. The Rocq results are named as external evidence only | |
| 223 | +| N5 | holds, as classified. Election shape/import (EWF, A2, A5), Uncond diagonal (A3), id/neg (A2), G-V4 (A1), cell-local release (A4), fin before apply (A8, A13), full visitation (A7) and R (A11) each have a guarantor or an OUTSIDE entry with a B1 obligation | |
| 224 | +| N6 | holds. `apply` and `lower` finalize by construction. F5a/F5b (removing either `fin`) are killed by the `k7_` twins (`--mutants`). The production-facing rule F9 fails the audit if a production file that imports the kernel lowers or collapses cells outside `apply` | |
| 225 | +| N7 | holds with **option C**. Measured: no production file references the kernel at all, so none calls `solve_with` / `elect_with` / `lfp_chaotic`, and rule F8 makes that executable. The #368 witness shows `Some(non-fixpoint)` for both the solver and the election. The dynamic driver must be a full sweep with no schedule parameter (A7). **#368 stays open** as a defect of the generic API and is a mandatory blocker before that API is used in production | |
| 226 | +| N8 | holds. A11 is `NO_GUARDED_EVIDENCE`, with eight forbidden positive readings. `record-absence-boundary` is its control, and F6 (reading absence as `borrow`) fails the audit | |
| 227 | + |
| 228 | +### E.2 Falsifiers |
| 229 | + |
| 230 | +| id | outcome | |
| 231 | +|---|---| |
| 232 | +| F1 | fires: a dropped harness gives `missing`, a fake name gives `extra`, and a loop that stops running `cargo kani` fails | |
| 233 | +| F2 | fires: `overlap` | |
| 234 | +| F3 | fires: no class, and a vague guarantor ("the frontend handles this") | |
| 235 | +| F4 | fires: a nonexistent guarantor path, and a nonexistent control symbol | |
| 236 | +| F5 | fires (`--mutants`): apply-without-fin is killed by `k7_unknown_opaque_and_differing_unselected_cells_never_consume` and `k7_witness_an_unfinalized_opaque_read_would_consume`; lower-without-fin is killed by the first | |
| 237 | +| F6 | fires: absence read as `borrow` | |
| 238 | +| F7 | fires: fairness "left to the caller" fails, and the #368 witness holds on the real kernel | |
| 239 | +| F8 | fires: a kernel-importing production file calling `solve_with`. **A same-named unrelated `solve_with` stays green** (see E.3 item 5) | |
| 240 | +| F9 | fires: a kernel-importing production file that runs `lower(c.collapse())` | |
| 241 | +| F10 | fires: `k10a` stops declaring `WF` although its body calls `any_system` | |
| 242 | + |
| 243 | +The selftest also covers: A8 with apply ordered before fin; a claimed |
| 244 | +production-size bound; a formal-crate witness relabelled as production; and |
| 245 | +a recorded blocker, which fails the gate. |
| 246 | + |
| 247 | +### E.3 Findings, classified under §10.1 |
| 248 | + |
| 249 | +1. **A13: cross-SCC export must be finalized (case 2).** |
| 250 | + - The kernel models one SCC; composition across SCCs is not modelled. |
| 251 | + - The F2 hazard exists at the SCC boundary too: through an Opaque edge a |
| 252 | + raw `(must, ⊥)` collapses to `must`, while `(must, no)` collapses to |
| 253 | + `may`. |
| 254 | + - The legacy driver already finalizes before export (`mos.rs`, |
| 255 | + `unwrap_or(Transfer::No)`). The guarded driver must do the same; this is |
| 256 | + a B1 obligation. |
| 257 | +2. **A14: static dispatch (case 2, inherited; phase-C obligation). This is |
| 258 | + the finding closest to KILL 9.** |
| 259 | + - The kernel reads the summary of the statically resolved callee. The |
| 260 | + sidecar has no dispatch fact, so the assumption cannot be discharged |
| 261 | + fail-closed from the A2 facts. |
| 262 | + - Why it is not a B0 kill: |
| 263 | + 1. it is not new to P-037: legacy `ConsumesParam` and the MOS keying make |
| 264 | + the identical assumption in today's verdicts; |
| 265 | + 2. B1 moves no verdict; |
| 266 | + 3. `INFERENCE`: while `ConsumesParam` folds any-path disposal into a |
| 267 | + release, the guarded consume set through a virtual call stays within |
| 268 | + the legacy end-to-end one. |
| 269 | + - It becomes live once A1 removes that fold. **Owner decision needed before |
| 270 | + phase C**: either a dispatch fact (a case-5 OwnIR amendment), or a ruling |
| 271 | + that accepts the inherited assumption. B1's shadow report must state that |
| 272 | + its refinement counts are conditional on static dispatch. |
| 273 | +3. **A15: the G-S4 masks and G-S2/G-S3 seeds come from a body⋈sidecar join |
| 274 | + (case 2).** |
| 275 | + - The pre-registration's candidate hidden precondition was that forward |
| 276 | + placement cannot be recovered from the A2 facts. It was **refuted**: |
| 277 | + legacy lowering puts a `use` / `release` op in the body tree for a |
| 278 | + relevant call, and the sidecar's `statement_line` joins to it. |
| 279 | + - What remains is a join whose ambiguity is detectable in every probed |
| 280 | + case. That gives the fail-closed rule recorded in A15. |
| 281 | + - `INFERENCE`: this is a probe-based argument, not a proof. If B1 cannot |
| 282 | + make the join provably fail-closed, B1 must STOP and ask for a placement |
| 283 | + fact (case 5). |
| 284 | +4. **A16: normal-return-only semantics (case 2, inherited).** |
| 285 | + - The legacy body drops catch blocks, so a release inside a catch is |
| 286 | + invisible to legacy and guarded alike. |
| 287 | + - This is P-036 vertical B, not widened by P-037. It is a phase-C |
| 288 | + classification note. |
| 289 | +5. **Instrument defect, fixed before the result.** |
| 290 | + - The first `--check` run flagged `rust/crates/own-analysis` for |
| 291 | + `solve_with`. That is the Rust core's own worklist dataflow solver over a |
| 292 | + `Schedule` enum, not the P-037 kernel. |
| 293 | + - The rule matched the name alone. It is now scoped to files that name the |
| 294 | + kernel crate, and the selftest pins that a same-named unrelated function |
| 295 | + stays green. |
| 296 | +6. **Doc drift.** `formal/p037-kernel/README.md` said "22 harnesses"; the |
| 297 | + source has 23. The count is now derived, never written. |
| 298 | + |
| 299 | +### E.4 Interpretations applied (for the owner to overrule) |
| 300 | + |
| 301 | +- **"Production guarantor" means a mechanism that exists today.** Production |
| 302 | + does not call the kernel yet. Assumptions whose only guarantor would be the |
| 303 | + future B1 adapter or driver are therefore `OUTSIDE_KANI_BOUNDARY`, each with |
| 304 | + a named `b1_obligation` and `derivable_from_a2_facts: true`. None of them is |
| 305 | + relabelled as a guarantor. |
| 306 | +- **KILL 5 ("pinned at the seam production will use").** That seam is the |
| 307 | + kernel's `apply` / `lower`: A1 moves the kernel functions rather than |
| 308 | + rewriting them (§8.1, acceptance item 6). It is pinned by F5 and by the |
| 309 | + production-tree rule F9. No guarded production seam exists yet; porting the |
| 310 | + K7 witness to it is a B1 obligation (A8). |
| 311 | + |
| 312 | +### E.5 B1 entry obligations (the manifest is normative; one line each) |
| 313 | + |
| 314 | +| id | obligation | |
| 315 | +|---|---| |
| 316 | +| WF / EWF | enforce the well-formedness predicates at construction; violation → `NO_GUARDED_EVIDENCE` | |
| 317 | +| A2 / A5 | the binding and transform table (Id/Neg only for a resolved callee at the elected ordinal; `call_result` Opaque in the solver) | |
| 318 | +| A3 | Uncond seeds only via `Cells::diag` | |
| 319 | +| A4 / A15 | seeds and masks only through the fail-closed join; the B0 probe shapes become B1 controls | |
| 320 | +| A6 | the dynamic driver composes the kernel's own `read` / `contribute` / `join` / `import` | |
| 321 | +| A7 | full sweep, no schedule parameter | |
| 322 | +| A8 | application only through `apply`; the K7 witness ported to the B1 seam | |
| 323 | +| A9 / A10 / A11 | absence, unresolved callees and ambiguous joins are `Unknown` / `NO_GUARDED_EVIDENCE`, never ⊥ and never positive | |
| 324 | +| A12 | pass bound `n·HEIGHT+1`; exceeding it → `NO_GUARDED_EVIDENCE` | |
| 325 | +| A13 | export only finalized cells across SCCs | |
| 326 | +| A14 | the shadow report is conditional on static dispatch | |
| 327 | + |
| 328 | +### E.6 B0 probe shapes (the A15 join, measured) |
| 329 | + |
| 330 | +Sources: `docs/evidence/p037-b0-probes/Probe{,2,3}.cs`. The extractor output |
| 331 | +(bodies and sidecars only) is in `observed.json`, taken at base `0ae8213` |
| 332 | +with `--flow-locals`. |
| 333 | + |
| 334 | +| probe | shape | observed | under the A15 rule | |
| 335 | +|---|---|---|---| |
| 336 | +| `Probe.Branchy` | statement forwards in then / else of a guard | `release@20` in then, `release@24` in else; one call per line | placeable | |
| 337 | +| `Probe.TwoIfs` | two eligible `if`s on one line | two `if@31` ops, no column; guards at columns 9 and 36 | guard↔`if` ambiguous → `NO_GUARDED_EVIDENCE` | |
| 338 | +| `Probe.Early` | `if (keep) return; Sink(s);` | `if@37 then:[return]`, `release@38` after | placeable (negative literal) | |
| 339 | +| `Probe2.InBranch` / `AfterIf` | a borrowing forward inside vs after a guarded `if` | `use@19` inside then vs `use@27` after the `if`; the facts differ | placeable: the indistinguishability hypothesis is **refuted** | |
| 340 | +| `Probe2.Behind` | a wrapper forward under an ineligible `if (ready)` | `if@33 then:[release@35]`, `guards: []` | placeable; the kept path must join `no` | |
| 341 | +| `Probe3.ShortCircuit` | `c && Ok(s)`, where `Ok` disposes | form `expression`; `if@17` with **empty** branches, so the consuming call has no body op | expression form → `NO_GUARDED_EVIDENCE` | |
| 342 | +| `Probe3.Ternary` | `c ? Use(s) : 0` | **no `functions[]` record** | R: `NO_GUARDED_EVIDENCE` | |
| 343 | +| `Probe3.Switch` | `case 1: Sink(s);` | lowered to `if@22 then:[release@24]` | placeable | |
| 344 | +| `Probe3.TryCatch` | `Sink(s)` inside a catch | form `statement`, but **no body op**; the body models only the try | no op → `NO_GUARDED_EVIDENCE`; a local release in a catch is invisible (A16) | |
| 345 | +| `Probe3.Loop` | a one-line `for` | `while@37` and `release@37` on one line | structural op on the line → `NO_GUARDED_EVIDENCE` (conservative) | |
| 346 | +| census `ctor-initializer` | `: base(s, keep)` | no body op for the initializer | no op → `NO_GUARDED_EVIDENCE` | |
| 347 | + |
| 348 | +### E.7 What B0 did not do |
| 349 | + |
| 350 | +- No semantic wiring. No change to guarded values, solver output, MOS, |
| 351 | + verdicts, `ConsumesParam`, Roslyn facts, the OwnIR schema or kernel |
| 352 | + semantics; the only kernel change is one new `#[test]`. |
| 353 | +- #368 not fixed. |
| 354 | +- No R value experiment. That measurement starts after the first real |
| 355 | + Phase-B shadow run (§10.6). |
| 356 | +- B1 not started. |
0 commit comments