diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md | 337 |
1 files changed, 337 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md b/debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md new file mode 100644 index 0000000..d47b8ea --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/25-VALUE-VALIDATION.md @@ -0,0 +1,337 @@ +# 25 — Value validation: measurable time-to-value evidence + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See +> [00-OVERVIEW](00-OVERVIEW.md) for status. Every number in this document is +> either **measured** on the bundled demo analog (recorded in +> `harness/value_demos/artifacts/`) or explicitly labelled a **projection** +> for the real case. No demo claims a real-GSP result the simulator cannot +> reach; device-model-blocked scenarios say so in their result. + +## 1. Why this document exists + +green-eye is only worth its maintenance if it turns real firmware questions +into answers faster than static analysis and supervised hardware cycles. The +project handoffs record those questions and their costs in wall-clock and +cycles: the 0x59 RM-init chase (PART 91, 2026-10-01), the 20 GR counters +(PARTs 46–64, 2026-09-24…29), the GPFIFO GET fault (PART 91 ADDENDUM 9), the +P6 service wiring (PART 92 ADDENDA 5–11, 2026-10-02), and the version pairing +question (570.144 vs 570.211). This document maps each of them to a runnable +demonstration under [`harness/value_demos/`](../harness/value_demos/), states +the baseline cost from the handoffs, and measures the green-eye path. + +## 2. Methodology + +### 2.1 What is measured, what is projected + +| Class | Meaning | Where it appears | +|---|---|---| +| **Measured (analog)** | Wall-clock of the green-eye path on the bundled `uproc_demo` firmware, native RV64 interpreter or QEMU; answers, counts, session bytes, trace slowdown | `result.json` per demo, `artifacts/results.json` | +| **Recorded (real baseline)** | Cost actually paid on the physical path, as recorded in the handoffs (dates, cycles, PARTs, wall spans) | `real_case.static_baseline` in each `result.json`, §5 below | +| **Projected** | What the same tool path would save on the real firmware once H2.2 executes it; labelled `projected` | `real_case.projection`; never presented as a result | +| **Blocked** | Real firmware access requires the H2.2 QEMU device model; `blocked_on_device_model: true` | every real `.fwimage` scenario here | + +Fidelity is exactly docs/06: L0 instruction semantics and L1 memory behaviour +are exercised; **L2 time/concurrency is not modelled** and **L3 device +environment is absent**. A green-eye observation is evidence about the +firmware, and anything that would change `open-iov-vfio` is escalated to a +physical capture before the product changes (docs/06 §6, docs/14 §7). + +### 2.2 Run contract + +* Each demo is a stdlib-only script run from the tool root; it prints the + question, commands, answer, evidence and elapsed time, then one + `RESULT_JSON: {...}` line. +* `run_all.py` executes each demo in a subprocess with a hard timeout, + aggregates `artifacts/results.json`, and enforces the suite budget + (per-demo ≤ 45 s, suite ≤ 180 s). +* Demos are deterministic: native sessions use virtual time (instruction + count), stimuli are fixed, and the version-diff patch site is verified + unique before use. Artifacts contain no host paths beyond the repo. + +### 2.3 Scenario selection and “why not edge cases” + +Every class below recurs across multiple handoff PARTs and sits on the +critical path to `vectorAdd`. The mapping is direct: + +| Demo | Recurs in | Why it is on the vectorAdd path | +|---|---|---| +| s01 reply consumer | PART 91 cycles 2–13; status-queue decode cycles 22–25 | RM init must complete (`0x1001`, `rpc_result=0`) before any RPC; a generic failure hides the first error | +| s02 counter publication | PARTs 46–64 | `cuCtxCreate` completes on per-channel counters; 4/20 blocked the client | +| s03 unknown commands | every new image; docs/19 | control inventory (M1) is the input surface the emulation must handle | +| s04 run-until | PART 91 ADDENDUM 9 (cycles 18–25) | each ladder rung (VAS, PDB, channel, token, GPFIFO, semaphore) is a reachability question | +| s05 snapshot diff | PART 91 ADDENDUM 6/7 (identity, static config) | static-config fields feed guest wiring; a zeroed field kept the device-id read at zero | +| s06 inject service | PART 92 ADDENDA 5–11 (P6a–P6c) | guest-originated work executing on the engine is the shape of the load | +| s07 version diff | PART 91 cycles 2–3; every GSP release | the emulation must track firmware changes without regressing | +| s08 mutation triage | docs/18 R5; docs/19 §3.4; PARTs 3–9 | fail-closed rules come from error paths, not happy paths | +| s09 Ghidra address | PART 91 ADDENDUM 9 cycle 24 | the GPFIFO GET fix was accepted only after the runtime fault PC existed | +| s10 evidence pack | docs/14 §6; every milestone | a finding without sessions/hashes/M1–M6 is not actionable | + +None of these is a bizarre edge case: they are the same observe → decode → +change loop the project runs on every firmware version, and each one is +cited more than once in the handoffs. + +## 3. Scenario → tool → baseline → measured + +Measured on the reference host (CPython 3.12); numbers regenerate with +`python3 harness/value_demos/run_all.py`. Times below are from the +`run_all` aggregate (~18.4 s total). + +| # | Real scenario (source, date) | Green-eye tool | Static/Ghidra-only baseline (recorded) | Analog (measured) | Real-case projection | +|---|---|---|---|---|---| +| s01 | 0x59 RM-init chase; fn72/fn73 (PART 91 + ADDENDA 2–7, 2026-10-01) | hook dispatcher, watch `result`, decode reply | cycles 2–13, ~11:40→16:52 ≈ 5 h 10 m; 10+ supervised boots (~5–6 min each); reply was generic `0x59` | **1.14 s**; 3/3 commands consumed and decoded; FAULT traps before the reply store | one hook + one watchpoint per command once H2.2 boots the image (blocked) | +| s02 | 20 GR per-channel counters (PARTs 46–64, 2026-09-24…29) | watch publication field, per-slot readback | ~5 days; PART 49 “only 4 of 20 counters ever moved”; ~2–3 s pin vs ~2 s client deadline | **1.34 s**; 20/20 slots published, 28 stores, store PCs captured | per-channel page watchpoints on real counter pages (blocked) | +| s03 | unknown control IDs / injected slots (docs/19, 2026-10-02) | sweep + dispatcher/handler hooks + classify | 189 real probes in ~36 s; static-only requires the indirect table per image | **5.96 s** for 24 IDs: 6 known, 8 injected-slot (aliases shown), 10 default | RM control/method sweep once H1.3/H2.2 exposes the table (blocked) | +| s04 | RM-client ladder reachability (PART 91 ADDENDUM 9, 2026-10-01) | `run_until`, trigger, direct `call` | cycles 18–25 = 19:12→20:53 ≈ 1 h 41 m, 8 supervised boots | **0.24 s**; natural hit / timeout / trigger / direct call | every ladder rung becomes a probe spec (blocked) | +| s05 | fn72 identity + 1656-B static config (PART 91 ADDENDUM 6/7, 2026-10-01) | before/after snapshot + schema diff | malformed fields suspected statically; acceptance required a live boot per iteration (10 NOCAT cycles) | **0.50 s**; 6 named fields changed, 7 byte runs fully attributed | same diff over real RM control/reply pages (blocked) | +| s06 | P6 service wiring / guest CE copies (PART 92 ADDENDA 5–11, 2026-10-02) | service injection SDK, both invocation routes | kernel rebuild + supervised cycle (~5–8 min) per rung; P6a→P6c spanned ~02:20–08:30 | **0.63 s**; blob loaded/registered; remote and mailbox results agree | RM service registry (M5) once H2.2 exists (blocked) | +| s07 | 570.144 vs 570.211 pairing (PART 91 cycles 2–3, 2026-10-01) | same stimulus on two builds, `compare_sessions` | one full boot (~5–6 min) per version plus manual log comparison | **1.14 s**; only the patched reply field moved; decision-table row = “prioritise” | real versions triaged on re-hosted firmware; symbols/sessions are the artifact (blocked for the raw image) | +| s08 | mutation / fail-closed corpus (R5; docs/18 §2; docs/19 §3.4) | seeded one-field mutations, signal classification | hand-derived asserts/NOCAT across PARTs 3–9; a 64-iteration QEMU run is the tracked harness baseline | **1.82 s**; 7 mutations → {ok:4, fault:2, no-progress:1}, all expectations matched | real controls land in the same harness (H1.3 surface, blocked) | +| s09 | GPFIFO GET fault `0x40000` vs `0x101000` (PART 91 ADDENDUM 9 cycle 24) | Ghidra TSV + bias + raw-address hook + `export_hits` | static hypothesis unproven; the decoded fault ended 8 supervised cycles (~1 h 41 m) | **0.72 s**; biased address resolves, hook fires at the exact PC, TSV annotated | names.tsv + bias loop once the image executes (blocked) | +| s10 | milestone evidence pack (docs/14 §6; cycles 11–13, 2026-10-01) | one recorded session → analyze/compare/hits/manifest/M1–M6 | hashes, sessions, logs and compare gathered by hand across boots | **4.92 s**; 12,276-byte session, A/B deterministic, all artifacts + overhead numbers | same pack against real sessions (blocked) | + +## 4. Exact commands (per demo) + +All from `open-iov/debug/risc-v-gsp-rm-vm`. `VALUE_DEMO_BACKEND` selects the +backend (`native` default, `qemu` where noted). + +```bash +# s01 — reply consumer (native; QEMU also supported) +python3 harness/value_demos/s01_reply_consumer.py +# hook("rpc_dispatch"); watch(gsp_ctrl+0x40, 8, 'w') +# mailbox cmd=1 arg0=0x1234 -> 0xacdb; cmd=7 -> ~7; cmd=6 -> trap, no result store + +# s02 — counter publication +python3 harness/value_demos/s02_counter_publication.py +# watch(gsp_ctrl+0x40, 8, 'w'); 20 x (mailbox cmd=5, run) -> 20/20 published + +# s03 — unknown command discovery +python3 harness/value_demos/s03_unknown_command.py +# fresh session per ID 1..24; hook dispatcher + 6 handlers; classify +# (findings.jsonl, control-discovery.md) + +# s04 — run-until / reachability +python3 harness/value_demos/s04_run_until_function.py +# run_until("rpc_sum"); run_until("rpc_fault", timeout=0.2) -> RspTimeout; +# mailbox cmd=6 + run_until; call("rpc_ping", (0x41,)) + +# s05 — snapshot diff +python3 harness/value_demos/s05_snapshot_diff.py +# snapshot before/after ECHO; Memory.diff; decode with gsp_ctrl.schema.json + +# s06 — service injection (needs `make sdk`) +python3 harness/value_demos/s06_inject_service.py +# load_service(...slot=6); invoke_remote; invoke_via_firmware + +# s07 — version diff +python3 harness/value_demos/s07_version_diff.py +# byte-patch the PING key (0xbeef -> 0xbef0) into version B; identical +# stimulus; decoded field diff + compare_sessions + +# s08 — mutation triage +python3 harness/value_demos/s08_mutation_triage.py +# 7 seeded one-field mutations; classify by stop signal + rpc_count + +# s09 — Ghidra address join (QEMU by default) +python3 harness/value_demos/s09_grabbing_a_ghidra_address.py +# names.tsv (program = runtime - 0x1000); load_names_tsv(bias=0x1000); +# hook(ghidra + bias); export_hits -> greeneye_hits.tsv + +# s10 — full evidence pack (QEMU by default) +python3 harness/value_demos/s10_full_session_evidence.py +# two identical recorded sessions; analyze + compare + hits + manifest + M1–M6 +# + startup/session/trace overhead + +# all of them +python3 harness/value_demos/run_all.py +python3 -m unittest tests.test_value_demos -v +``` + +## 5. Per-scenario detail + +### s01 — reply consumer (0x59 first-error visibility) + +The physical path sent `GSP_SET_SYSTEM_INFO` (fn72) and `SET_REGISTRY` +(fn73), and the only visible reply was `GSP_INIT_DONE rpc_result=0x59` +(`NV_ERR_OPERATING_SYSTEM`), wrapped generically by `RmStartup`; the RM log +ring was not mapped until the LOGRM id fix (`0x4c4f474d` “LOGM” vs +`0x4c4f47524d` “LOGRM”), so the failing collector step was invisible +(PART 91 ADDENDA 2–6). Ten-plus supervised boots later, the identity fix +(raw config-space dwords instead of unpacked fields) made init-done succeed +in 950 ms. The analog consumes each posted command, watches the result +field, and decodes the exact reply — including the case where the fault +handler traps before any reply store. **Success:** 3/3 dispatches, replies +match the ABI, FAULT has no result store. **Failure:** a command is not +consumed or a reply disagrees. + +### s02 — counter publication (20 slots) + +`cuCtxCreate` waits on 20 GR per-channel counters; the host saw the client’s +releases but could not translate the counter page, and for days only 4 of 20 +client-visible slots moved (PART 49, 2026-09-29). The page VA +(`0x20400f000`, slots at `+0xec0`, 16-byte stride) and the different GPA per +boot were only proven by a raw live page dump (PARTs 51, 64). The analog +publishes 20 slots and attributes each store to a PC. **Success:** 20/20 +published with one `rpc_count` increment per slot. **Failure:** any slot +missing or count delta disagreeing. + +### s03 — unknown command discovery + +New firmware can silently add or re-point dispatch entries; static reading of +an indirect table is per-image work. The 2026-10-02 sweep (189 probes, ~36 s) +found IDs 16–23 falling through to the injected-service handler table +(16–21 alias built-ins, 22–23 NULL returning `~id`) and 7–15/24–63 on the +default path (docs/19 §3.4). The analog sweeps 24 IDs with fresh sessions +and the same hook shape. **Success:** expected classes per ID. **Failure:** +a misclassification or an unreplayable probe. + +### s04 — run-until reachability + +The RM-client ladder asked per rung whether symbolized code executed under +the current stimulus; cycles 18–25 cost ~1 h 41 m of supervised boots +(PART 91 ADDENDUM 9). The analog shows three answers in one session: reached +on the natural path, timed out (not reached under this stimulus — docs/14 §5 +explicitly says this is not unreachability), reached via a trigger, and a +direct Frida-like call. **Success:** all four behaviors agree with the ABI. +**Failure:** a route reports reachable without a hit, or a direct call +disagrees. + +### s05 — snapshot diff + +The fn72 payload carried unpacked identity fields where the reference sends +raw config-space dwords; the server read the device id back as zero, and the +fix was confirmed by rejecting hypotheses field-by-field (PART 91 ADDENDUM 6, +cycle 12), then by decoding the full 1656-byte static-config reply (cycle +13). The analog diffs two snapshots and attributes every changed byte run to +a schema field. **Success:** `rpc_count`/`last_rpc`/`result`/`buf` change, +`result` equals the ECHO length, no unattributed run. **Failure:** an +unattributed changed region. + +### s06 — service injection + +P6 wired guest-originated copies through the real engine; every rung was a +kernel rebuild plus a supervised cycle with fail-safe release discipline +(PART 92 ADDENDA 5–11, 2026-10-02). The analog loads `injected_demo.elf`, +registers it in the dispatch table, and invokes it directly and through the +mailbox. **Success:** both routes agree with the SDK ABI. **Failure:** a +route disagrees, the slot is not registered, or the blob cannot load +(skipped with a reason when `make sdk` has not run). + +### s07 — version diff + +Cycles 2 (570.211) and 3 (570.144) both failed init with `0x59`, disproving +the image-pairing hypothesis at the cost of one full boot each, and the +project’s H4.1 decision table exists to separate “the code changed” from +“the behaviour moved” (docs/14 §4.1). The analog patches the PING reply +constant into a labelled version B and replays an identical stimulus; only +the patched field moves. **Success:** exactly the patched reply moves. +**Failure:** unrelated movement or an unreplayable version. + +### s08 — mutation triage + +The fail-closed rules come from error paths; the tracked harness is bounded +seeded mutation with outcome classes (docs/18 §2), and the discovery sweep +found the sentinel/boundary pointer faults (docs/19 §3.4). The analog runs 7 +one-field mutations in fresh sessions with `max_stops=1` (a faulted target +re-faults at the same PC, so the run stops at the first signal) and +classifies ok/trap/fault/no-progress. **Success:** every mutation matches +its case table. **Failure:** a contradicting class. + +### s09 — grabbing a Ghidra address + +The GPFIFO GET hypothesis (`0x101000`) was only settled when the decoded +status queue showed the fetch at `0x40000`; fix `6f9cbfa` followed +(PART 91 ADDENDUM 9, cycle 24). The analog exports a Ghidra-style +`names.tsv` with a deliberate base offset, loads it with the matching bias, +hooks the raw static address, triggers the path, and writes +`greeneye_hits.tsv` back for Ghidra. **Success:** biased address equals the +real function, hook records the hit, TSV annotates it. **Failure:** a hit at +a different address or an empty TSV. + +### s10 — full session evidence + +The evidence pack contract (docs/14 §6) was satisfied by hand for every +milestone (cycle evidence dirs, P6 dirs). The analog produces it in one run: +two deterministic sessions, analyze, compare, hits TSV, firmware/artifact +hashes, the M1–M6 skeleton (M2/M5 marked not measured without a physical +reference), and the measured overhead. **Success:** ABI reply, A/B +agreement, all artifacts, empty unexplained-delta list. **Failure:** +nondeterminism or a missing artifact. + +## 6. Overhead analysis (measured, with numbers) + +| Overhead | Measured on the analog | Real-path comparison | +|---|---|---| +| Session startup | native ≈ 0.001 s; QEMU ≈ 0.13 s per launch | one supervised hardware cycle ≈ 5–6 min (PART 91/92) | +| Session size | 12,276 B per recorded session (s10), including 2 snapshots and 16 event types | per-cycle evidence dirs hold multi-MB logs, rings and decodes | +| Trace slowdown | 3.1× run-only on the demo workload; 13.2× end-to-end including writing `blobs/trace.jsonl` (~200 k exec lines) | docs/16: ~2.4× exec trace on the demo workload (1.53 → 0.54 MIPS); guard ~7% | +| CI cost | `tests/test_value_demos.py`: 5 tests in ≈ 4.5 s; `run_all.py`: 10 demos in ≈ 18.4 s (budget 180 s) | a single physical cycle is 300–400× the entire demo suite | +| Host footprint | stdlib only; artifacts under `harness/value_demos/artifacts/`; no hardware, no guest, no kernel module | physical cycles hold D0/D3cold locks, mdev and a QEMU guest | + +**Why the overhead is dominated by the time saved.** The expensive baselines +are per-question hardware cycles and multi-hour manual campaigns (s01 ≈ 5 h, +s02 ≈ 5 days, s04/s09 ≈ 1 h 41 m each, s06 ≈ 6 h). The tool path for the same +question class costs 0.2–6.0 s on the analog plus ≤0.13 s of launch — under +7 s end to end for every demo, or ~0.02 % of one 5–6-minute cycle even +before the campaign time. Trace logging at 3–13× of a sub-second run is +still seconds; a 12 KB session replaces a multi-MB manual capture. The only +place the tools add real cost is the once-per-question scripting in +`harness/value_demos/` and the 4.5 s test, which run in CI and are reused by +every later version. + +## 7. OODA-loop compression + +| Stage | Before (real path, as recorded) | After (green-eye analog, measured) | +|---|---|---| +| **Observe** | one supervised boot + capture per hypothesis: 5–6 min, fail-safe discipline, one device-write experiment per cycle | launch 0.001–0.13 s, then hooks/watchpoints and bounded runs: 0.2–5.8 s per question | +| **Orient** | manual LOGINIT/LOGRM/NOCAT ring decode, static reports (REPORT3–10), hours per campaign; the ring was invisible until cycle 11 | `analyze`/`compare`/`Memory.diff` with named symbols: seconds, e.g. s05 attributes every changed byte to a schema field | +| **Decide** | one candidate fix per supervised cycle, accepted/rejected by another boot | version diff (s07), mutation matrix (s08) and per-slot publication (s02) test the whole candidate set in one deterministic run | +| **Act** | rebuild + supervised boot; escalate every conclusion to a physical capture | re-run the session (seconds); product change still gated on physical confirmation (docs/06 §6, docs/14 §7) | + +The compression is not “the simulator replaces the hardware”; it is the loop +around each physical experiment. green-eye reduces the number of physical +cycles needed to reach a confident hypothesis and produces the evidence pack +that makes the eventual physical confirmation cheap (s10). + +## 8. Success/failure criteria summary + +| Demo | Pass | Fail | +|---|---|---| +| s01 | 3/3 dispatches; PING/unknown replies match; FAULT traps before the store | command not consumed or reply disagrees | +| s02 | 20/20 slots published, `rpc_count` +20 | any slot missing / count mismatch | +| s03 | expected class per ID (6 known, 8 injected, 10 default) | misclassification or unreplayable probe | +| s04 | natural hit, timeout, trigger hit, direct call value | false reachability or wrong call value | +| s05 | required fields change, result = ECHO length, no unattributed run | unattributed changed region | +| s06 | remote and mailbox invocation agree with the SDK ABI | route disagreement or unloadable blob (skipped if SDK absent) | +| s07 | only the patched field moves | unrelated movement / unreplayable version | +| s08 | every mutation matches its expected class | contradicting class | +| s09 | biased address resolves; hook fires; TSV annotated | wrong address / empty TSV | +| s10 | ABI reply; A/B agreement; artifacts complete; no unexplained deltas | nondeterminism / missing artifact | + +## 9. Reproducing and extending + +```bash +cd open-iov/debug/risc-v-gsp-rm-vm +make demo && make sdk +python3 harness/value_demos/run_all.py # writes artifacts/results.json +python3 -m unittest tests.test_value_demos -v # fast gate +``` + +`artifacts/results.json` is the machine-readable source for the table in §3: +per-demo `elapsed_s`, `success`, `real_case`, answers, and artifact paths. +When the H2.2 device model lands, each demo's `projected` note becomes the +runbook for replacing its analog with the real image; the result format and +evidence contract do not change. + +## 10. Limits and fidelity statement + +* The analog firmware is not GSP-RM: no RM objects, status queue, object + model or GPU device model. Demos reproduce the *shape* of each scenario + (hook the dispatcher, watch the field, decode the reply, diff versions, + mutate, inject) and are labelled as such. +* L2 timing/concurrency is not modelled; no demo makes a timing claim. +* L3 device environment is absent; every real-firmware result is + `blocked_on_device_model`. +* Absence of a hit is a fact about that run at that fidelity, not proof of + dead code (docs/14 §2). +* Full suite status and per-demo times are regenerated by + `run_all.py`; do not quote a stale number without re-running it. |
