1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
|
# 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. |
|