diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-06 19:28:39 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-06 19:28:39 -0400 |
| commit | cd1f2528e8243fed3ce80d5170ef93928e4e7284 (patch) | |
| tree | 208c326a109347baf6c04917dca23ef60c73eabc /debug/risc-v-gsp-rm-vm/docs | |
| parent | 595dcded851dc45d78cc76280137100ea8b3d0ad (diff) | |
| download | open-iov-cd1f2528e8243fed3ce80d5170ef93928e4e7284.tar.gz open-iov-cd1f2528e8243fed3ce80d5170ef93928e4e7284.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')
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. | |
