1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
|
# 50 — Wall workflow and contract registry (in development)
> **Status: in development (2026-10-06, wall-directed wave).** Documents the
> wall-hell escape workflow (observe → classify → model → falsify → contract),
> the six wall classes and which tool may be used for each, the capture-request
> discipline, and the contract registry (`harness/contracts.py`,
> `harness/run_contracts.py`) that turns every solved wall into a re-runnable
> regression.
>
> Development-only: the registry re-runs offline tooling; it never touches a
> GPU, kernel module, mdev, guest or network, and it adds no measurement
> capability of its own. Fidelity stays what each wrapped tool claims (L0/L1;
> `06-ACCURACY.md`).
>
> **See also:** `47-VALIDATION-PREDICATE-PROBE.md` (`valprobe`),
> `46-SURGICAL-REHOST.md` (`rehost`), `48-GMMU-WALK-AND-LAUNCH-AUDIT.md`
> (`gmmu_walk`, `launch_audit`), `45-RM-RPC-CAPTURE-AND-REPLAY.md`
> (`rm_capture`), `19-CONTROL-DISCOVERY.md`, `40-NEW-VERSION-TRIAGE.md`,
> `25-VALUE-VALIDATION.md`, `27-TOOL-AUDIT.md`, `49-SIMULATION-SURFACE-MAP.md`,
> the private simulator handoff
> [`../../../../handoff-simulator.md`](../../../../handoff-simulator.md)
> §30–33 (read it when available; agents without private access skip it), and
> `open-iov/docs/eng-ops.md` §2/§6 (admission and automation safety).
## 1. Why this exists: wall-hell has a shape
The GR campaign's dominant cost is **wall-hell**: one blocking problem after
another, each paying supervised hardware cycles (5–6 min each, plus fail-safe
discipline) to learn one fact. The toolset now solves several wall classes
offline and deterministically (`docs/25` §3 records the measured value), but
knowledge kept only as prose is re-learned by the next agent and re-tested on
silicon by the next campaign. This document fixes two things:
1. **A wall is classified before it is worked.** Each wall belongs to exactly
one of six classes; only the tool that measures that class may answer it.
Most expensive wall errors are *domain-crossing inferences* (section 3).
2. **A wall is not closed until it is a contract.** The answer becomes a
declarative, re-runnable check over the existing tool entry point
(section 6), so the next firmware bump or tool change is caught by
`make test` instead of another cycle.
The registry is a **consumer** of proven tools, not a new probing capability:
it wraps `valprobe`, `rehost`, `gmmu_walk`, `launch_audit` and `rm_capture`
entry points as-is (docs/49 §4 lists their proven value).
## 2. Wall taxonomy — six classes
| Class | The question it covers | Measures it DIRECTLY | Must NOT be used to infer it | Artifact a correct answer leaves |
|---|---|---|---|---|
| **a. firmware boot/status** | Did the firmware reach rung Bx? Where is it stuck, with what stop reason/status word? | `sim/gsp_boot.py` rungs B0–B3, `harness/triage.py` (identity/probe/native_boot), probe sessions, `greeneye probe` first-fault report | engine memory tools (`gmmu_walk`, `launch_audit`) and predicate probes (`valprobe`) | `greeneye-session/1` with the rung, stop PC/status and first-fault address |
| **b. RM server behaviour** | What does the server read/write/wait on for this input; which branch/gate decides accept vs refuse; what state does a handler need? | `valprobe` (one firmware predicate branch with crafted state), `rehost` (one whole function; first missing state is the finding), `rm_capture` decode for what the client actually sent | engine-execution claims from hooks/predicate verdicts; `gmmu_walk`/`launch_audit` for server logic | valprobe/rehost report + session: entry, seeded state, stubs, labelled outcome, stop/fault |
| **c. engine memory contract** | Can the engine reach each leg of this submission (ring, QMD destination, param/program, release slot, semaphore) with the required access, and at which PA? | `launch_audit` (per-leg verdict against the engine's table view), `gmmu_walk` (the leaf mapping behind one VA) | host shadow bookkeeping as proof of engine reachability; server-side hooks; `valprobe` | `greeneye-launch-audit/1` spec+report with per-leg `ok`/`pa_mismatch`/`unmapped`/`access_denied` |
| **d. engine execution/dispatch** | Did the front-end/engine consume the entry, dispatch the method, write the release/semaphore? | Supervised capture + post-mortem decode (the only authority); `launch_audit` answers only its *address* leg | anything in this tool: no GR/PBDMA model exists by design (`docs/49` §6); page tables cannot prove method dispatch; a silent ring cannot be inferred from an all-`ok` audit | capture with the engine-side consumption evidence (GPFIFO GET/PUT movement, method/QMD execution, release/semaphore state) + post-mortem |
| **e. guest-driver/protocol interface** | What did the guest driver send (fn/cmd/params), and does a replay/another version differ? | `rm_capture` decode/compare/stimulus (send-side logs), guest diagnostics (`open-iov/tools/guest-diag`) | firmware predicate/branch results as a substitute for the driver's wire traffic; `gsp_boot` rungs | `greeneye-rm-rpc-capture/1` + `greeneye-rm-rpc-compare/1` and a stimulus JSON |
| **f. version skew/portability** | Did the image/code/behaviour move between versions, and which change explains it? | `harness/triage.py` (full chain), `harness/version_diff.py` (sections + symbols + M1–M6 skeleton), `./risc-v-gsp-rm-debugger.sh probe <image>` first-touch diff | one version's green run as portability proof; a `skipped` step read as a pass | triage report + version diff + one recorded `compare` session pair (self-diff zero) |
Two rules fall out of the table:
* **Class (d) has no offline tool, by design.** Do not attempt to simulate it:
the simulator makes no L2/L3 claims (`docs/06`). The offline contribution is
the *address leg* (class c) and the *server leg* (class b).
* **A wall usually names its own class in one sentence.** If two classes seem
to fit (e.g. "the launch is silent"), split the question: address leg →
(c), method execution → (d), server prewrite → (b). One wall = one class =
one decidable question.
## 3. The evidence ladder and the no-cross-domain rule
### 3.1 Observe → classify → model → falsify → contract
| Stage | What happens | Exit test |
|---|---|---|
| **1. Observe** | Collect only evidence that can discriminate: one bounded session/probe/capture, named stimulus, recorded artifacts | The observed fact is replayable and has a session/report path |
| **2. Classify** | Assign exactly one wall class (section 2) and name the metric the question is really asking (M1–M6, `docs/06` §5) | The class's DIRECT tool exists and is reachable offline; if not, record the gap (section 10) |
| **3. Model** | Run the class's tool with crafted/captured state (predicate branch, rehost, walk, audit, decode) | A result that could have come out the other way (a verdict, not a confirmation) |
| **4. Falsify** | Try to break the model: inverse variant, mutated spec, second run for determinism, one-bit differential (`docs/14`) | The falsifier fails to overturn the model; the determinism `compare` is empty |
| **5. Contract** | Pin the decisive run as a registry contract with an explicit expectation | `python3 harness/run_contracts.py --fast` passes and the contract can fail if the tool changes |
The ladder is the OODA loop of `docs/15` made runnable: model is Orient,
falsify is Decide, contract is Act/regress. Skipping stage 4 is how a
misread wall survives (the FECS refusal was initially misread as a V6
limitation until the predicate matrix falsified it; handoff PART 95).
### 3.2 Never infer across domains
The rule is absolute for claims that would change the product:
* **Host bookkeeping cannot prove engine execution.** The engine may read a
different PA than the one the post-mortem checks — that is exactly what
`launch_audit`'s `pa_mismatch` exists to expose. Only a capture shows what
the engine did (class d).
* **Page tables cannot prove method dispatch.** `gmmu_walk` proves a mapping
exists; a silent launch with all legs `ok` means the silence is downstream
of translation (front-end/engine/context), not that the method ran
(`docs/48` §3).
* **A predicate verdict cannot prove the whole handler path.** `valprobe` runs
one branch with the minimum state; the full handler may early-out earlier
or additional gates may exist downstream (`docs/47` §2; `docs/46` §5).
* **A decoded client record cannot prove what the server replied.**
`rm_capture` is send-side; captures currently have no reply sequence
(`docs/45` §7).
* **A demo-surface result is not RM state.** `control_discovery`/`reach` on
the demo surface classify demo dispatch; the RM surface waits for H1.3/H2.2
(`docs/19` §9).
* **One version's behaviour is not portability.** A new image requires the
class-(f) chain, even when one scenario passes.
Every claim in a handoff/write-up carries its class and its tool so a reviewer
can see the domain it came from.
## 4. Per-tool honesty table
Wraps the audits in `docs/27` and the limits in each tool doc. "L0/L1" means
instruction/memory fidelity only; no timing (L2) and no device-environment
(L3) claim (`docs/06` §2).
| Tool | What it MEASURES | What it does NOT measure | Known failure modes |
|---|---|---|---|
| `valprobe` (docs/47) | The branch one firmware predicate takes with crafted register/memory state plus at most one recorded stub; the verdict is a labelled outcome address | Whole-handler behaviour, RM object state, boot state, timing | Wrong entry/state ⇒ `fault` (first missing state) or `unlabelled` outcome; a stub can mask a dependency that would change the branch; crafted state is not what a live caller supplies |
| `rehost` (docs/46) | One bounded firmware function with crafted args/memory/snapshots; the first missing state is the finding | Service loops, interrupts/scheduling, MMIO semantics, timing | `budget`/timeout usually means entry is mid-function or loops; argument conventions must be caller-supplied; lenient unknown MMIO can mask environment dependence |
| `gmmu_walk` (docs/48) | The leaf PA/page-size/aperture/access for a VA through a raw table view; exact fault otherwise | Access ordering, execution, timing; video-memory high bits (`pa_complete: false`) | Wrong root/view ⇒ fail-closed faults; 64K dual path is modelled, not live-validated; incomplete view reports `unmapped`, never "unreachable" |
| `launch_audit` (docs/48) | Per-leg class of a submission's engine-visible memory contract vs host bookkeeping (`ok`/`pa_mismatch`/`unmapped`/`access_denied`) | Whether the engine actually executed/dispatched; anything downstream of the address path | Needs a real capture spec; missing table pages read as `unmapped` (capture error, not engine truth); no fixture ⇒ demo only |
| `rm_capture` (docs/45) | Decode of real client RPC log tags into records; compare of two captures; replay stimulus preparation | Server replies/event ordering; handles/addresses (omitted from stimuli); anything >192 B payloads until the helper chunks | Unknown tags/truncated payloads (`truncated: true`) are not replayable; send-side only; caller-address symbolisation deferred |
| `control_discovery` / `reach` (docs/19) | Unknown command-id classification on a surface; a reachability route with bounded timeout and required args | RM object state/behaviour (surface waits for H1.3/H2.2); dead-code proofs | A timeout is "not reached under this stimulus", never unreachability; long handlers may classify by entry hit with empty effects; trap/fault cause mapping is a documented fidelity gap |
| `triage` / `version_diff` (docs/21, docs/40) | Image identity/sections/symbols/probe/first-touch and M1–M6 diff between two versions | Silicon behaviour; a raw `.fwimage` boots only to its early fault by design; `skipped` is not `pass` | Missing baseline ⇒ `version_diff` step `skipped`; RM 610 refuses without human approval (exit 3); first-touch differences need attribution to profile vs logic before any conclusion |
## 5. Capture-request discipline: one capture, one decidable question
Supervised captures are the scarcest resource; a vague request burns a cycle
and still leaves ambiguity. The rules:
1. **State the question as one falsifiable sentence.** "Does the engine's PDB
resolve the inline-QMD destination VA to the host shadow PA?" — not
"capture more launch data".
2. **Write the acceptance test before asking.** Name the tool and the exact
report field that will decide it (e.g. `launch_audit run --spec` leg
verdict; `rm_capture compare` row deltas). If no existing consumer parses
the data, the capture is not ready.
3. **List the exact pages/registers, byte-exact.** Prefixes without
pretty-printing: PDB root, each level (PDE2/PDE1/PDE0), the leaf table(s),
the submission pointers (GPFIFO ring, `SET_INLINE_QMD_ADDRESS_A/B`, QMD
param/program/release-slot, report semaphore), and the shadow/chunk pages
the engine might write. A well-formed example: the launch-audit request
that listed the channel PDB tree + the `rmcall_gr_shadow` chunk DMAs
(simulator handoff §32; the private request holds the concrete PA list).
4. **Include the comparison basis.** The host/shadow bookkeeping for the same
VAs, so `pa_mismatch` is decidable; a second consumer's view for the same
object if the question is interface alignment.
5. **One question per capture.** Two questions ⇒ two lists; bundling makes a
partial answer unreadable.
6. **Prefer the existing ingestion format.** Raw pages → `greeneye-launch-
audit-spec/1`; RPC logs → the `rm_capture` tag grammar
(`harness/rm_capture.py --print-capture-hints`). Do not invent a format
that no tool reads.
7. **On a failed capture, record the fidelity gap, not a guess.** Missing
pages or truncated payloads become a new request or an unknowns-ledger row
(section 8), never an attribution.
8. **Gates.** The capture itself is supervised, human-run physical work
(`SIMULATOR-WORK-BOUNDARY.txt`); simulator tooling only prepares the spec
and reads the artifact afterwards.
Anti-patterns: "capture more logs" (no acceptance test); pretty-printed
tables instead of raw bytes (breaks byte-exact ingestion); one dump for two
questions; capturing without the host-side bookkeeping map (nothing to
classify against); re-capturing because the previous request lacked the
release slot PA.
## 6. The contract registry
### 6.1 Concept
Every solved wall becomes a **contract**: a declarative record that pins the
proven tool entry point and the expectation observed when the wall was
solved. New walls run the registry first - a failure localises the regression
to one class in seconds; only then is a new probe/capture justified.
| Contract field | Meaning |
|---|---|
| `name` | stable id, e.g. `fecs-predicate-matrix` |
| `wall_class` | one of a–f (section 2) |
| `doc` | the tool/wall documentation reference |
| `entry` | the exact existing entry point wrapped, e.g. `harness.valprobe:run_spec(fecs_spec())` |
| `expect` | the pass condition in words (falsifiable) |
| `speed` | `fast` (runs in `make test`) or `slow` (only `--all`/`--name`) |
| `requires` | input paths; absent ⇒ `skip`, never `pass` |
Rules for adding one: wrap an **existing** entry point (no new measurement
logic); the expectation must be able to fail; keep fast contracts small
(seconds total); one wall class per contract; link the doc; and if the wall
has a public write-up, link it too. A contract without a falsifier is prose.
### 6.2 Commands
```bash
python3 harness/run_contracts.py # fast contracts (make test path)
python3 harness/run_contracts.py --all # fast + slow
python3 harness/run_contracts.py --json dumps/contracts.json
python3 harness/run_contracts.py --fast --name gmmu-golden-vector # single contract
python3 -m unittest tests.test_contracts -v # fast contracts as a test
```
The runner prints pass/fail per contract and per-wall-class coverage (classes
with zero contracts are shown, so gaps stay visible); exit status is nonzero
if any selected contract fails; a `skip` is a missing prerequisite, not a
pass. Schema: `greeneye-contract-run/1`.
### 6.3 Shipped contracts (2026-10-06)
| Contract | Class | Wraps | Expectation | Speed |
|---|---|---|---|---|
| `fecs-predicate-matrix` | b | `harness.valprobe.run_spec(fecs_spec())` | 15/15 PROMOTE_CTX predicate variants labelled, 0 failed | fast |
| `fecs-predicate-determinism` | b | `harness.valprobe.run_demo()` | matrix twice, identical verdicts | slow |
| `rehost-demo` | b | `harness.rehost.run_demo()` | demo function returns the expected sum twice | fast |
| `gmmu-golden-vector` | c | `sim.gmmu_walk.run_demo()` | 5/5 captured VAs → logged DMA addresses | fast |
| `launch-audit-captured-contract` | c | `harness.launch_audit.run_demo()` | captured contract all `ok`; QMD hypotheses yield `pa_mismatch` + `unmapped` | fast |
| `rm-capture-decode-demo` | e | `harness.rm_capture.run_demo()` | synthetic capture decodes/compares/yields stimulus | fast |
Coverage gaps, stated honestly: class **a** and **f** have tools (`triage`,
`version_diff`, boot rungs) but no contract yet because a meaningful fast
expectation needs a pinned baseline pair (that is the queued golden-matrix
build, handoff §30); class **d** has no offline contract by design (section
2). Classes b/c/e are covered by the five fast contracts; `make test` runs
them.
## 7. Admission note (eng-ops §2)
The registry/runner is admitted per `open-iov/docs/eng-ops.md` §2:
| Field | Value |
|---|---|
| **Scenario** | Wall-directed loop: after a wall is solved offline, the answer must survive as a regression; before a new wall is worked, the known walls are re-checked; and every firmware/tool change must show whether a solved wall moved. |
| **Measurable outcome** | `greeneye-contract-run/1` summary: pass/fail/skip per contract + per-wall-class coverage; exit code and JSON artifact; `tests/test_contracts.py` in the suite. |
| **Baseline** | Re-running the same demos by hand per wall, or not re-running them at all; per-tool costs are recorded in `docs/25` §3 (0.2–6.0 s per demo path vs 5–6 min per supervised cycle). |
| **Evaluation threshold** | Keep if: (1) every fast contract passes in ≤ 10 s total in `make test`; (2) each newly solved wall gets exactly one contract within the same session; (3) a deliberate tool change can make a contract fail (falsifier proven by a synthetic failing case in `tests/test_contracts.py`). |
| **Failure signal** | A contract that cannot pin a concrete expectation; two contracts measuring the same thing; a fast contract exceeding the time budget; contracts added faster than walls are solved; or a wall re-opened with the registry green (meaning the contract did not capture the decisive run). On the first signal, prune or rewrite the contract before adding new ones. |
## 8. Unknowns ledger (practice)
Open questions that no current tool answers live in an **unknowns ledger**:
one row per question, with a discriminating experiment. The ledger itself is
maintained by the campaign **outside this tool**; this document describes
only the practice. Row shape:
| Field | Meaning |
|---|---|
| Question | one falsifiable sentence |
| Discriminating experiment | the run that decides it, with the class (a–f) |
| Tool / capture | the registry entry, probe spec, or supervised capture request (section 5) |
| Status | `open` → `probe-ready` (spec exists) → `answered` → `contract` (registry) or `closed` (no action) |
| Evidence | session/report path once answered |
Rules: a row moves to `answered` only with an artifact, and to `contract`
when the decisive run can be pinned (section 6); if the experiment needs a
class-(d) fact, the row says so and waits for a supervised capture rather
than a simulator guess; when a tool that already exists answers the row, a
new tool is not built (`eng-ops.md` §2 core rule).
## 9. Worked examples
* **FECS promote wall (2026-10-06, class b).** Live refusals (`0x1f`) looked
like a V6 channel limitation. Observe: refusal reproduced in logs.
Classify: firmware predicate (b). Model: `valprobe` crafted variants —
refuse iff the shared slot is non-NULL, regardless of aperture/size.
Falsify: `slot NULL` and sysmem-attr variants accept; a second matrix run
is identical. Contract: `fecs-predicate-matrix` +
`fecs-predicate-determinism`. Result: the refusal is the shared one-shot
rule, triggered by the campaign's own earlier promote; FECS is exonerated
(handoff PART 95; `docs/47` §3).
* **Launch silence (`qmd_rel=5`, class c → d).** Classify: the address leg is
(c), method execution is (d). Model: `launch_audit` over the captured
channel fixture partitions `ok`/`pa_mismatch`/`unmapped`; the downstream
question is not simulatable. Falsify: pending the real
`greeneye-launch-audit-spec/1` raw-page capture (section 5). Contract:
`launch-audit-captured-contract` covers the fixture today; the real-spec
validation is handoff §31 item V1.
* **Construct/state walls (`0x40`/`0x52a8b04`, class b).** `rehost` runs the
function and stops at the first missing state — the finding, not a defect.
The missing piece is the RM-object state-seeding fixture (handoff §30 ask
#1); until it exists those walls have no contract, and the registry run
before starting them shows exactly that gap (class b has only demo/known
inputs).
* **Engine dispatch (class d).** No offline contract exists by design. The
registry's coverage line for class d is the reminder that the answer must
come from a supervised capture + post-mortem, with `launch_audit` used
only for its address leg.
## 10. Limits, status and open items
* **Status (2026-10-06).** Registry, runner and tests implemented; 6
contracts (5 fast, 1 slow) at `greeneye-contract-run/1`; fast run ≈ 2 s;
`tests/test_contracts.py` green. Coverage: b (3), c (2), e (1); a/f/d
open as described in §6.3.
* The registry measures nothing new: if a wrapped tool's own demo/test is
wrong, the contract inherits that error. The `docs/25` demos and the
per-tool test modules remain the deeper checks.
* No timing, security or device-environment claim (L0/L1 only); injected
code, modified images and dumps stay development-only.
* Open items: pin the class-a/f contracts once a stable baseline pair exists
(golden matrices, handoff §30/§31); re-validate the launch-audit contract
against the first real `greeneye-launch-audit-spec/1`; add a contract only
when the wall it closes has an artifact.
|