summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-06 19:28:39 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-06 19:28:39 -0400
commit6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf (patch)
tree3b3680902cd2223cbe60aaec0a0799cfc7b26be6 /debug/risc-v-gsp-rm-vm/docs
parenta5473c91b8869b017385582ea969783b33553ded (diff)
downloadopen-iov-6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf.tar.gz
open-iov-6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf.zip
risc-v-gsp-rm-debugger: land docs 45-53 + harness/sim tool set with tests
rm_capture (chunked decode), launch_audit, state_seed/state_decode, valprobe, rehost, contracts+run_contracts, host_traffic, gmmu_walk, msgq_attach, libos_port, log_decode, server_snapshot; doc index updates in the tool overview, operations pages and the repo README.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md8
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/45-RM-RPC-CAPTURE-AND-REPLAY.md143
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/46-SURGICAL-REHOST.md91
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/47-VALIDATION-PREDICATE-PROBE.md172
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/48-GMMU-WALK-AND-LAUNCH-AUDIT.md179
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/49-SIMULATION-SURFACE-MAP.md80
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/50-WALL-WORKFLOW.md301
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/51-HOST-TRAFFIC-LOOP.md253
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md297
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/53-FIRMWARE-OBSERVABILITY-WORKFLOW.md245
10 files changed, 1769 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
index a5f14ab..ebbc20a 100644
--- a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
+++ b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
@@ -131,6 +131,14 @@ Interactively:
| [42-GMMU-FBIF-OBSERVABILITY](42-GMMU-FBIF-OBSERVABILITY.md) | observe-only GMMU/FBIF windows and trace (in development) |
| [43-GMMU-PTE-DECODER](43-GMMU-PTE-DECODER.md) | TU104 GMMU PTE/PDE decoder (in development) |
| [44-GMMU-ORACLE-AND-WORKFLOW](44-GMMU-ORACLE-AND-WORKFLOW.md) | GMMU/FBIF oracle comparison and static<->dynamic join (in development) |
+| [45-RM-RPC-CAPTURE-AND-REPLAY](45-RM-RPC-CAPTURE-AND-REPLAY.md) | real-client GSP RPC capture decode/compare/replay prep (in development) |
+| [46-SURGICAL-REHOST](46-SURGICAL-REHOST.md) | bounded function rehost on the emulator with crafted state (in development) |
+| [47-VALIDATION-PREDICATE-PROBE](47-VALIDATION-PREDICATE-PROBE.md) | firmware validation branch with crafted state + labelled outcomes (in development) |
+| [48-GMMU-WALK-AND-LAUNCH-AUDIT](48-GMMU-WALK-AND-LAUNCH-AUDIT.md) | functional GMMU walk + launch memory-contract audit (in development) |
+| [49-SIMULATION-SURFACE-MAP](49-SIMULATION-SURFACE-MAP.md) | emulator coverage map and value-to-campaign proof (in development) |
+| [50-WALL-WORKFLOW](50-WALL-WORKFLOW.md) | wall taxonomy, capture discipline, contract registry (in development) |
+| [51-HOST-TRAFFIC-LOOP](51-HOST-TRAFFIC-LOOP.md) | host-traffic transport, RPC framing, stub dispatch, replay (in development) |
+| [52-RM-OBJECT-STATE-SEEDING](52-RM-OBJECT-STATE-SEEDING.md) | RM-object state seeding schema + loader for rehost/valprobe (in development) |
| [RISC-V-GSP-RM-DEBUGGER-ROADMAP](../RISC-V-GSP-RM-DEBUGGER-ROADMAP.md) | long-term program H1–H7 |
## Where this fits in the Open-IOV workflow
diff --git a/debug/risc-v-gsp-rm-vm/docs/45-RM-RPC-CAPTURE-AND-REPLAY.md b/debug/risc-v-gsp-rm-vm/docs/45-RM-RPC-CAPTURE-AND-REPLAY.md
new file mode 100644
index 0000000..023689a
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/45-RM-RPC-CAPTURE-AND-REPLAY.md
@@ -0,0 +1,143 @@
+# 45 — GSP RPC capture ingestion and replay prep (in development)
+
+> **Status: in development (2026-10-06, capture-ingestion wave).** This page
+> documents `harness/rm_capture.py`, a stdlib-only harness that turns the
+> **supervised real-client GSP RPC capture** (the tag contract produced by the
+> send-path instrumentation runbook) into structured, replayable artifacts:
+> a decoded capture document, a per-record comparison report, and a replay
+> **stimulus** document for the simulator's B4/B5 CSB/message-queue seam.
+>
+> Development-only: the tool reads files and writes reports. It never touches
+> a GPU, kernel module, mdev, guest or network, and it never runs or schedules
+> the capture itself. The capture is a supervised, human-run physical action
+> (RUNBOOK P1/P7); the tool only ingests its log.
+>
+> **See also:** `06-ACCURACY.md` (fidelity levels, M1–M6),
+> `17-RM-HARNESS.md` (read-only RM oracle policy),
+> `36-GSP-DEVICE-MODEL.md` (the S1 device model and boot ladder),
+> `38-GSP-EM-API.md` (TLV boundary),
+> `44-GMMU-ORACLE-AND-WORKFLOW.md` (the same offline compare discipline for
+> GMMU/FBIF registers).
+
+## 1. What this is and why
+
+The open GR question is *what the real host client actually sends* while it
+creates and commits a GR context (the golden-image sequence and its promote
+controls). The authored capture runbook obtains that sequence from an
+instrumented reference kernel driver; this harness consumes the resulting log
+and prepares it for two workflows:
+
+1. **Compare** — the real client's captured sequence vs. the open-iov host
+ ladder's replay (or two driver/firmware versions), per record, with explicit
+ unmatched lists.
+2. **Replay** — an ordered stimulus document that the simulator's next rung
+ (B4/B5: host-traffic doorbell + first CSB/TRANSCOM poll served + one
+ scripted RM message end-to-end) can feed into the modelled firmware, so the
+ server side of the captured sequence can be observed off-hardware and
+ deterministically.
+
+The simulator consumes captured truth; it does not replace the capture.
+
+Known limitation up front: the capture runbook's hex helper currently caps a
+payload dump at 192 bytes. Payloads larger than that (for example a 560-byte
+`PROMOTE_CTX` control) decode as **`truncated`** and are **not replayable**
+until the helper is extended to emit chunked blocks. The decoder flags this
+per record and in the summary; it never pads a payload to look complete.
+
+## 2. The capture log contract
+
+Lines are matched anywhere in the line, so dmesg/NVRM/timestamp prefixes are
+fine. Tags (exact grammar: `python3 harness/rm_capture.py --print-capture-hints`):
+
+```
+GE-GOLDEN-TRIGGER gspclient=<0|1> mig=<0|1>
+GE-GOLDEN-BEGIN
+GE-GOLDEN-END status=0x...
+GE-SEND fn=0x... len=0x... ret=0x...
+GE-CTRL cmd=0x... hClient=0x... hObject=0x... size=0x... paramp=0x... ret=0x...
+GE-ALLOC cls=0x... hClient=0x... hParent=0x... hObject=0x... size=0x... paramp=0x... ret=0x...
+GE-CTRL-DONE cmd=0x... rpc_status=0x... params_status=0x...
+GE-<TAG> <offset-hex>: <hex bytes...> # attaches to the newest record of that tag
+```
+
+* Hex blocks attach to the most recent record **of their own tag**; an orphan
+ block is reported as a warning, never guessed onto another record.
+* A `GE-SEND` payload's named id is derived when no named control line was
+ captured: function `76` (`GSP_RM_CONTROL`) carries the control id at payload
+ offset 8; function `103` (`GSP_RM_ALLOC`) carries the class id at offset 12.
+* `ret=` is the caller return address: recorded as-is, capture-specific.
+
+## 3. Schemas
+
+* `greeneye-rm-rpc-capture/1` — `{format, source, sha256, records[], summary,
+ warnings, limits}`. Each record: `{index, kind, line, ts?, fn?, length?,
+ size?, cmd?, hclass?, hclient?, hobject?, hparent?, caller?, caller_raw?,
+ marker?, gspclient?, mig?, status?, rpc_status?, params_status?,
+ params_hex?, params_bytes, params_size_expected?, truncated?, anomalies?}`.
+* `greeneye-rm-rpc-compare/1` — inputs + `summary` (matched / agree /
+ shape-agree / partial / mismatch / A-only / B-only / agreement % /
+ coverage % / mean byte agreement %), `rows[]` (per aligned record, with
+ byte agreement and first-difference offset), `a_only[]`, `b_only[]`,
+ `fidelity`, `limits`, `warnings`.
+* `greeneye-rm-rpc-stimulus/1` — `markers[]` (golden slice boundaries) and
+ `messages[]` (`fn`/`cmd`/`hclass`/`wire_id`, `params_hex`, sizes, truncation
+ flag). Handles, physical addresses and caller addresses are **omitted** on
+ purpose: a replay must allocate/re-map them.
+
+## 4. Commands
+
+```bash
+python3 harness/rm_capture.py --print-capture-hints # prints only
+python3 harness/rm_capture.py decode --log capture.log --out dumps/rm-capture
+python3 harness/rm_capture.py compare --a real.json --b replay.json --out dumps/rm-compare
+python3 harness/rm_capture.py stimulus --capture real.json --out dumps/rm-stimulus.json
+python3 harness/rm_capture.py demo --out /tmp/rm-capture-demo # synthetic, measured
+```
+
+Inputs to `compare`/`stimulus` may be decoded JSON documents or raw logs; the
+loader auto-detects. A non-capture JSON format is a clean error.
+
+## 5. Simulator linkage (B4/B5) — not wired yet
+
+The stimulus document is the intended input for the simulator's
+host-traffic rung: once the doorbell path and the first CSB/TRANSCOM poll are
+served and a scripted message can be delivered end-to-end (handoff-simulator
+§17.5), the captured golden-image sequence is the natural first real message
+set. Until then this tool stays offline: `stimulus` states the pending wiring
+in its `limits`, and nothing in `sim/` imports it yet.
+
+Ordering honesty: the capture is **send-side only**; server replies were not
+captured by the probes. A replay therefore reproduces the stimulus, not the
+reply sequence.
+
+## 6. Admission record (draft — pending maintainer filing)
+
+Per the project admission rule (`docs/eng-ops.md` §2): all five fields. This
+record is a draft, like the simulator program's `gpgpu-sim/ADMISSION.md`
+records; a maintainer files it.
+
+| Field | Value |
+|---|---|
+| Scenario | RE/porting loop: ingest a supervised real-client GSP RPC capture into the workflow — decode the golden-image sequence, compare it with the open-iov ladder's replay or another version, and prepare a replay stimulus for the simulator's message-loop rung. No hardware access. |
+| Measurable outcome | Decode/compare/stimulus wall time vs. manual baseline; record and parameter integrity (no silent loss; truncation flagged); deterministic outputs (two runs byte-identical apart from timing). |
+| Baseline | Manual decode-by-eye of one golden sequence from a capture log: grep tags, count records, walk each parameter blob against the RPC structs, transcribe a replay table. Conservative estimate **1800 s** (not measured on a human). |
+| Evaluation threshold | Tool path ≤ **5%** of the baseline (≤90 s); demo `ok`; deterministic reports; new tests plus the full suite green. |
+| Failure signal | Silent record/parameter loss; truncation unflagged; non-deterministic reports; demo above threshold; or any physical action executed/scheduled by the tool → disable the feature and record it as a failed experiment. |
+
+Measured proof (2026-10-06, synthetic demo on this host): 15/14 records,
+13 agree + 1 shape-agree (flipped parameter byte) + 1 A-only (dropped alloc),
+**wall ≈ 0.02 s** vs the 90 s threshold; artifacts under the `demo --out`
+directory; 27 new tests, full suite 444 green (twice).
+
+## 7. Limits and deferrals
+
+* No hardware, module, mdev, guest or network action; the tool never runs the
+ capture.
+* The 192-byte helper cap: extend the runbook helper to chunk before any large
+ payload (promote entries) is considered replayable.
+* Handles/addresses are capture-specific and explicitly omitted from stimuli.
+* Send-side only; no reply/event ordering.
+* B4/B5 stimulus wiring, a captured-sequence vs. simulated-firmware accuracy
+ loop, and symbol/`addr2line` mapping of captured caller addresses are
+ deferred (the `gmmu_debug.py` static↔dynamic join is the pattern to reuse).
+* L0/L1 fidelity only; no timing claim.
diff --git a/debug/risc-v-gsp-rm-vm/docs/46-SURGICAL-REHOST.md b/debug/risc-v-gsp-rm-vm/docs/46-SURGICAL-REHOST.md
new file mode 100644
index 0000000..e64be25
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/46-SURGICAL-REHOST.md
@@ -0,0 +1,91 @@
+# 46 — Surgical rehost of firmware functions (in development)
+
+> **Status: in development (2026-10-06, rehost wave).** Documents
+> `harness/rehost.py`: run one bounded function of a real/modelled firmware
+> image on the native emulator, starting at a chosen entry with a crafted
+> register/memory state, and record a session + report. This is the workflow
+> the RE review ranks first for the live GR wall (docs/39 U2): answer
+> server-side hypotheses from rehosted execution with hooks/watchpoints/
+> memory tools instead of another static read or another hardware cycle.
+>
+> Development-only: executes firmware on the simulator. Never touches a GPU,
+> kernel module, mdev, guest or network. No timing/cycle claim (L0/L1).
+>
+> **See also:** `39-RE-WORKFLOW-REVIEW.md` (U2), `44-GMMU-ORACLE-AND-WORKFLOW.md`
+> (the same evidence discipline), `03-SCRIPTING.md` (hooks/watch/memory),
+> `16-CUSTOM-SIMULATOR.md` (backend design), `45-RM-RPC-CAPTURE-AND-REPLAY.md`
+> (the capture whose sequence a rehost can seed).
+
+## 1. What it does
+
+* Loads an ELF image into the native machine exactly like `sim.gsp_boot`
+ (device registry from the profile, optional boot shim, libos syscall model,
+ lenient MMIO).
+* Writes caller-supplied memory (`--mem addr=hex`), sets `a0..a7`
+ (`--arg`), installs a return trampoline (`ra` + `ebreak` sentinel) and a
+ private stack, and starts at `--entry` (hex address or ELF symbol).
+* Runs under `--budget` instructions and a wall-clock `--timeout`, stopping on
+ return (`R_BP` at the sentinel), fault, unhandled ecall, timeout or budget.
+* Snapshots regions before/after (`--snap addr:size`) with per-region changed
+ bytes, first-difference offset and sha256s; dumps nonzero registers on entry
+ and exit; records a `greeneye-session/1` directory and a
+ `greeneye-rehost/1` report.
+
+```
+python3 harness/rehost.py run --entry 0x5b01210 --arg 0 --budget 50000 \
+ --snap 0x1000000:64 --out dumps/rehost-gate
+python3 harness/rehost.py demo # synthetic self-test (demo firmware)
+```
+
+## 2. Report schema (`greeneye-rehost/1`)
+
+`status` is one of `return | fault | ecall | timeout | budget`; `fault`
+carries the trap cause and `tval`; `retval` is `a0` at the stop; snapshots
+carry the before/after diff. The report states its limits explicitly: it is
+**not a boot** (the function ran with only the supplied state), a fault is the
+**first missing state observed**, not a product defect, and there is no
+timing claim.
+
+## 3. First probes on the 570.144 image (2026-10-06)
+
+All offline, one bounded call each, `a0=0`, budget 300k (numeric vaddrs from
+the campaign analysis):
+
+| entry | status | stop pc | `a0` at stop | insns | fault |
+|---|---|---|---|---|---|
+| GR per-buffer commit gate `0x5b01210` | fault | `0x0` | `0x40` | 24 | illegal @ `0x0` |
+| per-buffer commit `0x5b00d2c` | budget | `0x5b0110c` | `0x40` | 189 | — |
+| commit caller `0x5b00dd8` | budget | `0x5b0120c` | `0x40` | 110 | — |
+| engine-ctx memdesc install `0x514cab8` | fault | `0x0` | `0x429b450` | 97 | illegal @ `0x0` |
+| promote handler `0x526a530` | budget | `0x5244134` | `0x40` | 167 | — |
+| per-entry commit `0x5290a48` | budget | `0x5897fdc` | `0x40` | 258 | — |
+| engine-class commit `0x5350078` | budget | `0x5350484` | `0x40` | 197 | — |
+
+Reading: with no engine-context state, the whole gate family bottoms out in
+`NV_ERR_INVALID_STATE (0x40)` — the same family the live campaign observes —
+and the memdesc-install path faults on a NULL callback early. That is a
+reproduction of known behaviour, not an answer: the next step is to **seed the
+context objects** (from the B4 message-loop state or from crafted structures)
+and A/B the promote entry shapes against these entries.
+
+## 4. Admission record (draft — pending maintainer filing)
+
+Per the project admission rule (docs/eng-ops.md §2), all five fields:
+
+| Field | Value |
+|---|---|
+| Scenario | dynamic-RE loop: run the GR promote/commit functions of the real 570.144 image off-hardware at a chosen entry with crafted state, to answer server-side hypotheses (gate/entry/context requirements) without hardware cycles. |
+| Measurable outcome | sessions per hypothesis; wall time per probe; zero hardware cycles; rehost status/fault map. |
+| Baseline | the live campaign's modes 4–22 day (docs/39: 730+ lines of static RE, three wedge-reboot cycles for one accepted root cause). |
+| Evaluation threshold | ≥1 wall hypothesis answered with a session pair and **zero** hardware cycles; probe loop ≤ 2 minutes per entry (measured: ≈0.1 s per probe); deterministic status/`a0` across runs. |
+| Failure signal | a probe cannot be driven because required device semantics are absent, and crafted-state seeding cannot reach the failing branch after two bounded attempts → record the gap and fall back to static + physical. |
+
+## 5. Limits
+
+* Not a boot: entry-state dependence is the whole difficulty; a `budget` stop
+ usually means the entry is mid-function or the path loops without returning.
+* The first probes use caller-supplied `a0`; the real argument conventions and
+ context layouts are to be supplied by seeding (or by B4/B5 state).
+* Single hart, flat memory, no interrupts/scheduling; unknown MMIO is lenient
+ and counted, and can mask environment dependence.
+* No cycle/timing/security claim; L0/L1 fidelity only.
diff --git a/debug/risc-v-gsp-rm-vm/docs/47-VALIDATION-PREDICATE-PROBE.md b/debug/risc-v-gsp-rm-vm/docs/47-VALIDATION-PREDICATE-PROBE.md
new file mode 100644
index 0000000..8d17859
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/47-VALIDATION-PREDICATE-PROBE.md
@@ -0,0 +1,172 @@
+# 47 — Validation-predicate probe (`valprobe`) — in development
+
+> **Status: in development (2026-10-06, wall-directed wave).** Documents
+> `harness/valprobe.py`: run one firmware **validation branch** of the real
+> image with crafted register/memory state (plus, where needed, one recorded
+> inline stub for a dependency outside the predicate) and record the branch
+> the firmware took. Born from the live GR wall's question class: *which
+> control/entry shapes does 570.144 GSP-RM accept or refuse, and why?*
+>
+> Development-only: executes firmware on the simulator. Never touches a GPU,
+> kernel module, mdev, guest or network. No timing/cycle claim (L0/L1).
+>
+> **See also:** `46-SURGICAL-REHOST.md` (whole-function rehost — the parent
+> tool), `44-GMMU-ORACLE-AND-WORKFLOW.md` (evidence discipline),
+> `39-RE-WORKFLOW-REVIEW.md` (time-to-value ranking), `06-ACCURACY.md`
+> (fidelity levels).
+
+## 1. Why it exists
+
+The live GR campaign submits `GPU_PROMOTE_CTX` shapes to the real GSP-RM and
+reads accept/refuse per supervised cycle. The decisive questions are
+firmware-side predicates, but a whole-function rehost (`docs/46`) early-outs
+on missing RM object state (`0x40`) before the predicate executes. This
+harness executes the predicate branch itself:
+
+* **entry**: the validation branch point (not the full handler);
+* **state**: exactly the registers/memory the branch reads, crafted in RAM;
+* **stub**: at most one `lui/addi/ret` stub for a dependency that is *not*
+ the predicate (e.g. a lock/table walk), recorded in the report;
+* **outcome**: execution stops on labelled outcome addresses; the verdict is
+ the branch taken, with the firmware's own arithmetic/indexing.
+
+## 2. Invocation
+
+```bash
+# Bundled matrix: the 570.144 PROMOTE_CTX per-buffer validation (FECS etc.)
+python3 harness/valprobe.py sweep # crafted-state, ~2 s
+python3 harness/valprobe.py sweep --boot-to B3 # boot the image first
+python3 harness/valprobe.py demo # matrix twice (determinism)
+python3 harness/valprobe.py run --spec my-spec.json # generic spec
+```
+
+A spec is `greeneye-valprobe-spec/1` with a `target`, an `outcomes` map
+(address → label), optional `boot_to`, and `variants` carrying `registers`,
+`memory` (`hex|u64|u32|u16|u8`), `stubs` (`addr` + `return`), `reads`
+(post-run) and `expect`. Report schemas: `greeneye-valprobe/1` per variant,
+`greeneye-valprobe-matrix/1` for the run (`valprobe-matrix.json`), with one
+`greeneye-session/1` per variant when recording is on.
+
+Outcome statuses: `outcome` (a labelled address was reached — the verdict),
+`return` (`ra` sentinel), `fault` (first missing state), `ecall`, `timeout`,
+`budget`. `pass` compares `outcome` to `expect`; `sweep` exits 3 on any fail.
+
+## 3. The current wall, answered offline (570.144, GR PROMOTE_CTX)
+
+Live facts this reproduces (V6 KMD-managed channel, supervised cycles):
+`PM`/`PATCH` init accepted, **FECS_EVENT init refused `0x1f`**, FECS VA-only
+accepted; engine dispatch still absent. The handoff's suggested next hardware
+experiment was "find a FECS init form V6 accepts (e.g. a sysmem attr 0x6 /
+different size / different bufferId)".
+
+The probe enters the per-buffer validation branch `0x5290dd4`
+(`FUN_ram_05290a48`) and executes the firmware's own logic. Firmware facts
+(disassembly, numeric addresses only):
+
+| # | Fact |
+|---|---|
+| 1 | `bInitialize=1`, `bufferId==9` (FECS) takes the branch at `0x5290e3c` |
+| 2 | that branch reads the **global GR-context memDesc slot**: `instance = pGpu[(idx+0x37c)]`, `table = *(u64*)(instance+0x190)`, slot index from the firmware's own `0x531b944` jump table (**FECS → 6**), and returns `NV_ERR_INVALID_ARGUMENT` (`0x1f`) **iff `slot != 0`** |
+| 3 | `physAttr`, `size`, `gpuPhysAddr`, `gpuVirtAddr` are **not read** in that branch |
+| 4 | `bInitialize=0`: `bNonmapped && !bInitialize` → `0x1f`; otherwise the entry passes |
+| 5 | init is allowed for ids 0..2, 8..11; refused for 3..7 and 12; ids > 12 refused a block earlier |
+
+So **if the server has already populated the FECS slot, no init form exists**
+— the aperture/size variants cannot change the verdict, and the only accepted
+FECS shape is `bInitialize=0` (a VA booking, matching the live VA-only
+accept). The campaign can redirect from "find a FECS init form" to
+"prevent/clear the server's FECS slot" or "accept VA-only and look at
+dispatch elsewhere" without spending a cycle on the aperture test.
+
+Matrix (15/15, deterministic; run twice with identical verdicts; 1.9 s
+crafted-state, 13 s with `--boot-to B3`):
+
+| variant | verdict |
+|---|---|
+| FECS init vidmem, slot non-NULL | `refuse-fecs-slot-exists-0x1f` |
+| FECS init vidmem, slot NULL | accept (commit path) |
+| FECS init **sysmem attr 0x6**, slot non-NULL | `refuse-fecs-slot-exists-0x1f` |
+| FECS init **sysmem attr 0x6**, slot NULL | accept |
+| FECS init size 0x1000, slot non-NULL | `refuse-fecs-slot-exists-0x1f` |
+| FECS no-init mapped, slot non-NULL | accept (skip path) |
+| FECS no-init nonmapped | `refuse-invalid-argument-0x1f` |
+| PM init / PATCH init | accept |
+| GFXP_CTRL_BLK (8) init | accept (skip path) |
+| PRIV (10) / UNR (11) init | accept (skip path) |
+| BUNDLE_CB (3) / GLOBAL_PRIV (12) init | `refuse-invalid-argument-0x1f` |
+| unknown id 13 | `refuse-invalid-argument-0x1f` |
+
+Each variant's report also carries the firmware-mapped slot index read back
+from the run (`6` for FECS) and the applied stub, so the data flow is in the
+evidence, not in prose.
+
+### 3.1 The FECS buffer is global and one-shot (live-confirmed 2026-10-06)
+
+The probe's refusal condition was then joined to a live backend run and the
+semantics are now settled:
+
+* The FECS memDesc lives in a **global GR context-buffer table shared across
+ channels** (per GPU/client, per gfid); the table's only writer is the
+ init-promote path, and a successful init leaves the slot set so that
+ **every later `bInitialize=1` FECS promote is refused `0x1f`** — independent
+ of aperture, size, address or channel.
+* A live run showed the decisive ordering: the backend's own **golden-channel
+ 9-entry atomic promote** (including `FECS_EVENT bInitialize=1`) was accepted,
+ and the per-entry FECS promote on a *different channel of the same client*
+ was refused `0x1f` **~0.1–0.3 s later**. The refusal was the shared
+ one-shot rule, not a channel deficiency.
+* The per-channel requirement for FECS is therefore only the **VA booking**
+ (`bInitialize=0`), which the same live channel already accepted. No client
+ "FECS init form" exists once any earlier promote initialized the shared
+ buffer; an atomic multi-entry set containing FECS init fails as a whole
+ after that point.
+
+This is what the probe class is for: it turns a supervised-cycle guessing loop
+into a seconds-long offline verdict plus one log join.
+
+## 4. Honesty rules
+
+* A probe is **not a boot** and **not a model**: only the seeded state exists;
+ the report lists entry, registers, memory, stubs, outcomes and limits.
+* A stub replaces an explicitly named dependency outside the predicate; the
+ predicate, its id→slot mapping call and its memory reads are the firmware's
+ own code. Prefer entering after the dependency when that is safe.
+* A fault is the **first missing state observed**, not a product defect; a
+ timeout/budget is not a verdict.
+* `--boot-to <rung>` runs the real image first so predicates that need
+ initialized globals can be probed with real state; the report records the
+ observed rung and whether the request was reached.
+* No timing/cycle claims; sessions and matrix JSON are the evidence.
+
+## 5. Tests
+
+`tests/test_valprobe.py` — stub encoding decodes as `lui/addi/jalr`, memory
+spec widths, spec shape, scratch layout bounds, a live reduced matrix with
+determinism, the firmware slot-index read-back (asserts `6`), boot-to-B3, and
+CLI exit codes. Live tests skip without the shipped image.
+
+```bash
+python3 -m unittest tests.test_valprobe -v
+python3 harness/valprobe.py demo
+```
+
+## 6. Problem classes and time-to-value
+
+Use this tool when the question can be stated as *"what will this firmware
+branch do with input/state X?"* — control-entry validation, accept/refuse
+rules, error-path predicates, field-dependency questions. It answers offline,
+in seconds, with labelled outcomes and a session; the alternative is static
+reading (rule known, cause not), or a supervised hardware cycle per variant
+(minutes to an hour plus teardown risk).
+
+The reference case (2026-10-06): a wall that had consumed five-plus supervised
+cycles and was still misdiagnosed as a channel limitation was closed by a
+15-variant probe (≈2 s, run twice, identical) plus one log join. The rule to
+copy: phrase the question as a predicate → probe it offline → join the verdict
+to live logs → spend the supervised cycle only on what the probe cannot see
+(the engine/silicon side).
+
+Companion tools: `harness/rehost.py` (docs/46) for whole-function calls with
+crafted state and snapshots; `sim.gsp_boot` for boot-state evidence (rung and
+state); `harness/triage.py` / `version_diff.py` for release-to-release
+questions.
diff --git a/debug/risc-v-gsp-rm-vm/docs/48-GMMU-WALK-AND-LAUNCH-AUDIT.md b/debug/risc-v-gsp-rm-vm/docs/48-GMMU-WALK-AND-LAUNCH-AUDIT.md
new file mode 100644
index 0000000..c412f21
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/48-GMMU-WALK-AND-LAUNCH-AUDIT.md
@@ -0,0 +1,179 @@
+# 48 — GMMU walk & launch memory-contract audit (in development)
+
+> **Status: in development (2026-10-06, engine-path wave).** Documents the
+> functional page-table walker (`sim/gmmu_walk.py`) and the launch
+> memory-contract auditor (`harness/launch_audit.py`). Together they answer,
+> offline and deterministically, *where the engine would read/write each
+> address of a submission* — the class of question behind "the front-end never
+> wrote the QMD" and "the release never fired".
+>
+> Development-only tooling: never a shipped component, never in a guest,
+> never touching hardware. Fidelity is the address view (L1); no timing or
+> engine-behaviour claim.
+>
+> **See also:** `43-GMMU-PTE-DECODER.md` (entry decoding), `42`/`44`
+> (observe-only GMMU/FBIF windows and the oracle workflow), `46`/`47`
+> (surgical rehost and predicate probes), `06-ACCURACY.md` (fidelity levels).
+
+## 1. What the two tools are
+
+* **Walker** (`sim.gmmu_walk`): given a page-directory base PA and a sparse
+ physical-memory view of the table pages, it follows VER2 entries through all
+ five levels and reports the leaf mapping — PA, page size, aperture,
+ writable/read-only — or the exact fault (`table_missing`,
+ `invalid_aperture`, `pte_invalid`, `unsupported`, `va_out_of_range`).
+ Every level's raw entry and decode is kept in the report.
+* **Auditor** (`harness/launch_audit.py`): takes a submission's **memory
+ contract** — the labelled VAs a launch requires (ring, inline-QMD
+ destination, parameter/program pages, release slot, report semaphore, ctx
+ buffers) — walks each leg, and classifies the result against what the host
+ believes:
+
+| verdict | meaning |
+|---|---|
+| `ok` | the engine can reach the leg with the required access |
+| `pa_mismatch` | the walk resolves to a different PA than the host's bookkeeping (the engine writes the shadow while you check the guest page, or vice versa) |
+| `unmapped` | no mapping/valid leaf: the engine's access is dropped or faults |
+| `access_denied` | the leaf is read-only for a write leg |
+
+Schemas: `greeneye-gmmu-walk/1`, `greeneye-launch-audit-spec/1`,
+`greeneye-launch-audit/1`, `greeneye-launch-audit-check/1`.
+
+```bash
+python3 -m sim.gmmu_walk demo # captured channel-PDB vector
+python3 -m sim.gmmu_walk run --pdb-pa 0x12f16e000 \
+ --va 0x100000 --mem 0x12f16e000=<hex> … # ad-hoc walk
+python3 harness/launch_audit.py demo # captured contract + fork classes
+python3 harness/launch_audit.py run --spec audit.json
+python3 harness/launch_audit.py check --spec partial.json # pages to dump next
+python3 harness/launch_audit.py capture-hints # exact live dump list + skeleton
+```
+
+## 2. Evidence and validation
+
+* Geometry, field positions and encodings come from the same evidence base as
+ the PTE decoder (`docs/43`); the walker composes the decoder rather than
+ re-implementing it.
+* The 4K and 2M paths are **validated against a live captured channel PDB**:
+ `sim.gmmu_walk demo` walks five VAs and must reproduce the DMA addresses the
+ capture logged (5/5, deterministic). The capture lives as numeric entries in
+ the module (`channel_pdb_fixture`).
+* The 64K dual path is **modelled** from the format definitions and reported
+ with `evidence: "modeled"` until a capture validates it. Video-memory leaf
+ addresses are reported with `pa_complete: false` (the decoder does not claim
+ the high bits there).
+* Unsupported encodings fail closed; an incomplete table view reports
+ `unmapped`, never "unreachable".
+
+## 3. Using it on the live launch wall
+
+`harness/launch_audit.py capture-hints` prints the capture recipe plus the
+exact page list and a copy-paste `greeneye-launch-audit-spec/1` skeleton for
+the current wall. In short: the channel's PDB base PA, a raw PA→bytes dump of
+the table pages reachable from it for the contract VAs, the submitted stream's
+pointers (GPFIFO ring, `SET_INLINE_QMD_ADDRESS_A/B` destination, QMD
+parameter/program/release-slot addresses, report semaphore), and the host's
+shadow bookkeeping for the same VAs. Put them in one spec and run the audit.
+
+### 3.1 `check` — iterate the capture with an empty or partial dump
+
+`check` runs the same walks but reports, per contract leg, the FIRST table
+page the `memory[]` view is missing — the page to dump next — with the
+level/index path that reached it, and a consolidated `needed_pages[]` list
+(sorted, de-duplicated, each entry naming the legs that need it). A leg whose
+walk faults on entries that are present (invalid aperture/PTE, unsupported
+encoding) is `fault` and needs no more pages; `complete` means every table
+page the walk needed is in the spec (not that any mapping is correct).
+
+Start with only `pdb_pa` + `contract` (empty `memory`) and let the tool drive
+one dump level per iteration; then run the verdict audit on the completed
+spec:
+
+```bash
+python3 harness/launch_audit.py check --spec m7-launch.json # -> needed_pages
+python3 harness/launch_audit.py check --spec m7-launch.json --out ./out
+python3 harness/launch_audit.py run --spec m7-launch.json
+```
+
+### 3.2 Current live wall dump list (m7 evidence, 2026-10-06)
+
+The m7 launch silence needs these raw pages (`capture-hints` prints the same
+list with the spec skeleton):
+
+* host-side chain (the validated 5/5 fixture): root `0x12f16e000`, pd2
+ `0x12f16f000`, pd1 `0x11065e000`, pd0 `0x110663000`, pt `0x15908d000`;
+* forward-window PT pages `0x1b8706000` (pdeIndex 4128) and `0x1197a6000`
+ (pdeIndex 4131); the geometry rule `pdeIndex = (pd1_idx << 8) | pd0_idx`
+ places the forward windows at pd1 index 16, and their pd1/pd0-level chain
+ entries are server-written (in no host log) — dump them too if they fall
+ outside the host pages above;
+* the 16 guest-scratch-PT page DMAs (`vaBase=0x200000`, capacity 512) — the
+ Tier-2 shadow legs resolve through them;
+* the `rmcall_gr_shadow` chunk DMAs — the engine-write shadow pages for the
+ high-VA legs.
+
+Contract VAs: QMD destination `0x204608000` (write), release slot
+`0x204027f7c` (write), low-window ring/push-buffer legs `0x100000..0x104000`,
+and the high-VA param/program legs, which are NOT directly mapped — they
+resolve through the shadow aliases (`0x210000`/`0x220000`/`0x260000`) whose
+PTEs come from the guest scratch pool.
+
+Reading the result:
+
+* `pa_mismatch` on the inline-QMD destination or the release slot means the
+ engine operates on a different PA than the one the post-mortem checks — fix
+ the bookkeeping/observation point, not the engine path.
+* `unmapped` means the engine's access is dropped at translation; the fault
+ names the exact missing level/table — `check` names the page to dump.
+* all-`ok` with a silent launch means the address path is complete: the
+ silence is downstream (front-end/engine/context), and the next suspect is
+ the context program itself (see docs/46/47 for the commit-gate probes).
+
+### 3.3 Demo scenarios and expected verdicts (pinned by tests)
+
+`demo` audits the fork classes and prints every verdict:
+
+| scenario | mapping (VA → PA) | host bookkeeping | expected verdict |
+|---|---|---|---|
+| captured channel contract | 5 low-window legs (validated fixture) | matches capture | all `ok` |
+| `qmd-dest-shadow-trusted` | 0x204608000 → shadow 0x15e8fa000 | shadow PA | `ok` |
+| `qmd-dest-host-checks-guest` | 0x204608000 → shadow | guest page 0x204608000 | `pa_mismatch` |
+| `qmd-dest-unmapped` | 0x204608000 → unmapped | n/a | `unmapped` |
+| `engine-write-shadow-host-checks-guest` | alias 0x210000 → shadow PA | guest PA | `pa_mismatch` |
+| `engine-write-shadow-bookkeeping-aligned` | alias 0x210000 → shadow PA | shadow PA | `ok` |
+
+The last two are the next wall class after dispatch clears: CUDA output
+buffers at high VAs, where Tier-1 is refused (0x1f) and the leg resolves
+through the guest scratch PT pool to a shadow page. The shadow/guest PAs in
+the demo are synthetic (documented in the `run_demo` docstring), not m7
+captures.
+
+## 4. Reuse across firmware/driver version upgrades
+
+Both tools are data-driven and fail closed; nothing about them encodes a
+specific firmware version except the entry-encoding family:
+
+1. On a new version, re-run `python3 -m sim.gmmu_walk demo` and the test
+ module. If the encoding family changed, the walker refuses with
+ `unsupported` rather than guessing.
+2. Extend the evidence base (decoder field tables/encoders) and the LEVELS
+ geometry in one place (`sim/gmmu_pte.py` / `sim/gmmu_walk.py`), add one
+ captured golden vector per new format, and pin it in tests.
+3. Keep `firmware`/`gmmu` fields in audit specs so reports are self-describing
+ across versions; the audit refuses an unknown `gmmu` kind.
+4. Re-capture the contract data from the new version (the capture recipe is
+ version-independent) and re-run; compare the two audit reports for
+ behaviour drift.
+
+## 5. Tests
+
+`tests/test_gmmu_walk.py` (15) and `tests/test_launch_audit.py` (20):
+captured golden vector, synthetic round-trips, 2M/64K paths, all fault
+classes, read-only leaves, mismatch/unmapped/access-denied verdicts,
+check-mode page needs (empty memory, partial memory, complete+fault legs,
+needed-pages dedup/ordering, CLI exit codes/artifact), engine-write shadow
+demo verdicts, determinism, spec errors and CLI exit codes.
+
+```bash
+python3 -m unittest tests.test_gmmu_walk tests.test_launch_audit -v
+```
diff --git a/debug/risc-v-gsp-rm-vm/docs/49-SIMULATION-SURFACE-MAP.md b/debug/risc-v-gsp-rm-vm/docs/49-SIMULATION-SURFACE-MAP.md
new file mode 100644
index 0000000..f3de0c9
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/49-SIMULATION-SURFACE-MAP.md
@@ -0,0 +1,80 @@
+# 49 — Simulation Surface Map and Handoff-vGPU Value Case
+
+Status: session 12 assessment (2026-10-06). Scope: the simulator workstream
+(`open-iov/debug/risc-v-gsp-rm-vm/`) and what it offers the GR campaign.
+Rule: nothing goes to `Handoff-vGPU-IO.md` unless a past wall proves it saved
+time. This document is that proof table and the coverage map for future walls.
+
+## 1. What the emulator is for (and is not)
+
+It boots the **real 570.144 blob** to `B3` (474k insns, deterministic, device
+model + LIBOS + MPU, `greeneye-session/1` recordings) and provides
+dump/inspect/inject around it. It is **not** a GR/PBDMA engine model: engine
+questions are answered by hardware captures + the GMMU/launch audit, never by
+simulation (L2/L3 absent, docs/06). Fidelity claims are L0/L1 only.
+
+## 2. Coverage map — what exists per subsystem
+
+| Subsystem (future-bug surface) | Tool / entry | Standard format | Status |
+|---|---|---|---|
+| Image/boot/portability | `sim/gsp_boot.py`, `harness/probe*`, `triage.py`, `version_diff.py` | `greeneye-session/1` | built, B3 ceiling |
+| Device windows / MMIO / BARs | `sim/devices`, `harness/gmmu_debug.py`, device-map | device-map JSON + sessions | built |
+| LIBOS/RTOS services, MPU | `sim/libos.py`, `sim/mpu.py` | sessions | built |
+| GMMU PTE/PDE (VER2/4K/2M/64K) | `sim/gmmu_pte.py`, `gmmu_walk.py`, `gmmu_oracle.py` | walk reports | built, live 5/5 fixture |
+| Launch memory contract (QMD/param/program/sem legs) | `harness/launch_audit.py` | `greeneye-launch-audit-spec/1` | built; awaiting supervised trace |
+| Validation predicates / gates | `harness/valprobe.py` | valprobe spec + labelled outcomes | built (FECS 15/15, seconds) |
+| Whole-function exec with crafted state | `harness/rehost.py` + snapshots | sessions | built; queued for construct gate |
+| Client RPC traffic (decode/replay-prep) | `harness/rm_capture.py` | capture JSON + compare | built; decode from logs |
+| Client↔server live traffic loop | **B4/B5 — missing** | (rm_capture stimulus) | strategic build |
+| RM object state (clients/channels/contexts) | **state-seeding fixture — missing** | (sessions) | campaign ask #1 |
+| Engine method execution (GR/PBDMA) | **not simulated by design** | captures + `launch_audit` | out of scope, L2/L3 absent |
+| Instrumentation/injection | hooks/watchpoints/snapshots, SDK, MCP, Ghidra bridge | `events.jsonl`, blobs, hits TSV | built |
+
+## 3. Standard capture formats (already unified)
+
+* `greeneye-session/1`: `session.json` + `events.jsonl` + `snapshots/` +
+ `blobs/` + `summary.md`; render with `analyze`, diff with `compare`.
+* `rm_capture` JSON: real client RPC decode/compare/stimulus preparation.
+* `greeneye-launch-audit-spec/1`: `pdb_pa` + `contract[]`
+ (label/va/access/expect_pa) + raw table pages → per-leg verdicts
+ (`ok`/`pa_mismatch`/`unmapped`/`access_denied`).
+* valprobe spec: crafted state + labelled outcomes → accepted/refused matrix.
+* symbols DB (export) and device map (harvest/query) schemas.
+
+## 4. Value-to-Handoff proof (do not add unproven tooling)
+
+| Past wall | Emulator contribution | Measured value | Verdict |
+|---|---|---|---|
+| FECS `0x1f` one-shot rule | `valprobe` predicate matrix, offline | matrix in seconds vs hours of static/log work; closed lane (a) | **proven, adopted** |
+| Fetch-state hypothesis (userd ctl/GET/PUT) | whole-image access scan | zero accesses proven exhaustively without a hardware cycle | **proven, closed §6** |
+| GMMU/QMD mapping forks | live 5/5 channel-PDB fixture + walker | fixture and verdict classes without silicon | **proven for audit input** |
+| Launch silence (`qmd_rel=5`) | `launch_audit` fork partitioned (`ok`/`pa_mismatch`/`unmapped`) | single spec run classifies the fork once trace exists | **ready, pending supervised spec** |
+| Golden-channel construct `-110`/`0x52a8b04` | rehost/probe queued; static analysis answered first | honest counter-example: analysis beat emulator here | **no claim yet** |
+| Engine execution/shape mismatches | none | captures + audit answer it | **never promise emulation** |
+
+Rule: a capability is added to Handoff-vGPU only with a row in §4 and an
+eng-ops admission (scenario + metric + baseline + threshold + failure signal).
+
+## 5. When the campaign should consult the emulator (and when not)
+
+| Wall type | Consult simulator? | Tool |
+|---|---|---|
+| Validation/refusal predicate, unknown gate | yes, first (seconds) | `valprobe` |
+| Address translation / PDB / launch contract | yes | `gmmu_walk`, `launch_audit` |
+| "Does the server read/write address X?" | yes | access scan / watchpoints |
+| Full client traffic interaction with server | only after B4/B5 | rm_capture + sessions |
+| RM `-110` deadlock with live objects | rehost if state can be crafted; else source | `rehost` |
+| Engine/silicon timing, PBDMA behaviour, real dispatch | no | hardware captures + audit |
+
+## 6. Queued build order (each needs the §4 admission before Handoff mention)
+
+1. **B4/B5 host-traffic message loop** — replay `rm_capture` stimulus into the
+ real blob, dump every reply/write: the generic tool for the wall class the
+ campaign keeps hitting ("what does the server do with our traffic").
+2. **RM-object state-seeding fixture** — execute whole construct/handler paths
+ with crafted channels/contexts (campaign ask #1).
+3. **Per-version golden matrices** — promote stable probe/audit runs to CI so
+ firmware bumps are regression-checked.
+
+Do **not** build: GR/PBDMA engine model, speculative backends, or any tool
+without a wall scenario it demonstrably solves faster.
diff --git a/debug/risc-v-gsp-rm-vm/docs/50-WALL-WORKFLOW.md b/debug/risc-v-gsp-rm-vm/docs/50-WALL-WORKFLOW.md
new file mode 100644
index 0000000..24ee3ce
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/50-WALL-WORKFLOW.md
@@ -0,0 +1,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.
diff --git a/debug/risc-v-gsp-rm-vm/docs/51-HOST-TRAFFIC-LOOP.md b/debug/risc-v-gsp-rm-vm/docs/51-HOST-TRAFFIC-LOOP.md
new file mode 100644
index 0000000..e56094c
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/51-HOST-TRAFFIC-LOOP.md
@@ -0,0 +1,253 @@
+# 51 — Host-traffic message loop: doorbell + msgq transport + replay (in development)
+
+> **Status: in development (2026-10-06, B4/B5 transport wave).** This page
+> documents `sim/host_traffic.py`, the transport half of boot-ladder rungs
+> B4/B5 (docs/35-GREENEYE-SIMULATOR-DESIGN.md §3.3): a modelled host
+> doorbell, a command/status message-queue pair laid out with the real queue
+> arithmetic, queue-element framing with the real checksum, a stub dispatcher
+> and deterministic replay of an `rm_capture` stimulus (docs/45). The **real
+> 570.144 firmware is not run by this module**: there is no real message loop,
+> no RM semantics and no firmware-behaviour claim.
+>
+> Development-only, offline: no GPU, kernel module, mdev, guest, network,
+> QEMU, `/dev` access or reboot. Nothing here is a product dependency and
+> nothing is injected into a guest.
+>
+> **See also:** `35-GREENEYE-SIMULATOR-DESIGN.md` §3.3 (ladder), `36` (device
+> model; the falcon window this doorbell overlays), `45` (capture decode and
+> stimulus), `49` (surface map; B4/B5 is queued build #1), `06-ACCURACY.md`
+> (L0/L1 only), `open-iov/docs/eng-ops.md` §2 (admission rule).
+
+## 1. What this is and why
+
+The campaign keeps hitting one wall class: *what does the server do with the
+traffic we send it, and what does it read/write/wait on for this control*.
+The strategic tool for that class is B4/B5: replay a captured real RPC
+sequence into the emulated GSP through an emulated transport and record the
+replies and memory writes as evidence (handoff-simulator §27, §30, §31 item
+B1; docs/49 §6 build order #1).
+
+This wave lands the **transport and framing** so the replay plumbing exists
+and is tested before the firmware loop is connected:
+
+* a doorbell register model at the real hardware offset;
+* command/status msgq rings in simulated memory, laid out with the arithmetic
+ the real host queue code uses (including the `MSGQ_FLAGS_SWAP_RX`
+ cross-link of the two rx headers);
+* queue-element and RPC framing with the real header layouts and checksum;
+* a 2-handler stub dispatcher plus `NV_ERR_NOT_SUPPORTED` for real RM
+ functions, so a message can travel host → command ring → dispatch → status
+ ring → host and be recorded;
+* replay of a `greeneye-rm-rpc-stimulus/1` document (or a capture document
+ converted by `harness/rm_capture.py`), message by message, in order.
+
+The dispatcher is deliberately a **stub**: answers are labelled
+`stub:echo`, `stub:status_query`, `stub:unsupported` in the transcript. A
+stub verdict is never presented as firmware behaviour.
+
+## 2. Admission record (draft — pending maintainer filing)
+
+Copied from the queued execution item (private live contract §31 item B1),
+expanded to the `open-iov/docs/eng-ops.md` §2 five-field format; a maintainer
+files the final record. One scenario, one axis (axis 4, bug-analysis time).
+
+```text
+TOOL ADMISSION RECORD id: TOOL-2026-XX (draft)
+tool: sim/host_traffic.py (new; B4/B5 transport + replay wave)
+loop: dynamic-RE (campaign traffic-interaction walls)
+axis: 4 — bug-analysis time (supervised cycles avoided / TTFA)
+owner/decider: <maintainer>; executor: agent
+scenario: a campaign traffic-interaction wall (e.g. the V6 launch
+ silence) asks what the server does with a given control or
+ reply traffic; replay the captured stimulus into the modelled
+ transport and read the server-side evidence offline.
+measurable: wall resolved from emulator evidence alone or with < 1
+ supervised cycle; per wall: a session/transcript pair with
+ every submitted message, dispatch handler and reply recorded.
+baseline: today = static decompiler dig + one or more supervised
+ hardware cycles per hypothesis (unrecorded as a single
+ number; docs/49 §4).
+threshold: keep if 2 of the next 3 wall-class instances are answered
+ offline; N >= 3 wall-class instances; environment: this tool
+ checkout, simulator only.
+failure signal: replies diverge from captured server behaviour, or the tool
+ cannot represent the control/queue shape -> record the
+ fidelity gap and stop (no emulation edit from simulator-only
+ evidence).
+evidence: tests/test_host_traffic.py (36 tests) + `demo`/`replay`
+ session directories under the `--out` directory.
+decision: admit-pilot
+review-by: one quarter / five sessions trials-dir: dumps/
+```
+
+Queued-item exit criterion (handoff §31 B1): *one real 512-byte RPC control
+replayed end-to-end with the reply captured in a session*. This wave's demo
+replays a **real-shaped** 512-byte `GSP_RM_CONTROL` (built to the documented
+control shape, from no capture) end-to-end through the transport; its reply
+is the stub dispatcher's framed `NV_ERR_NOT_SUPPORTED`, captured in a session.
+`replay` consumes a real stimulus document the moment a supervised capture
+provides one — the capture itself is still pending, so no captured-sequence
+claim is made yet.
+
+## 3. What the transport is based on (real offsets and structures)
+
+All facts below come from public OpenRM register/queue header material (the
+same class of evidence the tracked device model already uses). The capture
+tool exposes the overlapping facts as constants (`RPC_HEADER_BYTES`, the
+`GSP_RM_*` ids, the payload offsets), not as struct types, so this module
+mirrors them and `test_rpc_constants_match_capture_tool` fails if they drift.
+The models are L0/L1 framing and ordering only; no timing claim.
+
+| Fact | Value | Use in the model |
+|---|---|---|
+| GSP queue-head register | `NV_PGSP_QUEUE_HEAD(i) = 0x110c00 + i*8`, 32-bit RW, i < 8 | doorbell window `PRIV+0x110c00`, 0x40 bytes; reached by the firmware through the PRIV aperture |
+| Host kick | the 570.144 submit path writes the queue head after queueing (observed value 0) | `HostTraffic.submit` rings queue 0 with 0; the recorded value is the firmware-visible read-back |
+| Falcon mailbox | `NV_PGSP_FALCON_MAILBOX0/1` 0x110040/0x110044 | already modelled by `sim/devices/falcon.py`; not duplicated here |
+| `msgqTxHeader` | 8 x u32: version, size, msgSize, msgCount, writePtr, flags, rxHdrOff, entryOff | `MsgqTxHeader` / `MsgqQueue` |
+| `msgqRxHeader` | 1 x u32: readPtr | `MsgqQueue.set_read` / `.read_ptr` |
+| msgq arithmetic | `rxHdrOff = align32(16) = 32`, `entryOff = align36(16) = 48`, `msgCount = (size - entryOff) / msgSize` | `MsgqQueue.format` reproduces it exactly |
+| Queue flag | `MSGQ_FLAGS_SWAP_RX = 1`; the real command queue sets it and `msgqRxLink` cross-links the rx headers between the command and status buffers | `SharedQueues` places the command rx header in the status buffer and vice versa |
+| `GSP_MSG_QUEUE_ELEMENT` | 16 B authTag + 16 B aad + checksum + seqNum + elemCount + 4 B reserved + 32 B `rpc_message_header_v` (payload at 0x50) | `build_queue_element` / `parse_queue_element` |
+| Element/queue sizes | element min 0x1000, max 0x10000; default command and status queue 0x40000 each; header size 0x1000; header align 4, element align 12 | defaults in the module; a message stages contiguously and splits across ring slots exactly as the real producer copies it |
+| Multi-element | elemCount in the first element; the contiguous staged stream is split into 0x1000-byte slots (tail zero-padded); checksum covers the staged stream | `split_element_stream` + `SharedQueues._place`/`_take` (wrap-safe); max 16 elements |
+| Checksum | XOR of 64-bit words over `0x30 + rpc.length` bytes, then hi32 ^ lo32; the stored field makes the receiver's recomputation zero | `compute_message_checksum` / `build_queue_element` (verified) |
+| RPC header | 32 bytes: header_version (0x03000000), signature ("VRPC" 0x43505256), length (= 32 + payload), function, rpc_result, rpc_result_private, sequence, spare | `build_rpc_message` / `parse_rpc_message` |
+| RM ids | `GSP_RM_CONTROL = 76`, `GSP_RM_ALLOC = 103`, `GSP_GET_GSP_STATIC_INFO = 65` (same as the capture tool) | replay mapping for `control`/`alloc` records |
+| NV status | `NV_OK = 0`, `NV_ERR_NOT_SUPPORTED = 0x56` | stub replies |
+
+Modelled rather than proven, and therefore labelled:
+
+* the **shared memory address and queue sizes** are fixture defaults; the real
+ driver learns them from the `GSP_ARGUMENTS_CACHED` / message-queue init
+ arguments. Nothing here reads a real `MESSAGE_QUEUE_INIT_ARGUMENTS`.
+* the **doorbell read-back** is storage of the last written value (the header
+ says RW); the firmware's read use of this register has **not** been observed.
+* the **command/status direction** is the documented one (host writes the
+ command queue, GSP writes the status queue), but no real GSP has yet been
+ observed consuming or producing through this model.
+* the **MSI-X/event path** by which the real GSP signals a status-queue write
+ is not modelled; replies are read by polling (`read_reply`).
+
+## 4. Files and API
+
+| File | Contents |
+|---|---|
+| `sim/host_traffic.py` | framing (`build_rpc_message`, `parse_rpc_message`, `build_queue_element`, `parse_queue_element`, `compute_message_checksum`), `MsgqQueue`, `SharedQueues`, `DoorbellModel`, `DoorbellBlock` (a `sim.devices` overlay), `HostTraffic`, `load_stimulus`, `replay_stimulus`, `run_demo`, CLI |
+| `tests/test_host_traffic.py` | 36 offline tests: framing/checksum, queue arithmetic + cross-link, doorbell model + guest instruction access, submit/dispatch/reply, determinism, replay (stimulus + capture doc), recorder events, demo + CLI |
+| `docs/51-HOST-TRAFFIC-LOOP.md` | this page |
+
+`HostTraffic` surface:
+
+* `submit(message)` / `submit_rpc(function, params, sequence=None)` — frame,
+ write one command element, advance `writePtr`, ring the doorbell (recorded).
+* `poll()` — GSP-side drain of the command ring through the handler table,
+ writing framed replies to the status ring; returns the dispatched count.
+ Bounded by construction (each iteration consumes one element).
+* `read_reply()` — host-side consumption of one status element.
+* `run_cycles(budget)` — run the attached `sim.machine.Machine` for a bounded
+ budget (cap 2M) and then `poll`; without a machine it only polls.
+* `transcript()` / `state()` / `close()` — deterministic transcript, queue and
+ doorbell state; `close()` writes `blobs/host-traffic.json` when a recorder
+ is attached.
+* `doorbell_block()` — a `DoorbellBlock` sharing the same `DoorbellModel`, so
+ the guest-visible register and the host model are one state. Attach it with
+ `machine.devices.add(host.doorbell_block())`; the narrow window overrides
+ the wider falcon window only for `0x110c00..0x110c3f`.
+
+Records: `greeneye-host-traffic-{demo,replay,log,reply}/1`. The transcript has
+no host timestamps and is byte-identical across runs; the optional
+`greeneye-session/1` recorder events (`host_traffic_config`, `doorbell`,
+`host_submit`, `host_dispatch`, `host_reply_read`, `host_replay_skip`,
+`host_transcript`) follow the standard event shape, where `t` is host-timed
+and non-semantic.
+
+## 5. How to run it
+
+```bash
+cd open-iov/debug/risc-v-gsp-rm-vm
+
+# Deterministic end-to-end demo (echo + status query + real-shaped 512-byte
+# GSP_RM_CONTROL; stub replies), with a recorded session:
+python3 -m sim.host_traffic demo --out dumps/host-traffic-demo
+
+# Replay a stimulus document (or a capture document / raw capture log):
+python3 -m sim.host_traffic replay --stimulus dumps/rm-stimulus.json --out dumps/host-traffic-replay
+python3 -m sim.host_traffic replay --stimulus capture.json --allow-truncated
+
+# Tests:
+python3 -m unittest tests.test_host_traffic -v
+```
+
+`replay` maps stimulus records as follows: `send` carries the wire function
+and payload directly; `control` and `alloc` records carry a named id and are
+mapped with the capture tool's constants (76 / 103) using the captured
+parameter block as the payload. That identity is an **assumption** until a
+capture with full send-side payloads exists (the current runbook helper also
+truncates payloads above 192 bytes; truncated messages are refused by default
+and skipped with `--allow-truncated`).
+
+## 6. Real vs stubbed — the honest table
+
+| Piece | State |
+|---|---|
+| RPC header + queue element framing, checksum | **real** (mirrored; drift-tested against `harness/rm_capture.py` constants) |
+| msgq header arithmetic + `SWAP_RX` cross-link | **real** (mirrored from the public queue header and `msgqTxCreate`/`msgqRxLink`) |
+| Doorbell register offset/width/access | **real** (public register header); read-back is storage of the last write, firmware read use unobserved |
+| Guest access to the doorbell window through `Machine` | **real** (device window + recorded store PC); exercised by instruction-level tests |
+| Command/status rings in simulated memory | **real layout**, fixture addresses and sizes |
+| Multi-element messages | **real split** (contiguous staged stream across ring slots, wrap-safe, checksum over the stream) |
+| Dispatcher (`echo`, `status_query`) | **stub** (ids 0x40/0x41 are not RM functions) |
+| Reply to real RM functions | **stub** `NV_ERR_NOT_SUPPORTED` — framed and transported exactly; not firmware behaviour |
+| Real firmware message loop | **not run** (B5 remains open) |
+| RM semantics (controls, allocs, objects, events) | **absent** |
+| MSI-X / GSP→host interrupt | **not modelled** (polling) |
+| Timing/cycles | **not claimed** (L2 absent; `run_cycles` is an instruction budget) |
+
+## 7. What this does NOT do yet (and the next step to B5)
+
+Not done: booting the real image and serving its message-loop poll; reading
+the real `GSP_ARGUMENTS_CACHED`/queue-init parameters; event/MSI-X delivery;
+any RM handler.
+
+Next step for B5, concretely (each point is bounded and session-recorded):
+
+1. Boot the real 570.144 image to B3 (`sim.gsp_boot`, 474k instructions,
+ stop at the message-loop port wait, `ecall` at `0x4018038`).
+2. Attach `host.doorbell_block()` to the boot registry before the run, so the
+ firmware's first queue-head access is served by the model and recorded,
+ and place the command/status rings at addresses the firmware is told
+ (stub: the fixture block; real: the addresses from a captured
+ `GSP_ARGUMENTS_CACHED`).
+3. Ring the doorbell from the host model and continue the machine with
+ `run_cycles` until it polls the rings; classify the first poll: which
+ address does it read (queue header? Tx/Rx header? entry?) and what
+ read-back semantics does it need.
+4. Compare the firmware's observed ring arithmetic against this module's
+ (field-by-field); where the real init used `MSGQ_FLAGS_SWAP_RX`, the
+ cross-link must already match; fix only with a recorded session pair
+ (docs/06 accuracy loop), never from a decompiler reading alone.
+5. Then replay one captured stimulus message with the real image and record
+ the reply/writes as the B5 session. Until a supervised capture exists,
+ this stops at step 3–4 with the stub dispatcher.
+
+The seam for step 3 already exists: `HostTraffic(machine=..., shared_base=...)`
+uses the machine's RAM for the rings and `run_cycles` steps the core; the
+doorbell block overlays the falcon window without touching
+`sim/devices/falcon.py` or the profile.
+
+## 8. Limits, deferrals and verification
+
+Limits: L0/L1 only; no capture has been consumed yet (no supervised runbook
+execution); control/alloc payload identity is an assumption; truncated
+payloads are refused or skipped, never padded; a stub reply is never a
+firmware claim; no real queue-init parameters.
+
+Deferred: real-firmware B4/B5 wiring (above), event/MSI-X delivery, RM-object
+state seeding (docs/49 build #2), golden matrices (#3).
+
+Verification of this wave: `python3 -m unittest tests.test_host_traffic -v`
+→ 36 tests green; full suite `make test` green; `demo` `ok: true` with all
+checks (`echo` payload round-trip, status magic, 512-byte control replay,
+framed stub verdict, one doorbell ring per submit, all replies read);
+`ruff check` clean on the new files. Two-run determinism is asserted by
+tests (transcript and replies byte-equal).
diff --git a/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md b/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md
new file mode 100644
index 0000000..1dc6203
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md
@@ -0,0 +1,297 @@
+# 52 — RM-object state seeding (`state_seed`) — in development
+
+> **Status: in development (2026-10-06, state-seeding wave).** Documents
+> `harness/state_seed.py`: a declarative **seed** of RM object state
+> (`greeneye-state-seed/1`) that is placed into the same native machine
+> `harness/rehost.py` uses, then run at a chosen entry under a bounded budget.
+> The tool records which seeded state the firmware reads, in order, and the
+> **first gate** it stops at: the address and the state the returning/faulting
+> path checked. This is the state-seeding fixture the campaign asked for:
+> rehost early-outs on missing objects; a seed supplies the
+> objects so the next blocker is named instead of guessed.
+>
+> Development-only: executes firmware on the simulator. Never touches a GPU,
+> kernel module, mdev, guest or network. No timing/cycle claim (L0/L1).
+>
+> **See also:** `46-SURGICAL-REHOST.md` (the runner this reuses),
+> `47-VALIDATION-PREDICATE-PROBE.md` (the predicate probe the mechanism proof
+> replays), `45-RM-RPC-CAPTURE-AND-REPLAY.md` (the capture whose objects a
+> seed can mirror), `06-ACCURACY.md` (fidelity levels), `39-RE-WORKFLOW-REVIEW.md`
+> (time-to-value rankings).
+
+## 1. Why it exists
+
+A cold rehost of a construct/promote entry on the 570.144 image bottoms out in
+the error family the live campaign observes (`0x40` invalid state, or an
+illegal instruction at `pc 0x0` after a NULL indirect call), because the RM
+object state the handler walks -- the client/device object, the channel or
+context object, the graphics instance and its memDesc table -- does not exist.
+The first rehost probes could only record the stop (`docs/46` §3); they could
+not say *which object* was missing, so the next step was still a guess.
+
+`state_seed.py` makes the missing state explicit:
+
+* a **seed** declares symbolic objects (clients, channels, graphics contexts,
+ memdescs/context buffers, generic RM objects), their fields, an allocator
+ that places them at chosen addresses, and pointer fields that reference each
+ other by name;
+* a **loader** compiles the seed into byte writes and register values and
+ applies them to the native machine through the same wiring `rehost.py`
+ uses;
+* a **runner** executes the entry under a bounded budget with watchpoints on
+ every seeded field and records the ordered first-touch state reads, then
+ reports the first gate: the last seeded state read before the stop plus the
+ stop reason.
+
+A field whose value is not known is left at the literal `"unknown"` marker:
+the seeder does not write it and the report lists it. A gate that reads an
+`unknown` field is a finding (the minimal object set is incomplete), not a
+seed bug.
+
+## 2. Schema `greeneye-state-seed/1`
+
+```json
+{
+ "format": "greeneye-state-seed/1",
+ "name": "construct-gate-minimal",
+ "entry": "0x52a70ec",
+ "allocator": {"base": "0x10010000", "align": "0x1000"},
+ "stack": "0x1003e000",
+ "registers": {"a0": "@client", "a1": "@channel", "a2": "0x1003f000"},
+ "stubs": [{"name": "engine_stub", "return": 0}],
+ "objects": [
+ {"name": "client", "type": "client", "size": "0x2000",
+ "fields": {"0x1de8": "@engine_node", "0x1dd8": {"value": 0, "size": 8}}},
+ {"name": "engine_node", "type": "rm_object", "size": "0x1000",
+ "fields": {"0x600": "unknown"}}
+ ],
+ "memory": [{"pa": "0x200000", "u32": 7}],
+ "outcomes": {"0x5290ebc": "refuse-fecs-slot-exists-0x1f"},
+ "watch": [{"pa": "0x200000", "size": 4, "label": "raw-state"}],
+ "reads": [{"pa": "0x200000", "size": 4, "label": "raw-after"}],
+ "provenance": {"source": "construct-path disassembly", "date": "2026-10-06"},
+ "notes": ["minimal object set; +0x600 callback unknown"]
+}
+```
+
+Object `type` is one of `client`, `channel`, `graphics_context` (an alias of
+`channel`), `graphics_instance`, `memdesc`, `ctx_buffer` or the generic
+`rm_object` (no field map; every field is explicit).
+
+Field value forms (object `fields`):
+| Form | Meaning |
+|---|---|
+| number / numeric string | raw value; the width is the field-map width, else 8 bytes |
+| `{"value": v, "size": n}` | raw value with an explicit width (1/2/4/8) |
+| `"@name"` / `"@name+0xNN"` | pointer to another object's or stub's base (+offset) |
+| `"unknown"` / `{"unknown": true}` | explicit unknown: not written, reported in `unknown_fields` |
+| `{"ptr": "@name+0xNN"}` | pointer form in a dict (same as the string form) |
+
+Object placement (the allocator/pointer story):
+
+* objects without `pa` (or its alias `va`) are placed in declaration order
+ from `allocator.base`, each aligned to `align` (object-level `align`
+ overrides) and advanced by `size`;
+* every object name becomes a symbol, so `"@client+0x10"` resolves to the
+ object's address plus `0x10` when the writes are compiled;
+* stubs are the same: a named stub without `addr` is allocated after the
+ objects and can be used as a pointer target (`"@engine_stub"`);
+* in this machine memory is flat and the MPU is inactive, so PA and VA are
+ identical; `va` is accepted as an alias of `pa` and recorded as such.
+
+Other top-level keys:
+
+| Key | Meaning |
+|---|---|
+| `entry` | code entry (required unless `registers.pc` is given) |
+| `registers` | any `x0..x31`/ABI register name; `pc` is an entry alias, not a register |
+| `stack` | initial `sp` when the seed does not set `sp` explicitly |
+| `memory` | raw page entries: `pa` (or `addr`) plus one of `hex/u64/u32/u16/u8` |
+| `stubs` | `{addr?, name?, return}` -> a `lui/addi/jalr ra` returning `return` |
+| `outcomes` | address -> label; reaching one stops the run as `outcome` |
+| `watch` | extra watch ranges `{pa/addr, size, label}` for the state-read log |
+| `reads` | post-run reads included in the report |
+| `provenance`, `notes` | free-form evidence/provenance carried into the report |
+
+## 3. Runner and report `greeneye-state-seed-report/1`
+
+```bash
+python3 harness/state_seed.py demo
+python3 harness/state_seed.py emit --kind fecs --out /tmp/fecs.json
+python3 harness/state_seed.py emit --kind target-minimal --out /tmp/target.json
+python3 harness/state_seed.py emit --kind target-advanced --out /tmp/target-adv.json
+python3 harness/state_seed.py validate --seed /tmp/target.json
+python3 harness/state_seed.py run --seed /tmp/target.json --out dumps/state-seed-target
+```
+
+`run` loads the seed, boots the native machine exactly like `rehost` (device
+registry from the profile, boot shim, libos syscall model, lenient MMIO),
+applies the plan, installs a return trampoline, watchpoints every seeded field
+and runs under `--budget`/`--timeout`. It stops on `outcome` (a labelled
+address), `return` (the trampoline), `fault`, unhandled `ecall`, `timeout` or
+`budget`, and records a `greeneye-session/1` directory plus
+`state-seed-report.json`.
+
+Report highlights:
+
+* `objects[]`: the compiled layout (name, type, PA, size);
+* `unknown_fields[]`: every field the seed marked `unknown`, with its mapped
+ role where one exists;
+* `state_reads[]`: the ordered first touch of each watched field
+ (`pc`, `addr`, `label`, `value`, `width`) -- the state the path checked;
+* `gate`: for a stop, the last read before it and the verdict:
+
+| gate kind | meaning |
+|---|---|
+| `null_indirect_call` | the stop is `illegal @ pc 0x0` and the last read was a 0 function-pointer field; `pc` is the load site, `addr`/`label` the pointer read |
+| `fault` | a fault with a different last read |
+| `<status>_before_gate` | timeout/budget before any early-out; last read shown |
+
+## 4. Known object field map (derived so far)
+
+Status `known` = read by a documented path; `unknown` = the offset is real
+(from the same disassembly) but its semantics/value are not established.
+Every entry is numeric evidence, no source paths.
+
+| Type | Offset | Width | Status | Role | Evidence |
+|---|---|---|---|---|---|
+| client | `0x1be0` | 8 | known | graphics-instance pointer array base (index `0x37c + idx`) | promote predicate (`docs/47`) |
+| client | `0x1dd8` | 8 | known | engine-context pointer; `0` selects the no-engine join | construct-path disassembly (this doc) |
+| client | `0x1de8` | 8 | known | sub-device object; `+0x600` callback is invoked | construct-path disassembly |
+| client | `0x1aa8` | 8 | unknown | sub-object consumed later (`+0x24c` callback) | construct-path disassembly |
+| client | `0x1cc8` | 8 | unknown | sub-object consumed later (`+0x150`, `+0x1d0`) | construct-path disassembly |
+| client | `0x1e48` | 8 | unknown | sub-object on the no-engine path (`+0x110` callback) | construct-path disassembly |
+| channel / context | `0x0d28` | 4 | known | graphics-instance index (u32) | promote predicate (`docs/47`) |
+| channel / context | `0x0898` | 8 | unknown | callback invoked inside `0x5285aa8` | construct-path disassembly |
+| channel / context | `0x0d01` | 1 | unknown | u8 flag: `0` takes the panic path on a failed lookup | construct-path disassembly |
+| channel / context | `0x0cd1` | 1 | unknown | u8 flag before the promote calls | construct-path disassembly |
+| channel / context | `0x0648` | 8 | unknown | callback on the invalid-state path | construct-path disassembly |
+| channel / context | `0x03b8` | 8 | unknown | identity marker compared across objects | construct-path disassembly |
+| channel / context | `0x00ee` | 8 | unknown | sub-object (`+0xe8` read as an index/gfid) | construct-path disassembly |
+| graphics_instance | `0x0190` | 8 | known | pointer to the per-buffer memDesc slot table | promote predicate (`docs/47`) |
+| graphics_instance | `0x0161` | 1 | unknown | u8 flag gating the `0x529bd44` call | construct-path disassembly |
+| graphics_instance | `0x03b8` | 8 | unknown | identity marker compared across objects | construct-path disassembly |
+| memdesc | `0x0000`, `0x0008` | 8 | unknown | slot value semantics outside the promote predicate | promote predicate only checks non-NULL |
+| ctx_buffer | `0x0000` | 8 | unknown | context-buffer (memdesc) object; field semantics unknown | promote predicate only checks non-NULL |
+
+## 5. What the demonstration proves (2026-10-06)
+
+`python3 harness/state_seed.py demo` runs three things, all offline,
+deterministic and bounded:
+
+1. **Mechanism proof on a path that needs no RM objects.** The
+ `fecs-promote` seed replays the `docs/47` predicate through this loader:
+ `FECS init, slot non-NULL` -> `refuse-fecs-slot-exists-0x1f` (stop
+ `0x5290ebc`, 75 instructions) and `slot NULL` -> `accept-commit-path`
+ (`0x5290cd0`). The only difference between the two seeds is the seeded slot
+ value, so the seed data is driving the firmware's own predicate. Two runs
+ of the same seed are identical (status, outcome, instructions, reads).
+2. **First-gate attempt on `FUN_ram_052a70ec` (entry `0x52a70ec`).** The
+ minimal seed places a client, a channel/context, a graphics instance and a
+ memdesc table, sets `a0`/`a1`/`a2`, and walks the real prologue. The path
+ reads, in order: `client+0x1dd8`, `client+0x1cc8`, `client+0x1aa8`,
+ `client+0x1de8`, `client+0x1e48`, then (no-engine join) `channel+0xd28`,
+ then the gate: at `0x52a724c` it loads the `client+0x1de8` sub-object's
+ `+0x600` callback (=0, marked `unknown`) and calls it at `0x52a7268`; the
+ call faults `illegal @ pc 0x0` after 106 instructions. **First gate:
+ `engine_node+0x600` (the indirect call target), read at `0x52a724c`.** The
+ report's `unknown_fields` names it, and the ordered read list shows the
+ client fields the seed satisfied.
+3. **Gate advance.** The same seed with `+0x600` pointed at a recorded
+ return-0 stub walks past the first gate and stops at the next one: inside
+ `0x5285aa8`, `channel+0x898` is loaded at `0x5285af4` and called at
+ `0x5285b0c`; the target is 0 (marked `unknown`), fault after 143
+ instructions. This is the loop the fixture is for: seed the named state,
+ re-run, name the next gate.
+
+Timing on this machine (same environment for all rows): FECS single run
+0.08 s, construct single run 0.07 s, whole `demo` 0.8 s. Zero hardware
+cycles.
+
+## 6. Admission record
+
+Per the project admission rule (`open-iov/docs/eng-ops.md` §2), all five
+fields:
+
+| Field | Value |
+|---|---|
+| Scenario | dynamic-RE loop: run state-gated RM firmware paths (construct/promote family) off-hardware with crafted client/channel/context objects so the first missing state is named without a hardware cycle; trigger = a wall where rehost stops with no state attribution. |
+| Measurable outcome | per seed: one `greeneye-state-seed-report/1` with layout, `unknown_fields[]`, ordered `state_reads[]` and a `gate` (address + state checked); sessions per attempt; wall time per run. |
+| Baseline | cold rehost of the same entry with no objects (docs/46 §3): stop with `0x40`/illegal-at-0 and no attribution, so the next object is a guess; the alternative for an attributed answer is a supervised construct cycle ([live campaign log PART 96 Add. D, 2026-10-06]: `-110` + NOCAT, FLR recovery). |
+| Evaluation threshold | ≥1 wall-class question answered with the gate identified in ≤1 session per attempt, two-run deterministic, zero hardware cycles; seed loop ≤2 min per object. Measured: 0.07–0.08 s per run, 0.8 s demo. |
+| Failure signal | a seeded run cannot reach any gate (faults in the loader itself), or the same gate reproduces with two different seeds and the read state cannot be attributed -> record the gap here and fall back to static + supervised capture. |
+
+## 7. Proven vs hypothesised
+
+**Proven by this wave (sessions/reports are the evidence):**
+
+* the loader places objects, pointers, raw memory, stubs and registers
+ byte-exact and deterministically (unit tests and reports);
+* the FECS predicate verdicts are reproduced through the seed with the slot
+ value as the only variable;
+* the construct prologue's ordered state reads are real (watchpoint
+ first-touch on the seeded fields), and the first gate for the minimal seed
+ is the `client+0x1de8` -> `+0x600` indirect call;
+* stubbing that callback advances execution to the `channel+0x898` gate,
+ which proves the fixture can iterate the object set.
+
+**Hypothesised / not claimed:**
+
+* the object **type names** `client`, `channel`, `graphics_context` are
+ inferred from the promote predicate and the construct call shape, not from a
+ live RM object dump;
+* fields marked `unknown` in §4 have no established semantics, and their
+ values in the demo seeds are 0 (or a stub), not real object state;
+* the real function behind `engine_node+0x600` and `channel+0x898` is not
+ known; the return-0 stubs are synthetic probes, never product code;
+* no timing, scheduling or engine-side behaviour is claimed (L0/L1 only).
+
+## 8. Minimal object set still missing
+
+To walk further than the second gate, the fixture still needs (in order):
+
+1. the **`engine_node` object** (`client+0x1de8`) with a real or recorded
+ `+0x600` callback (first gate);
+2. the **`channel+0x898` callback** reached inside `0x5285aa8` (second gate);
+3. the **`obj_1cc8`** object with `+0x150` (a valid object pointer) and
+ `+0x1d0` flags, and **`obj_1aa8`** with its `+0x24c` callback (next gates
+ after the lookup);
+4. **`obj_1e48`** with its `+0x110` callback on the no-engine path;
+5. **memdesc field semantics** (`+0x0`, `+0x8`, ...) for paths that actually
+ read a memdesc rather than only checking non-NULL;
+6. the **`channel+0x0d01` / `+0x0cd1` / `+0x0648` / `+0x03b8` / `+0x00ee`**
+ fields once a path reaches the lookup-result and invalid-state branches.
+
+The function-pointer fields are the load-bearing gap: they must come from a
+live/B4 object dump or a capture, because a seeder must not invent code
+addresses. Recorded return-0 stubs are the bounded way to cross a gate and
+name the next one, and are always labelled as stubs in the report.
+
+## 9. How to author a new seed
+
+1. Start from the target's entry and the known field map (§4); write the
+ smallest object set the entry's prologue reads, marking everything else
+ `unknown`.
+2. Give each object a name; wire pointer fields with `"@name"`; set the entry
+ registers (`a0`/`a1`/...) and `stack`.
+3. `validate` the seed to see the layout and the `unknown_fields` list; check
+ nothing overlaps and every pointer resolved.
+4. `run` it (add `--out` to keep the session + report). Read `state_reads[]`
+ to confirm each object field was actually consumed, then read `gate` to get
+ the blocker.
+5. If the gate is a callback, re-run with that field pointed at a named stub
+ to reveal the next gate; repeat until the required object cannot be
+ authored without a capture. The remaining `unknown_fields` are the
+ minimal missing object set for the next session.
+
+## 10. Limits
+
+* Not a boot and not an RM model: only the seeded state exists; the report
+ lists it and every limit inline.
+* A fault is the first missing state observed, not a product defect; a
+ timeout/budget is not a verdict.
+* Watchpoints report the first touch of each watched field (repeated accesses
+ are not replayed); the ordered first-touch list is the gate evidence.
+* PA = VA here (flat memory, inactive MPU); a seed that needs a translated
+ view must say so explicitly.
+* No timing/cycle/security claim; L0/L1 fidelity only.
diff --git a/debug/risc-v-gsp-rm-vm/docs/53-FIRMWARE-OBSERVABILITY-WORKFLOW.md b/debug/risc-v-gsp-rm-vm/docs/53-FIRMWARE-OBSERVABILITY-WORKFLOW.md
new file mode 100644
index 0000000..4106244
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/53-FIRMWARE-OBSERVABILITY-WORKFLOW.md
@@ -0,0 +1,245 @@
+# Firmware Observability Workflow: using the emulator in the present problem space
+
+> **Status: 2026-10-06.** This guide says where the emulator belongs in the
+> Open-IOV engineering loop, what it can and cannot answer, and exactly which
+> commands to run for each question. It introduces four tools added this
+> session (`state_decode`, `log_decode`, `server_snapshot`, `msgq_attach`) and
+> wires them to the existing ones (`valprobe`, `rehost`, `state_seed`,
+> `launch_audit`, `gmmu_walk`, `host_traffic`, `rm_capture`, `contracts`,
+> `triage`, `version_diff`). It is offline-only: no GPU, module, guest or
+> network. Fidelity is L0/L1 (instruction/memory); never quote timing or
+> device-environment results from it.
+>
+> Index registration in `docs/00-OVERVIEW.md`/`README.md` is intentionally
+> left to the in-flight docs edit owned by the simulator workstream.
+
+---
+
+## 1. The layer map: what belongs where
+
+The project sits on three layers; each layer has one honest oracle.
+
+| Layer | What it decides | Oracle | Emulator role |
+|---|---|---|---|
+| **Firmware / server** (GSP-RM: controls, objects, context commit, logs) | "What will the firmware do with this control/state?" | the emulator (predicates, rehosts, seeded handlers, message loop) + a live log ring | first-class: dumps, probes, replay |
+| **Front-end / engine** (PBDMA fetch, inline-QMD install, GR dispatch, SMs) | "Does the engine execute this work?" | **silicon only** — the emulator has no PBDMA/GR model by design | evidence preparation and diffing only; *never* a verdict |
+| **Guest / host software** | "Did the guest see correct completion?" | the live guest plus the acceptance strings | offline decode of what happened |
+
+**Consequence:** the emulator is the right tool for *server decisions* and
+*evidence reduction*; it is the wrong tool for *engine dispatch*. When a wall
+is engine-side, the emulator's job is to (a) exonerate the server half and
+(b) reduce the live search space, not to declare a fix.
+
+The **differential loop** that makes this practical:
+
+```
+ live cycle / captured evidence ──► snapshot + decode + log join
+ ▲ │
+ │ diff / query
+ │ ▼
+ one hardware test ◄── ranked candidates ◄── emulator probes/rehost (server half)
+```
+
+---
+
+## 2. The four new tools
+
+All four are stdlib-only, deterministic, session-log friendly, and each has
+tests in `tests/` (84 tests across the four as of 2026-10-06). Run everything
+from the tool root.
+
+### 2.1 `harness/state_decode.py` — turn channel state into named fields
+
+Decodes INSTANCE / RAMFC / context-buffer words into field tables and diffs
+two snapshots. The bundled table is deliberately small and evidence-only
+(INSTANCE magic and GPFIFO VA; RAMFC_TARGET bits 16/17 = ENG/CE context valid);
+everything else comes from an extensible `--map` (`greeneye-state-map/1`).
+
+```bash
+# Decode a captured ladder dump (finds INSTANCE/RAMFC blocks automatically)
+python3 harness/state_decode.py decode --input <evidence>/06-ladder-<run>.txt --json out.json
+
+# Diff two decoded states (or two raw inputs) -> greeneye-state-diff/1
+python3 harness/state_decode.py diff a.json b.json
+```
+
+Use it whenever a run's verdict is "silence": the field table shows context
+validity bits and the words that correlate with success, and the diff shows
+what a cycle changed.
+
+### 2.2 `harness/log_decode.py` — the server's stderr, readable
+
+Normalizes the two real dump shapes (raw ring dumps with `LOGINIT-raw:`/
+`LOGRM-raw:` headers and hexdump rows; textual NOCAT/record lines incl.
+continuation call stacks and aggregate entries) into `greeneye-log-decode/1`,
+with histograms, filters, and optional nearest-symbol resolution.
+
+```bash
+python3 harness/log_decode.py decode <evidence>/21-logdump-<run>.txt \
+ --rec-type 5 --json log.json # just the asserts
+python3 harness/log_decode.py decode <log> --symbols symbols.json
+```
+
+Use it to read *why* the server refused or asserted: recover addresses become
+function names, and record histograms expose repeated failure signatures.
+
+### 2.3 `harness/server_snapshot.py` — canonical server-state snapshot + diff
+
+Collects chunk dumps and record lines from evidence directories/files into one
+`greeneye-server-snapshot/1` JSON (labels → words + records), and diffs two
+snapshots into `greeneye-server-snapshot-diff/1`.
+
+```bash
+python3 harness/server_snapshot.py collect --file <evidence>/06-ladder-a.txt \
+ --created 2026-10-06T00:00:00Z --json snap-a.json
+python3 harness/server_snapshot.py collect --file <evidence>/06-ladder-b.txt \
+ --created 2026-10-06T00:00:00Z --json snap-b.json
+python3 harness/server_snapshot.py diff --a snap-a.json --b snap-b.json
+```
+
+This is the A/B instrument for the differential loop: two cycles, one diff,
+sorted per-chunk added/removed/changed words. Record differences (e.g. dmesg
+timestamps) are visible and honest rather than silently merged.
+
+Note: `20-gsp-phys-state-*.txt` is an aggregate key:value state, not a memory
+chunk; it yields no chunks by design. Pair it with `state_decode` only after
+converting its contents to a chunk form.
+
+### 2.4 `sim/msgq_attach.py` — boot the real firmware to its message loop
+
+Implements docs/51 §7 steps 1–3: boots the real 570.144 image to the B3
+message-loop port wait, attaches the host doorbell/queue rings before the run,
+rings the doorbell, continues under a bounded budget, and classifies the first
+poll (`greeneye-msgq-attach/1`). It reports honestly: loop reached or first
+divergence, with the reason.
+
+```bash
+python3 -m sim.msgq_attach run --doorbell --json attach.json --record <sess-dir>
+python3 -m sim.msgq_attach demo # bounded run + honest skip if no image
+```
+
+Measured today: **B3 reached** (~474k instructions), doorbell write performed
+and checksum-verified; the **libos port model** then completes the
+external-interrupt receive handoff (`wait @ 0x4019df4: completed shuttle 10
+(extintr) size=0`), and the loop task's next syscall is `TASK_STATE_QUERY` at
+`0x401a46c` — the libos trap/report path. **First ring poll: still none**, and
+the report says exactly why (trap state for the query; the command-queue
+consumer task is never scheduled in this single-hart model; the
+`GSP_ARGUMENTS_CACHED` address handoff is not delivered). No fabricated poll;
+two runs are byte-identical.
+
+---
+
+## 3. Run-to-the-wall procedure (reproducible)
+
+This is the exact sequence to bring the emulator to the current frontier and
+produce its verdict on what it *can* decide.
+
+```bash
+cd open-iov/debug/risc-v-gsp-rm-vm
+EVID=<our evidence directory>
+
+# 1. Server state at the current runs, decoded
+python3 harness/state_decode.py decode --input $EVID/06-ladder-<run>.txt
+
+# 2. Server log records around the promote/launch
+python3 harness/log_decode.py decode $EVID/21-logdump-<run>.txt
+
+# 3. A/B the two runs that differ in launch capture
+python3 harness/server_snapshot.py collect --file $EVID/06-ladder-<a>.txt --json snap-a.json
+python3 harness/server_snapshot.py collect --file $EVID/06-ladder-<b>.txt --json snap-b.json
+python3 harness/server_snapshot.py diff --a snap-a.json --b snap-b.json
+
+# 4. Real image to the message loop (bounded; honest boundary report)
+python3 -m sim.msgq_attach run --doorbell
+```
+
+What today's run of this procedure established:
+
+* INSTANCE decodes cleanly (magic `0xface`, GPFIFO VA `0x100000`); the
+ `0x00030000` marker appears at instance words `0x4c`/`0xac` — the same
+ success marker the live campaign reads, now a named field candidate rather
+ than an unexplained word. The RAMFC chunk's own `RAMFC_TARGET` is zero,
+ consistent with the server programming target bits through the instance map.
+* The log dump decodes to 122 records with histograms and frame addresses —
+ the server's assert stream is now machine-readable and symbolizable.
+* The two ladder runs diff cleanly at word level — the A/B instrument works.
+* The real firmware boots to its message-loop wait deterministically, and the
+ first thing it needs is **libos port delivery**, not more MMIO.
+
+**Emulator verdict on the present wall:** the wall is the *forwarded launch's
+inline-descriptor install* (front-end/engine physics). The emulator can and did
+exonerate the server half (context validity markers, control acceptance,
+mapping), but it **cannot decide engine dispatch** and will not be asked to.
+The candidate fixes remain the live-lane tests (restore the proven stream
+shape, add the descriptor-virtualization control, relocate the prelude) and,
+if those fail, the qmdwin variant — all hardware-side.
+
+---
+
+## 4. The full tool chooser (existing + new)
+
+| Question you actually have | Tool | Kind |
+|---|---|---|
+| What fields does this chunk hold? What changed between runs? | `state_decode` | new |
+| Why did the firmware assert/refuse? What do its records say? | `log_decode` | new |
+| Give me one canonical state snapshot per cycle, and diff it | `server_snapshot` | new |
+| Does the real image reach its message loop? What does it poll first? | `msgq_attach` | new |
+| Which entry/control forms does the firmware accept, on which fields? | `valprobe` | existing |
+| Run one whole handler with crafted state; first missing state is the finding | `rehost` + `state_seed` | existing |
+| Where would the engine read/write each launch leg? | `launch_audit` (+ `gmmu_walk`) | existing |
+| Walk a captured page table through a VA | `gmmu_walk` | existing |
+| Replay captured host traffic against the transport model | `host_traffic` | existing |
+| Decode/compare real-client RPC captures | `rm_capture` | existing |
+| Re-validate proven tools before a cycle | `contracts` / `run_contracts` | existing |
+| New image triage / version delta | `triage` / `version_diff` | existing |
+
+---
+
+## 5. Next emulation iterations (the continuation loop)
+
+Ordered by what each unblocks; each is bounded and session-recorded.
+
+1. **libos port delivery — landed (partial):** the port-role model
+ (`sim/libos_port.py`) now carries the doorbell wake through the
+ external-interrupt receive contract. Measured next blockers: the
+ `TASK_STATE_QUERY` trap state (`xcause`/`xepc` from the wait point) and
+ scheduling the command-queue consumer task (the single-hart model runs one
+ task). Success for this iteration = the consumer's first ring read
+ (`addr/width/value`), not a fabricated poll.
+2. **`GSP_ARGUMENTS_CACHED` / queue-init parsing**: relocate the rings to the
+ addresses the host actually gives the firmware instead of the fixture block.
+3. **CSB/TRANSCOM read-back semantics** for the loop's poll window.
+4. **One served RPC**: dispatcher stub returning a real captured reply
+ (`rm_capture` stimulus), recorded as the B5 session.
+5. **MSI-X/event delivery and IRQ arming** so replies can be pushed, not only
+ polled.
+6. **Front-end/engine model — decision gate, not a default.** Reproducing the
+ inline-descriptor install (PBDMA method execution, IQ2M) would let the
+ emulator rank engine-side fixes offline, but the project has explicitly
+ excluded an engine model (surface map §6; it is silicon physics, and offline
+ engine claims are forbidden). Treat this as a gated, admitted research item
+ only if the live lane's next tests leave an unresolved ambiguity that only a
+ model could separate — never as a routine build.
+
+---
+
+## 6. Fidelity, honesty and safety rules
+
+* Emulator outputs are L0/L1: instruction and memory semantics of the firmware.
+ No timing (L2), no device environment (L3), no silicon behaviour.
+* Silence is not health: a missing fault does not mean success. Prefer
+ engine-authored observables; front-end-authored signals are not proof.
+* Every run keeps its session/report; a claim without a session is an anecdote.
+* Offline only: never a GPU, module, mdev, guest, network or capture from this
+ tree. Injected code is development-only and stays here.
+* The differential loop is only as good as the captured evidence; a verdict on
+ a partial capture names the next page (or field) to capture, not a cause.
+
+---
+
+## 7. Change log
+
+| Date | Change |
+|---|---|
+| 2026-10-06 | Created with the four new tools (`state_decode`, `log_decode`, `server_snapshot`, `msgq_attach`), the layer map, the run-to-the-wall procedure and the continuation loop. |