diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-02 10:58:21 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-02 10:58:21 -0400 |
| commit | 002e3d7d13c414ea874abbee8d82803dd3a72fe5 (patch) | |
| tree | 6627c12fa07f6a1cf73e5aaeb7390e698dd3a896 /debug/risc-v-gsp-rm-vm/docs | |
| parent | 2f249817df327f5dcccf0464ad517fa60d6071d3 (diff) | |
| download | open-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.tar.gz open-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.zip | |
debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs')
18 files changed, 3537 insertions, 20 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 f1e1946..3b56512 100644 --- a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md +++ b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md @@ -42,11 +42,17 @@ whole stack scriptable, and QEMU is just the first execution backend. | Session recording + `analyze` report | working, live-verified | | Declarative probe specs | working, live-verified | | Plugin loader + example plugin | working | -| Test suite | 22 tests green (live QEMU smoke included) | +| Native RV64IMAC simulator (M1, `--backend native`) | working; QEMU-parity differential test | +| Native exec/access trace (M1.5, R4): `--trace`, analyzer coverage | working; trace blob + determinism test | +| Test suite | 155 tests green (live QEMU + native differential included) | | Ghidra bridge | scripts + docs | | Real GSP `.fwimage` probe (first-fault report) | working (honest: it does not boot without a TU104 model — see 06) | -| TCG full-memory tracing plugin | source + optional build | -| MCP server / custom simulator / full device model | roadmap only (08) | +| TCG full-memory tracing plugin | source + optional build (verified live) | +| Automation harness (mutation/portability/version diff) + value demos | working (18/19/21/25) | +| Host RM oracle harness (`rmharness`) | wrappers + capture working; device tables depend on driver permissions (17) | +| First-touch device map + lenient native MMIO | working (24) | +| MCP server (`plugins/mcp_server.py`) | working (20); stdlib-only, read-only by default | +| Native M2+ device model | roadmap only (08, 16) | ## Quick start @@ -84,7 +90,18 @@ Interactively: | [13-GHIDRA-CONTROL](13-GHIDRA-CONTROL.md) | driving green-eye from Ghidra | | [14-ADVERSARIAL-INTEROP-GUIDE](14-ADVERSARIAL-INTEROP-GUIDE.md) | adversarial firmware navigation | | [15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) | agentic workflows and runbooks | -| [16-CUSTOM-SIMULATOR](16-CUSTOM-SIMULATOR.md) | non-QEMU simulator design (later) | +| [16-CUSTOM-SIMULATOR](16-CUSTOM-SIMULATOR.md) | native simulator (M1/M1.5 implemented; M2+ design) | +| [17-RM-HARNESS](17-RM-HARNESS.md) | host RM oracle harness (`rmharness`) | +| [18-HARNESS-AUTOMATION](18-HARNESS-AUTOMATION.md) | mutation/portability automation | +| [19-CONTROL-DISCOVERY](19-CONTROL-DISCOVERY.md) | unknown-call discovery and reachability | +| [20-MCP-SERVER](20-MCP-SERVER.md) | bundled MCP server plugin | +| [21-CI-RELEASE-PIPELINE](21-CI-RELEASE-PIPELINE.md) | release triage pipeline | +| [22-PORTING-CUDA-AND-DEVICES](22-PORTING-CUDA-AND-DEVICES.md) | porting playbook | +| [23-RM610-READINESS](23-RM610-READINESS.md) | RM 610 readiness checklist | +| [24-DEVICE-MAP](24-DEVICE-MAP.md) | first-touch MMIO device map | +| [25-VALUE-VALIDATION](25-VALUE-VALIDATION.md) | value-validation demos s01–s10 | +| [26-ARCHITECTURE](26-ARCHITECTURE.md) | full architecture and API surface map | +| [27-TOOL-AUDIT](27-TOOL-AUDIT.md) | tool audit: purpose, tests, usage, status | | [GREEN-EYE-ROADMAP](../GREEN-EYE-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/03-SCRIPTING.md b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md index c677c8d..89c61c4 100644 --- a/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md +++ b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md @@ -4,7 +4,8 @@ green-eye scripts are ordinary Python programs run with `./greeneye.sh run <script.py>` (or `python3 -m greeneye run …`). The -names `ge`, `session`/`s`, `memory`, `symbols`, `recorder` are preloaded. +names `ge`, `session`/`s`, `memory`, `symbols`, `recorder` are preloaded, and +`__file__` points at the script path (use it to locate bundled assets). ## 1. Attach or launch diff --git a/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md b/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md index fa0fe0c..db9ee27 100644 --- a/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md +++ b/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md @@ -82,9 +82,9 @@ changes behind plugin-level flags and document them in the session metadata (`s.recorder.meta`). **(d) AI/harness integration** — export sessions/reports to external tools, -generate probe specs, score runs. **MCP servers are planned as a plugin** -(`docs/08-ROADMAP.md`), not part of v0.1; do not add MCP dependencies to the -core package before that rung is scheduled. +generate probe specs, score runs. The **MCP server is implemented as a bundled +plugin** (`../plugins/mcp_server.py`, see [20-MCP-SERVER](20-MCP-SERVER.md)): +stdlib-only, read-only by default, no dependency added to the core package. ## 4. Example plugin diff --git a/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md b/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md index 857102e..f7f46c3 100644 --- a/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md +++ b/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md @@ -83,6 +83,10 @@ $GHIDRA/support/analyzeHeadless <proj_dir> <project> -process <program> -noanaly `ExportForGreenEye.java` is the reverse helper: it walks the current program and writes a green-eye-compatible `addr<TAB>name<TAB>size` TSV. +`greeneye.ghidra.render_ghidra_command(proj_dir, project, program, hits_tsv)` +renders the `analyzeHeadless … MarkGreenEyeHits.java` command line above for +scripts (the helper is not wired into the CLI). + ## 5. Breakpoints from Ghidra Two practical routes today: diff --git a/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md index 10f4f5f..5d8a9b1 100644 --- a/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md +++ b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md @@ -68,6 +68,10 @@ only after the guest-bridge work (PLAN B) no longer depends on it. ## R3. MCP server plugin +**Status: implemented.** The bundled plugin is `plugins/mcp_server.py`; see +[20-MCP-SERVER](20-MCP-SERVER.md) for the live tool list, transports and +capability flags. The text below is the original design sketch. + **Why:** expose green-eye sessions to AI harnesses and other agents under the plugin model the user requested. @@ -78,8 +82,9 @@ session/analysis API as MCP tools: `list_sessions`, `open_session`, no network exposure by default. The plugin depends on an MCP library only at plugin load time; the core stays stdlib-only. -**Gate:** v0.1 must be stable (acceptance list in 00) and the session format -frozen. Explicitly deferred by the owner; do not add MCP code before then. +**Gate:** v0.1 stability and the session format. Implemented stdlib-only +(no MCP library), stdio/loopback only, read-only unless `--allow-write` / +`--allow-inject` are passed. ## R4. Full-memory trace (TCG plugin integration) diff --git a/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md b/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md index d310f22..fb78265 100644 --- a/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md +++ b/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md @@ -19,8 +19,11 @@ surface, plus items explicitly labelled roadmap; see also docs/06-ACCURACY.md. |---|---| | `probe`/`record`/`analyze`/`compare`/`symbols`/`inject`/`ghidra`, hooks, watchpoints, snapshots, sessions, `export_hits` | working (v0.1) | | Ghidra control bridge (`run_until`, breakpoints, hits) | working (v0.1) | -| Full TCG access trace (`qemu/greeneye_trace.c`) | source + optional build; not folded into sessions yet (R4) | -| Mutation harness (AFL++ class), device model, MCP | roadmap (R5, H2/R1, H7/R3) | +| Full TCG access trace (`qemu/greeneye_trace.c`) | source + optional build (verified live); native equivalent folds into sessions (`--trace`, R4); direct TCG JSONL import is still roadmap | +| Mutation harness, control discovery/reach, portability/version diff | working (docs/18, 19, 21, 22) | +| First-touch device map + lenient native MMIO (H2.1) | working (docs/24) | +| MCP server (`plugins/mcp_server.py`, H7/R3) | working (docs/20) | +| Full device model (H2.2) | roadmap (docs/16, 24) | ## 2. The adversary model (a moving target, not a malicious party) diff --git a/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md b/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md index 2b77ca3..2f99f7b 100644 --- a/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md +++ b/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md @@ -9,10 +9,116 @@ backend: a small deterministic RISC-V machine (`libgreenmachine`) implementing the same `Session` contract as today's QEMU backend. It expands `GREEN-EYE-ROADMAP.md` §H3 (H3.1–H3.4) and the §H6 soft-processor annex. -**Nothing here exists today.** The only supported backend is `QemuTarget` + -`RspClient` (`greeneye/target.py`, `greeneye/rsp.py`); native is gated behind -H3 entry — H2.2, the QEMU device model that makes the real GSP image reach its -message loop — and must not be started ad hoc. This document only scopes it. +**Beyond M1.5 (§1.1), nothing here exists yet.** The only production backend is +`QemuTarget` + `RspClient` (`greeneye/target.py`, `greeneye/rsp.py`); M2+ of +the native backend is gated behind H3 entry — H2.2, the QEMU device model that +makes the real GSP image reach its message loop — and must not be started ad +hoc. This document scopes it. + +### 1.1 M1 status (implemented in this tree) + +M1 was explicitly commissioned and is implemented behind `--backend native`; +the status marker above still applies to the remaining milestones (M2–M6) and +to any use on real GSP images. + +* **What exists.** A pure-Python, stdlib-only RV64IMAC interpreter under + `sim/` (`sim/decode.py` decoder, `sim/machine.py` 256 MiB RAM + NS16550A + UART + execution loop) with an `RspClient`-shaped adapter in + `greeneye/sim_backend.py` (`SimClient`, `SimTarget`, `launch_native`). + `greeneye.session.Session` runs unchanged on it: registers by name, memory, + SW/HW breakpoints, watchpoints, step/continue/interrupt, `run_until`/`call`. +* **Firmware parity.** The bundled `uproc_demo` boots unmodified; a scripted + hook/watch/leave scenario produces an event-for-event identical profile on + QEMU and native (`tests/test_sim_backend.py::DifferentialTests`), including + handler arguments and return values. Watchpoints stop *before* the accessing + instruction, which reproduces QEMU's `watch` stop and the Session + clear/step/re-arm dance exactly. +* **Deviations from this design.** M1 here is the *reference* interpreter, not + the Rust product core of §12; no MMU, FPU, GC, snapshots/moments, map DSL, + scheduler, event bus or injection ABI (those are M2–M5). There is no + self-modifying code: the per-PC decode cache is invalidated by debugger + writes only. +* **Time model.** Virtual time is instruction count: `continue_(timeout)` + executes at most `timeout * 1_000_000` instructions (SIM_IPS), then reports + a budget timeout. `Session.run(duration=...)` therefore bounds native runs + by virtual time, deterministically; QEMU's wall-clock execution naturally + reaches different heartbeat counts in the same duration (see §14). +* **Measured performance** (one host thread, CPython 3.12, this machine): + ~2.3 MIPS tight RAM loop, ~1.5 MIPS sustained demo workload + (`examples/scripts/sim_bench.py`), ~1.25 MIPS with a hook + watchpoint + active. The ≥10 MIPS M1 target in §14 is a compiled-core target and is not + met by the Python reference; the demo self-test completes in ~2k + instructions, so the parity suite still runs in seconds. +* **Stop semantics.** `ebreak` (the demo's `rpc_fault`) stops *at* the trap as + a SIGTRAP like the gdbstub, and `step` executes over it; `wfi`, `fence` and + `fence.i` are no-ops (single hart, no ordering model); unmapped accesses and + illegal instructions stop as SIGSEGV/SIGILL stops instead of raising. + +### 1.1a M1.5 status - native exec/access trace (implemented in this tree) + +The roadmap R4 full-trace path now exists natively, without the QEMU TCG +plugin and without leaving the process: + +* **Engine.** `sim/trace.py` (`Trace`, `make_trace`) collects compact JSONL + lines from the execution loop in `sim/machine.py`: + `{"t":"exec","vcpu":0,"pc":"0x...","vt":N}` and + `{"t":"read"|"write","vcpu":0,"addr":"0x...","size":S,"pc":"0x...","vt":N}`, + ending in the plugin's `{"t":"summary",...}` line. `vt` is the machine's + deterministic instruction count; nothing reads the host clock. +* **Knobs (same semantics as `qemu/greeneye_trace.c`).** `exec=on|off` + (default on in the CLI), `start=`/`end=` inclusive PC filters applied to the + executing instruction - so memory accesses are filtered by *instruction* PC + exactly as the plugin filters at translation time - and a ring budget + `max_events` after which lines are counted as `dropped` (default 1,000,000, + `0` = unlimited). +* **Delivery.** The trace is written at Session close as the session blob + `blobs/trace.jsonl` (referenced from `session.json` as `trace_blob`, with + `trace` config/counts in the metadata), so it never swamps `events.jsonl`; + folded `exec`/`access`/`read`/`write` events in `events.jsonl` are also + accepted by the analyzer. +* **Analyzer.** `greeneye analyze` renders an `## Execution trace` section: + exec/read/write totals, unique PCs grouped by symbol, and accessed 4 KiB + pages with read/write counts. `greeneye compare` adds a trace-stream totals + delta and includes trace writes in the written-page delta. +* **API.** `ge.launch(..., backend="native", trace=...)` and + `launch_native(trace=...)` take `True`, a dict + `{"exec", "start", "end", "max_events"}`, or a `sim.trace.Trace`. +* **Measured cost.** On the tight one-instruction loop microbenchmark the + always-present `if trace` guard costs ~7% versus an unguarded copy; on the + demo workload (best-of runs) untraced throughput is unchanged within noise + (1.53 MIPS before, 1.55 MIPS after). Tracing itself costs ~2.4x on the demo + workload and ~3x on the microbenchmark. `examples/scripts/sim_bench.py + --trace` reports both. + +### 1.2 Usage + +```bash +./greeneye.sh demo --backend native # the canned demo, native backend +./greeneye.sh run examples/scripts/trace_rpc.py --backend native +printf 'bp rpc_sum\nc 0.2\nquit\n' | ./greeneye.sh attach --backend native +./greeneye.sh record --backend native --trace --duration 0.5 +./greeneye.sh record --backend native --trace --trace-exec off \ + --trace-start 0x80000000 --trace-end 0x80010000 +./greeneye.sh analyze dumps/<stamp>-record # renders the trace coverage section +python3 examples/scripts/sim_bench.py --trace # throughput + trace overhead +python3 examples/scripts/sim_diff.py # QEMU/native profile diff +``` + +```python +import greeneye as ge +s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", backend="native", + record="dumps/traced", trace={"exec": True, "start": 0x80000000}) +s.hook("rpc_sum", on_enter=lambda hit: print(hit.arg(0), hit.arg(1))) +s.watch(s.symbols.lookup("gsp_ctrl") + 0x10, size=8, kind="w") +s.run(duration=1.0) +s.close() # writes blobs/trace.jsonl +``` + +`--backend` is accepted by `demo`, `run`, `attach`, `record`, `inject` and +`ghidra`; the default stays `qemu` and the QEMU path is behaviourally +unchanged. `attach --port` stays QEMU-only (there is no server to attach to), +and `--trace` is native-only (QEMU uses the TCG plugin). Everything below +remains the design for M2+. ## 2. Why a custom simulator @@ -147,6 +253,13 @@ class Backend(Protocol): Compatibility decisions: +* **M1 implementation note.** Instead of the `Backend` protocol and typed + `Session(backend=...)` sketched below, M1 implements the narrower, already + exercised `RspClient` method surface (`greeneye/sim_backend.py`) and leaves + `Session` untouched. The `Backend` refactor is deferred until a second + non-RSP backend (e.g. Verilator, §16) actually needs it; §5's table is + still the M2+ target. + * `Session(client=...)` keeps working; `Session(backend=...)` is added and `client=` becomes an alias for `RspBackend(client)`. `Memory` is re-typed against `Backend`; its public methods do not change. @@ -291,16 +404,17 @@ single producer for stop-based and full-access events: |---|---|---| | `session_start`, `image_loaded`, `hook`, `watch`, `note`, `exit`, `session_end` | existing | unchanged | | `bp_hit`, `leave`, `watch_hit`, `stop`, `step`, `mem_read`, `mem_write`, `snapshot*`, `probe*`, `plugin*` | existing | unchanged; `mem_*` may now come from the bus, not only tooling | -| `exec` | new (M1, optional) | `pc`, `insn`, `vt`, `core`, `symbol` | -| `access` | new (M2) | `pc`, `addr`, `size`, `rw`, `region`, `vt`, `core` | +| `exec` | native M1.5 (blob) | `pc`, `vt` (+ `symbol` when folded into an event) | +| `access` | native M1.5 (`read`/`write` lines or folded events) | `pc`, `addr`, `size`, `vt`; `rw` only for the folded `access` form | | `device` | new (M2) | `device`, `name`, `rw`, `value`, `vt` | | `irq` / `schedule` | new (M3) | `line`, `state`, `core`, `vt`, `reason` | | `inject_*` | existing shape (docs/12) | `service_injected`, `call_enter`, `call_return` unchanged | `t` stays the recorder's monotonic seconds **filled from virtual time**, so the same run yields the same file. `exec`/`access` volume is controlled by -profile logging and a ring budget; the analyzer gains coverage/heatmap -rendering (roadmap R4). `greeneye compare` treats backend identity, `t` and +profile logging and a ring budget; for the native backend that is implemented +(M1.5, §1.1a): the stream is a session blob and the analyzer renders coverage +and accessed pages. `greeneye compare` treats backend identity, `t` and host paths as non-semantic, so cross-backend comparison is first-class. ## 11. Injection ABI @@ -352,12 +466,20 @@ familiar with the codebase; milestones are sequential. | # | Scope | Exit criteria | Effort | |---|---|---|---| | M1 | RV64IMAC interpreter, RAM, NS16550A-compatible UART, loader; `Session` adapter with `Backend` | bundled demo uproc boots unmodified; the canned instrumentation script (`03 §12`) produces the same hook/watch/stop timeline and memory snapshots as QEMU (canonical comparison); determinism test green | 4–6 wk | +| M1.5 | Native exec/access trace (R4 equivalent): `--trace`, filters, blob + analyzer coverage | trace determinism test green; analyzer renders unique PCs/pages; non-trace path unregressed (`sim_bench.py --trace`) | 1–2 wk | | M2 | Memory-map DSL + `falcon`/`csb`/`ptimer` devices + profile validation; native region snapshots; moments v1 | a re-hosted uproc build boots from a profile with no core changes; moment save/restore round-trips; restoring reproduces the event-stream hash | 4–6 wk | | M3 | Deterministic scheduler (budgets, timers, IRQ events) + event log/replay | multi-task test profile replays identically from the log on two hosts; tie-break and replay tests in CI | 3–4 wk | | M4 | Device-model parity with H2.2 + `dma`/`pmu_stub` + boot shims | real GSP image reaches the same stable state as the QEMU H2 exit under the same profile; differential suite passes for probe and boot fixtures; L3 parity documented, L2 still not claimed | 6–10 wk | | M5 | Injection ABI + service registry + `log`/`stub`/`forward` interception | an SDK v1 blob loads and serves a control end-to-end; each interception mode has a recorded acceptance session; fuzzing seed/restore demonstrated | 4–6 wk | | M6 | Performance/ergonomics: profiling, fast paths, `--backend native`, docs + flag-removal criteria | §14 targets met; `greeneye demo --backend native` in the differential CI job; user docs updated | 3–4 wk | +> **M1 status: done (pure-Python reference implementation).** Ported without +> the `Backend` refactor (see §5 note); differential parity and a determinism +> test are in `tests/test_sim_backend.py`; performance is §1.1. **M1.5 (native +> trace) is done** in §1.1a: `sim/trace.py`, `tests/test_sim_trace.py`, +> analyzer coverage rendering and `examples/scripts/sim_diff.py`. M2–M6 above +> are unchanged. + Total: roughly 24–36 engineering-weeks, consistent with the roadmap's "months; explicitly later" classification; none of it starts before the H3 entry gate, and if H2 slips this plan waits. @@ -389,6 +511,24 @@ harness, trended in CI): | MMIO access vs RAM access | ≤5× | M2 | | Event bus throughput | ≥1 M events/s buffered, lossless under budget | M3 | +> **M1 measured (pure-Python reference).** 2.3 MIPS tight RAM loop, 1.5 MIPS +> sustained demo workload, 1.25 MIPS with a hook + watchpoint active +> (`examples/scripts/sim_bench.py`, CPython 3.12). The ≥10 MIPS M1 row is a +> compiled-core gate and remains open for the Rust core; the Python reference +> exists to pin semantics and differentially test the firmware, which it does +> at parity. QEMU executes comparatively more instructions per wall second on +> this host, so equal wall-clock `run` durations produce different heartbeat +> counts across backends — the hook/watch/leave event profile is identical. +> +> **M1.5 trace overhead (same host).** The tracing guard is a single local +> boolean test per instruction plus one per memory access; A/B against an +> otherwise identical loop copy: ~2.4 MIPS vs ~2.6 MIPS on a pathological +> one-instruction loop (~7%), while the demo workload is unchanged within +> run-to-run noise (1.53 vs 1.55 MIPS best-of). With `exec=on` the demo +> workload runs at ~0.54 MIPS (~2.4x slower); an access-only trace is +> proportionally cheaper. Use `--trace-start/--trace-end` to bound the logged +> region, and the default 1M-event ring budget to bound memory. + ## 15. CI strategy * The Python package and QEMU path remain the default, always-on suite; no diff --git a/debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md b/debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md new file mode 100644 index 0000000..59db437 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md @@ -0,0 +1,164 @@ +# 17 — RM host oracle harness (`rmharness`) + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status. +> The RM tools are read-only host diagnostics. They are development tooling: +> never shipped, never present in a guest, never a driver dependency. + +## 1. Purpose + +The GSP emulation can only be trusted against ground truth. Two of the +metrics in [06-ACCURACY](06-ACCURACY.md) need the real host RM as a reference: + +* **M1 — inventory coverage**: which controls/engines the real RM exposes; +* **M6 — memory side-effects / fixed points**: real VRAM/heap/BAR1 numbers + and engine inventory at a moment in time. + +`rmharness` packages the host-side oracle tools and wires their output into +green-eye sessions, so a simulator/emulation session and a host RM capture +can be recorded, diffed and cited together. It is the host half of the +accuracy loop (H4.5), not a boot path or a replacement for PLAN B traces. + +## 2. Tool inventory + +| tool | source | role in the loop | +|---|---|---| +| `rmtool` | `tools/rmtool/` (new) | CLI oracle: `gpu list/topology/info`, `engine list/info`, `mem stats`, `nv2080 class list`, allowlisted `nv2080 method call`, `sample timer`; `--format=table\|json\|csv` | +| `rmtop` | `tools/rmtop/` (new) | ncurses dashboard over the same query layer; `--once` renders table/json/csv for capture | +| `rmq` | `tools/rmq/` (pre-existing, unmodified) | FIFO/INTERNAL device info tables for TU104 static data | + +Both new tools share one ioctl layer (`tools/rmtool/src/rm_api.c`, +`query.c`): version check, `NV01_ROOT_CLIENT` -> `NV01_DEVICE_0` -> +`NV20_SUBDEVICE_0` alloc ladder, and `NV_ESC_RM_CONTROL` queries. The +device-open protocol follows what `nvidia-smi` does: open +`/dev/nvidia<N>`, `NV_ESC_WAIT_OPEN_COMPLETE`, then allocate on +`/dev/nvidiactl`. The ABI structs match the running driver (the in-tree +userspace SDK snapshot is older and its 40-byte `NVOS64` is rejected). + +## 3. Build + +```bash +make -C tools/rmtool # builds rmtool + lib/librmapi.a +make -C tools/rmtop # builds rmtop (ncursesw) +make -C tools/rmq # unchanged, still builds +``` + +Requirements: gcc, ncursesw for rmtop. No external SDK, NVML or +libnvidia-ml linkage. `make clean` removes all artifacts. + +## 4. Running + +```bash +tools/rmtool/rmtool --help +tools/rmtool/rmtool gpu list --format=json +tools/rmtool/rmtool gpu info -d 0 +tools/rmtool/rmtool engine list +tools/rmtool/rmtool mem stats +tools/rmtool/rmtool sample timer --interval=100ms --count=20 +tools/rmtop/rmtop --once +tools/rmtop/rmtop -g 0 --interval=250 # interactive (needs a TTY) +``` + +`gpu list`/`gpu topology` need no device access (they read +`/proc/driver/nvidia/gpus` and PCI sysfs). Everything else opens a +short-lived RM client and issues read-only controls; this is the same +access model as `nvidia-smi` and is safe alongside a running CUDA job. + +`--device-spec`/`-d` accepts a minor (`0`) or a bus id (`0000:01:00.0`) and +maps to the RM `deviceId`; `--device PATH` overrides the `/dev/nvidiaN` +node. + +## 5. green-eye integration + +Wrappers `rmharness/rmtool.sh`, `rmharness/rmtop.sh`, `rmharness/rmq.sh` +locate and build the tools and pass arguments through. `rmharness/harness.sh` +adds capture: + +```bash +cd debug/risc-v-gsp-rm-vm +./rmharness/harness.sh --session dumps/real-rm-before --name before \ + -- rmtool engine list --format=json +./rmharness/harness.sh --session dumps/real-rm-before -- rmtop --once +``` + +The harness appends one `rm_harness_run` event per run and stores +stdout/stderr under `<session>/blobs/` with sizes and SHA-256. Existing +sessions are appended to, never rewritten; a missing `session.json` is +created with the `greeneye-session/1` format. Because these runs are +external, the event `t` field is `0.0`; `start`/`end` (epoch seconds) carry +the wall clock. `greeneye analyze` tolerates the session; the run count shows +up in the event summary. + +`plugins/rmharness_plugin.py` exposes the same wrappers in the REPL +(`rmtool`, `rmtop`, `rmq`), recording through `ge.recorder.blob()` + +`ge.recorder.event()`. Write-looking arguments are refused and recorded as +`rm_harness_skip`. The plugin is stdlib-only and is exercised without QEMU +by `rmharness/tests/test_rmharness_plugin.py` (fake `ge`). + +## 6. Safety contract (absolute) + +* Host-side read-only queries only. The tools allocate only an RM client / + device / subdevice handle pair; they never create engine, channel or memory + objects. +* Never load/unload kernel modules, bind/unbind the driver, create/destroy + mdevs, claim the physical GPU, reboot, or touch `/sys/fs/pstore`. +* Write paths are enforced in code, not by convention: + * `rmtool` refuses `mem alloc/map/unmap/pin`, `engine allocate/start/stop/ + config`, and `channel/context/resserve/cudasw/resman/gsp` with exit 4/3; + * `nv2080 method call` only sends controls on a read-only allowlist + (`GPU_GET_INFO_V2`, `GPU_GET_NAME_STRING`, `GPU_GET_ENGINES_V2`, + `MC_GET_ARCH_INFO`, `FB_GET_INFO_V2`, `BUS_GET_PCI_INFO`, + `TIMER_GET_TIME`, `FIFO_GET_DEVICE_INFO_TABLE`, + `GPU_GET_CLASSLIST_V2`, ...) and refuses anything else before the ioctl; + * `harness.sh` only accepts `rmtool`, `rmtop`, `rmq`; + * the plugin blocks write-looking arguments. +* Another agent may be running a live CUDA/PLAN B job; the tools do not + touch its context and all smoke checks above run without a GPU claim. + +## 7. What was and was not run + +Smoke checks executed on the development host (driver 570.211.01, TU104 +RTX 2080 Super Max-Q, one GPU at `0000:01:00.0`): + +* `--help`/`--version` for both tools; `gpu list`, `gpu topology` (proc/sysfs + only); +* `gpu info`, `engine list`, `mem stats`, `sample timer`, `rmtop --once` + (read-only RM controls) — all returned live data (`TU100`, 10 engines GR + + 5 CE + NVDEC + NVENC + SW + SEC2, 8 GiB VRAM, BAR1 256 MiB); +* `harness.sh` session recording and the plugin test. + +Not run, and why: `rmq` device info tables and `nv2080 class list` return +`NV_ERR_INSUFFICIENT_PERMISSIONS` / `NV_ERR_NOT_SUPPORTED` for an +unprivileged client on this driver (reported, not escalated); interactive +`rmtop` was not driven (no TTY in the build shell); nothing here exercises +migrations, MIG or a second GPU. + +## 8. Roadmap tie-ins + +* **docs/06-ACCURACY** — recorded `rm_harness_run` blobs are the host + reference for M1/M6; keep the JSON captures in the session directories + cited by the metric table. +* **GREEN-EYE-ROADMAP H3** — the custom simulator's device map and RM-core + interception need real control semantics (IDs, sizes, status values) to + validate against; `rmtool` supplies that inventory before the H2.2 boot is + reached, and the shared `rm_api.c` can become the simulator's host-RM + client for differential tests. +* **GREEN-EYE-ROADMAP H4.5** — the canonical observe -> compare -> implement + loop can now include a host capture (`harness.sh --session`) beside the + simulator session, giving every emulation change an evidence pair. +* **R4 (full-memory trace)** — the session-event schema already carries + external runs; when the TCG trace lands, host `rm_harness_run` events and + firmware `access` events can be interleaved for a complete real-vs-sim + picture. +* **H1.3** — injected services that emulate RM controls should be diffed + field-by-field against the `rmtool nv2080 method call` reply for the same + control id. + +## 9. Extending safely + +1. Add new read-only controls to the allowlist **and** to `rmtool.h` + structs, with the exact kernel-side size (the userspace SDK snapshot is + not authoritative). +2. Never add an override that bypasses the allowlist/refusals. +3. Record any new query in the session and document the skip reason for + anything privilege-gated. +4. Keep `tools/rmq/` untouched; call it from the harness. diff --git a/debug/risc-v-gsp-rm-vm/docs/18-HARNESS-AUTOMATION.md b/debug/risc-v-gsp-rm-vm/docs/18-HARNESS-AUTOMATION.md new file mode 100644 index 0000000..ecd0614 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/18-HARNESS-AUTOMATION.md @@ -0,0 +1,284 @@ +# 18 — Harness automation: portability matrix and mutation harness + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status. + +Two stdlib-only orchestration tools in `harness/` turn the manual playbooks +into repeatable runs: + +| Tool | Roadmap | What it does | +|---|---|---| +| `harness/portability_matrix.py` | **R6**, H4.1 | Runs probe + scripted demo + recorded sessions against several firmware ELFs and emits a cross-target delta report with the M1–M6 skeleton. | +| `harness/mutation_harness.py` | **R5**, H4.3 | Deterministically mutates the demo firmware's mailbox input surface, classifies abnormal outcomes, and records every attempt as a session plus a compact finding. | + +Both use the **QEMU backend only** (the same `probe`/`demo` machinery as the +CLI). They intentionally do not depend on `greeneye.sim_backend`, and they +never attach to an existing gdbstub, hardware, a guest or a kernel module. + +Requirements: `qemu-system-riscv64`, Python 3.10+, and (for the bundled demo) +`make demo`. All commands run from `open-iov/debug/risc-v-gsp-rm-vm/`. + +--- + +## 1. Portability matrix + +### 1.1 Usage + +Self-comparison of one build (a determinism check): + +```bash +python3 harness/portability_matrix.py \ + --target firmware/uproc_demo/build/uproc_demo.elf \ + --target firmware/uproc_demo/build/uproc_demo.elf \ + --out dumps/matrix-demo \ + --probe-instructions 200 --scenario-duration 0.2 +``` + +Old vs new firmware with the project's symbol DBs (one DB applies to every +target in the invocation; run separate invocations for per-version DBs): + +```bash +python3 harness/portability_matrix.py \ + --target firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \ + --target firmware/57X-GSP/570.211/gsp-570.211.fwimage.bin \ + --ghidra-index analysis/570.144/decompiled_index.tsv --ghidra-bias 0 \ + --symbols symbols-570.144.json \ + --out dumps/matrix-570 +``` + +Replacing the builtin scenario with a fixed green-eye script: + +```bash +python3 harness/portability_matrix.py --target <rehosted.elf> \ + --scenario-script examples/scripts/trace_rpc.py --out dumps/matrix-rehosted +``` + +Flags: `--probe-instructions N` (0 = wall-clock `--probe-duration`), +`--scenario-duration`, `--max-stops`, `--max-rows`, `--symbols`, +`--ghidra-names/--ghidra-index/--ghidra-exports/--ghidra-callgraph` +(`--ghidra-bias`), and repeatable `--target` (positional targets also work). + +### 1.2 What each target does + +1. **Probe** — the ELF is flattened and relocated to `0x80000000` exactly as + `greeneye probe` does, then run for a fixed instruction budget (default + 2000) or a wall-clock budget. The first exception is parsed with + `greeneye.analyze.parse_qemu_faults`; the stopped PC is annotated from the + symbol DB. Raw `.fwimage` targets fault early here; that is the expected + first-touch data, not a harness failure. +2. **Symbols** — ELF `.symtab` plus optional Ghidra exports and/or an + existing JSON DB, merged and saved to `<out>/symbols/<target>.json` + (runtime addresses; the relocation `bias` is recorded in the DB meta). +3. **Scenario** — the target boots under QEMU with hooks on every `rpc_*` + handler that resolves and a write watchpoint on `gsp_ctrl.rpc_count`, runs + its self-test, then receives one deterministic host stimulus + (`mailbox = PING (1)`, `arg0 = 0x1234`). Everything is recorded as a + normal `greeneye-session/1` directory. A re-hosted ELF without `rpc_*`/ + `gsp_ctrl` symbols still runs; it simply produces an empty profile. + +### 1.3 Output format + +```text +<out>/ + portability-report.json stable machine-readable report + portability-report.md rendered report + symbols/<target>.json merged symbol DB per target + sessions/<target>-probe/ recorded probe session + sessions/<target>-scenario/ recorded scripted session + compare/<a>--<b>.md greeneye compare report per target pair +``` + +JSON shape (stable keys, targets in CLI order, tables sorted): + +```json +{ + "format": "greeneye-portability-matrix/1", + "tool": "harness/portability_matrix.py", + "backend": "qemu", + "targets": [{ + "name": "...", "path": "...", "sha256": "...", "entry": "0x...", + "status": "ok", + "probe": {"status": "...", "stopped_pc": 0, "stopped_symbol": "...", + "fault_count": 0, "first_fault": null}, + "symbols": {"count": 13, "sources": ["elf"], "path": "symbols/x.json"}, + "scenario": {"session": "sessions/x-scenario", "event_counts": {}, + "bp": {}, "watch": {}, "writes": {}, "ctrl": {}}, + "metrics": [{"id": "M1", "status": "not measured", "observed": {}}] + }], + "comparisons": [{"a": "...", "b": "...", "deltas": {"bp": [], "watch": [], "writes": []}, + "all_zero": true, "metrics": []}], + "skipped": [] +} +``` + +Determinism: fixed probe instructions, fixed scenario phases and the same +stimulus make hit/watch/write profiles reproducible, so an identical target +compared with itself reports **zero deltas**. The M6 control-block snapshot +hash is *not* a fixed point while the firmware heartbeats; compare snapshots +only between quiesced runs. Metrics are marked `"not measured"` unless a +reference capture exists (docs/06 §5); `observed` carries the local facts the +matrix could gather. + +Targets that are missing, not RISC-V ELF, or too large are reported with +`status: "skipped"` and a `skip_reason`; a failing step yields `"error"` for +that step and does not abort the matrix. + +--- + +## 2. Mutation harness + +### 2.1 Usage + +```bash +python3 harness/mutation_harness.py --out dumps/mutation-7 \ + --iterations 64 --seed 7 --timeout 0.25 --settle 0.2 +``` + +Targeting the command/args surface plus an optional RAM window: + +```bash +python3 harness/mutation_harness.py --out dumps/mutation-ram \ + --iterations 128 --seed 11 --memory-region 0x80000900:0x100 +``` + +Budgets: `--iterations` (1..10000), `--seed`, `--timeout` (0.01..10 s total +observation per mutation; a short first window plus one confirmation window +for slow handlers), `--settle` (per attempt, until `SELFTEST DONE` is seen), +and `--max-log-bytes` (QEMU log window per fault check; trap loops flood +`-d int`). `--kernel` defaults to the bundled demo; a non-demo ELF is refused +unless `--allow-foreign-elf` is given, and remains QEMU-only. + +### 2.2 Strategies + +| Strategy | Input surface | +|---|---| +| `cmd-invalid` | command IDs outside the demo's valid range (`0, 7..15, 24+, 0x100, 0xffff, 2^64-1`) | +| `arg-boundary` | one of arg0/arg1/arg2 set to `0, 1, 2, 0xff, 0x100, 0x7fffffff, 2^63, 2^64-1`, ... | +| `bit-flip` | one bit flipped in the command or an argument of a valid command | +| `buffer-length` | ECHO/FILL/SUM against `gsp_ctrl.buf` with lengths `0, 1, 31, 32, 33, 255, 256, 257, 4096, 2^32, ...` | +| `mem-region` | one byte bit-flip inside `--memory-region` (when supplied) | + +Each iteration uses a **fresh QEMU session**, settles the demo through its +self-test, applies exactly one mutation, runs bounded, then reads back the +control block. No hardware watchpoints are installed, so the only stops are +budget interrupts and real traps. + +### 2.3 Outcome classes + +| Class | Detection | +|---|---| +| `ok` | command dispatched (or `cmd=0`) and the demo oracle, when one exists, matched | +| `trap` | first new exception is a breakpoint (`ebreak`), or control transferred below `0x100` | +| `fault` | first new QEMU exception is a load/store/fetch/illegal fault (`parse_qemu_faults`) | +| `no-progress` | command written but `rpc_count` did not advance within both windows (no fault) | +| `unexpected-result` | result/last_rpc contradict the documented demo oracle for `cmd ∈ {0,1,3,5, invalid}` | +| `exit` / `settle-failed` / `error` | target exited; self-test never observed; iteration exception | + +The oracle is deliberately small: handlers that dereference `arg0` (ECHO, +SUM) and injected slots 0–5 have no oracle, so their successful executions +are `ok` with `expected: null`. + +### 2.4 Output format + +```text +<out>/ + findings.jsonl one compact finding per iteration (deterministic order) + summary.md unique outcome classes, strategy coverage, examples, replay command + sessions/iter-0000/ recorded greeneye session per iteration +``` + +Finding shape: + +```json +{"iteration": 5, "strategy": "arg-boundary", + "mutation": {"cmd": 4, "arg0": 0, "arg1": 255, "arg2": 0, "region": null}, + "outcome": "fault", + "detail": {"observed": {"result": 48, "last_rpc": 5, "rpc_count_delta": 0}, + "fault": {"cause": 5, "desc": "fault_load", "epc": 2147483860, + "tval": 255, "access": {"rw": "read", "access_addr": 255, + "size": 1, "reason": "rejected"}}}, + "session": "sessions/iter-0005"} +``` + +Replay is re-running the same command: the per-iteration RNG is seeded from +`(seed, index)`, finding fields avoid wall-clock data, and `findings.jsonl` +is byte-identical for the same seed/budgets on the same host. Session +directories under `sessions/` contain the usual `events.jsonl`, `session.json` +and `summary.md`; they are the evidence behind each finding. + +--- + +## 3. Tests + +```bash +python3 -m unittest tests.test_harness -v +make test # includes the harness tests with the rest of the suite +``` + +The integration tests boot the demo twice for a self-comparison (must show +zero deltas) and run 12 seeded iterations twice (findings file, at least one +outcome class, byte-identical replay). They skip cleanly when QEMU or the +demo ELF is absent. + +--- + +## 4. Attaching reports to a portability change + +For the docs/07 P1/P2/P3 loop and the docs/14 §6 evidence pack: + +1. Run one matrix invocation per version/device/architecture family with the + same scenario options, and keep `<out>/portability-report.json`, the + per-pair compare reports and the session directories (or their sha256 + list). +2. Record the firmware hash (`sha256` in each target entry), the symbol + source list and `bias` from `<out>/symbols/*.json` meta. +3. Fill the M1–M6 table using `observed` from the matrix plus the real + reference capture; the matrix marks agreement metrics "not measured" on + purpose. Cite docs/06 L2/L3 limits in any claim. +4. For an input-surface change, run the mutation harness before and after the + change and diff the outcome-class tables; attach `findings.jsonl` as the + corpus entry list, with each finding's session as evidence. +5. Re-run the unchanged stimulus after the emulation change: profiles must + improve or stay equal (the docs/06 accuracy loop). + +--- + +## 5. Safety + +* Demo / re-hosted firmware under QEMU only. Never physical hardware, never + a guest, never a kernel module, never an existing gdbstub or the native + simulator. +* The mutation harness refuses non-demo ELFs unless `--allow-foreign-elf` is + passed explicitly, and still refuses non-ELF images. +* Memory-region mutation is bounds-checked against the QEMU RAM window; no + host memory is touched outside the emulated target. +* Every write is recorded in a session (`mem_write`, `note`, mutation fields); + budgets (`duration`/`max_stops`/`--timeout`) bound every run. +* Only temporary work directories and `<out>/` are written on the host. + +--- + +## 6. Limits and fidelity + +* L2/L3 claims are out of scope: no timing, interrupt or device-model + conclusions (docs/06). The matrix observes RAM-resident behaviour and the + first unmapped access, nothing more. +* A QEMU log window is bounded; a fault not in the window is recorded as its + secondary signal (`trap_loop`/`no-progress`) rather than guessed. +* Mutation covers one mailbox write per iteration; multi-write races and + concurrency are future work (H2/H4.4). +* The demo oracle is small and documented; "unexpected-result" is only + meaningful for the listed commands. + +--- + +## 7. Roadmap mapping + +| Item | Where this doc/tool contributes | +|---|---| +| **R5 — fuzzing and mutation harness** | `mutation_harness.py`: bounded seeded mutation, outcome classes, recorded corpus (`findings.jsonl` + sessions) | +| **R6 — multi-version orchestration** | `portability_matrix.py`: probe + rehost/demo + compare + M1–M6 for a target set | +| **H4.1 — semantic diffing** | identical stimulus per target, `compare_sessions` plus hit/watch/write deltas in one report | +| **H4.3 — mutation/fuzzing** | error-path corpus with symbol-annotated sessions, feeding fail-closed rules | +| **H4.5 — reimplementation loop** | reports are the attachable evidence for the docs/07 checklist | +| **V1 — metrics/golden data** | deterministic profiles and seeded findings support golden comparisons | +| **H7 — agentic harness** | self-describing JSON/markdown artifacts designed for an agent to drive and review | diff --git a/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md b/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md new file mode 100644 index 0000000..0f9afbe --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md @@ -0,0 +1,445 @@ +# 19 — Control discovery and hot-path reachability + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status and +> [`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) H4 for the program. + +Two harness tools close the loop between "a new firmware image does something +we do not model" and "here is the code path, its trigger and its effects": + +| Tool | What it answers | +|---|---| +| `harness/control_discovery.py` | Which command IDs exist, which code path each one takes, and what it changes. New/undiscovered controls fall out of the sweep instead of waiting for documentation. | +| `harness/reach.py` | Given one decompiled symbol/address: is it reachable, what triggers it, what arguments does it need, what happens. | + +Both are simulator-only, bounded, evidence-producing, and depend only on the +public `Session`/hook/watch primitives (stdlib-only Python). They are the +practical start of the "Sandsifter for RM" idea in +[`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) H4 and reuse the contracts +in [06-ACCURACY](06-ACCURACY.md) and [14-ADVERSARIAL-INTEROP-GUIDE](14-ADVERSARIAL-INTEROP-GUIDE.md). + +--- + +## 1. The problem + +The GSP's RM is a message/method system: services own objects and answer +controls dispatched by ID. NVIDIA ships new images on a hidden schedule, +stripped and signed, and new services, messages and controls appear between +versions. Static diffing (docs/18 portability matrix, docs/14 section 4.1) +tells us the code changed; it does not tell us which input activates the new +path, what the path needs, or what it changes. A sweep of the input surface +plus dynamic hooks answers exactly those questions, and it is safe to run in a +simulator where the firmware cannot be damaged. + +## 2. The discovery model + +### 2.1 Sweep spaces + +A "call surface" is a finite input space with an observable effect: + +| Surface (today / future) | Space | Probe action | +|---|---|---| +| demo mailbox (`--surface demo`) | command IDs `1..--max-id`, with argument patterns | write `gsp_ctrl.arg0..2`, then `gsp_ctrl.mailbox` | +| RM control/method entry (`--surface rm`, H1.3) | control IDs, classes and method opcodes | call the RM method entry with `(control, a0, a1, a2)` | +| future CSB/TRANSCOM message functions | message function IDs | write the message buffer and ring the doorbell | + +ID 0 is the demo's idle mailbox sentinel (the polling loop ignores it), so the +sweep starts at 1; surface classes with a meaningful 0 can start there instead. + +The same machinery applies to any of them because the sweep is described by a +:class:`Surface` subclass: a probe function, an effect observer, and an +entry-point table. + +### 2.2 Deterministic argument patterns + +Every ID is probed with `--patterns` (default all three): + +* `zero` — `(0, 0, 0)` (the "no arguments" baseline); +* `sentinel` — three distinct pseudo-random 64-bit values from a stream keyed + by `(seed, id)`, so an ID that misuses an argument is caught; +* `boundary` — `(2^64-1, 1, 2^63-1)`, the usual off-by-one/size boundaries. + +Patterns are deterministic: the same `--seed`/`--max-id` replays the same +findings byte-for-byte (verified by `tests/test_discovery.py`). + +### 2.3 Entry-point modelling + +Before probing, the tool reads the surface's declared entry points: + +* the dispatcher symbol (`rpc_dispatch` on the demo) is hooked for every + probe, capturing the command and the raw `arg0..arg2` at dispatch entry; +* every non-zero handler-table slot (`gsp_ctrl.handlers[0..7]`) is hooked so a + hit can be attributed to a function even when the command is not one of the + documented switch cases. + +On real RM the same role is played by the RM-core dispatch table / object +class table: hook the dispatcher, then resolve the handler the simulator +routes to. + +### 2.4 Effect capture + +Each probe observes: + +* control-block fields before/after (`state`, `rpc_count`, `last_rpc`, + `result`, `buf` hash) plus write watchpoints on the scalar fields, which + also record the *store PC* (`effects.watch`); +* the new QEMU exception window (`-d guest_errors,unimp,int`), parsed with + `greeneye.analyze.parse_qemu_faults`, so traps (`cause 3`) and faults are + distinguished from "no progress"; +* stop signals as a backend-independent fallback (a native backend has no + QEMU log: SIGTRAP maps to trap, SIGSEGV/SIGBUS/SIGILL to fault; budget + SIGINTs are deliberately excluded from findings). + +### 2.5 Classification + +One probe yields one outcome, in priority order: + +| Outcome | Meaning | +|---|---| +| `trap` | breakpoint exception (`ebreak`) or trap loop below `0x100` | +| `fault` | load/store/fetch/illegal exception | +| `method-return` | an RM-style call entry returned a value | +| `no-progress` | the mailbox was never consumed in the budget | +| `injected-slot` | command routed through the injected-service handler table | +| `known-handler` | a declared handler ran | +| `default-path` | the dispatcher catch-all ran (no dedicated handler) | +| `settle-failed` / `error` | the pre-probe state or the probe itself failed | + +`default-path`, `injected-slot` and `trap/fault` are the interesting classes: +they are where new behaviour, injection wiring and input bugs live. + +--- + +## 3. `control_discovery.py` — unknown-call discovery + +### 3.1 Usage + +Full demo mailbox sweep (189 probes: IDs 1..63 times three patterns): + +```bash +python3 harness/control_discovery.py --out dumps/discovery --seed 7 +``` + +A quick sweep for CI or bring-up (IDs 1..32, one pattern): + +```bash +python3 harness/control_discovery.py --surface demo --out dumps/discovery-min \ + --max-id 32 --patterns zero --seed 5 --timeout 0.25 +``` + +Other surfaces / budgets: + +```bash +# RM surface (documented future interface; see section 5) +python3 harness/control_discovery.py --surface rm --out dumps/discovery-rm + +# cap the probe count and keep every recorded session +python3 harness/control_discovery.py --out dumps/discovery-cap --iterations 40 --record-all +``` + +Flags: `--max-id` (1..4096), `--patterns zero,sentinel,boundary`, `--seed`, +`--iterations` (0 = all probes), `--timeout` (per-probe observation window), +`--settle` (self-test budget), `--backend qemu|native` (QEMU is the supported +default; native drops the QEMU fault log and uses signals), `--kernel` +(bundled demo by default; a foreign ELF needs `--allow-foreign-elf`), +`--record-classes` and `--record-all`, `--max-log-bytes`. + +Each probe uses a **fresh session**, settles through the self-test, then +applies one command. Fresh sessions are the safe strategy: a trap or fault can +leave the demo in a state that would corrupt the next probe. + +### 3.2 Output + +```text +<out>/ + findings.jsonl one compact finding per probe (deterministic order) + discovery-report.json format + options + per-ID aggregate + findings + control-discovery.md rendered report grouped by class + sessions/<idx>-id<NNN>-<pattern>/ kept evidence for interesting classes +``` + +Finding shape (stable keys, no wall-clock data): + +```json +{"probe": 15, "cmd": 16, "pattern": "zero", "args": [0, 0, 0], + "outcome": "injected-slot", + "handler": "rpc_ping", "handler_entry": 2147483720, "documented_handler": null, + "dispatch_seen": true, "dispatch_pc": 2147483912, "dispatch_args": [16, 0, 0, 0], + "rpc_count_delta": 1, + "effects": {"changed": [{"field": "result", "before": 48, "after": 48879}], + "watch": [{"field": "result", "pc": 2147484066, "symbol": "rpc_dispatch"}]}, + "fault": null, "signals": [], "settle_ok": true, + "session": "sessions/0015-id016-zero", "reason": null} +``` + +### 3.3 Reading the report + +The markdown report opens with the outcome counts and an "Ids by class" line, +then sections that matter: + +* **Known handler ids** — the documented dispatch table (by the firmware's own + control block, not the docs); +* **Injected-slot dispatch ids** — commands whose path is the injected-service + table, with the handler each aliases; +* **Unknown / undiscovered ids** — the explicit list that answers "what is + not handled here yet"; +* **Traps and faults** — IDs that reach error paths under some input; +* **All findings by id** — the join key for `findings.jsonl`. + +### 3.4 What the demo sweep already shows + +Measured on 2026-10-02, demo ELF sha256 `c038a04a...`, 189 probes in ~36 s: + +* IDs 1..6 hit `rpc_ping`/`rpc_echo`/`rpc_fill`/`rpc_sum`/`rpc_state`/ + `rpc_fault`; the ID 6 path ends in the `ebreak` trap and never writes + `result` (the dispatcher's post-handler stores do not run); +* **discovered class:** IDs 16..23 fall through the switch into the + injected-service handler table. IDs 16..21 alias the *built-in* handlers + (`16 -> rpc_ping`, ..., `21 -> rpc_fault`); IDs 22..23 are NULL slots and + return `~id`. This is exactly the kind of wiring a new firmware can add + silently; +* IDs 7..15 and 24..63 take the catch-all and return `~id` with + `rpc_count + 1` — the unknown-ID space; +* argument patterns change the class where the handler uses the argument: + `rpc_echo` and `rpc_sum` fault on sentinel/boundary pointers, while + `rpc_ping`/`rpc_state`/`rpc_fill` stay on the handler path. + +--- + +## 4. `reach.py` — hot-path reachability + +### 4.1 Usage + +```bash +# Directly callable target, with an argument +python3 harness/reach.py --target rpc_ping --method call --args 0x41 \ + --out dumps/reach-ping + +# Natural boot/self-test path +python3 harness/reach.py --target rpc_sum --method run --out dumps/reach-sum + +# Only reachable through a mailbox command: discover the trigger from the +# handler table and apply it +python3 harness/reach.py --target rpc_fault --method trigger --out dumps/reach-fault + +# Replay a saved probe spec; emit one for CI +python3 harness/reach.py --probe dumps/reach-fault/probe.json --emit-probe /tmp/fault-probe.json +``` + +Flags: `--method auto|stimulus|trigger|run|call`, `--all-methods`, `--args`, +`--arg-count` with `--arg-fill zero|sentinel`, `--timeout`, `--settle`, +`--stimulus script.py`, `--probe spec.json`, `--emit-probe`, `--record [DIR]`, +`--max-prior`, `--kernel`, `--backend`, `--allow-foreign-elf`. + +### 4.2 Methods + +`auto` tries methods in this order and stops at the first hit (use +`--all-methods` to compare routes): + +1. `stimulus` — run a green-eye script (or a probe spec's stimulus) with the + target hook and watchpoints armed. The script drives `session.run` itself. +2. `trigger` — read the surface's handler table, find the command ID(s) that + resolve to the target, settle the firmware, apply that command and run + bounded. This is the trigger discovery for IDs 1..6 and 16..23 on the demo. +3. `run` — `Session.run_until(target, timeout)` from launch, which catches the + natural boot/self-test path. +4. `call` — `Session.call(target, args)`, the Frida-like direct call, for + unit-testing a function in isolation. + +A method reports a hit when the target-entry hook fires, even if execution +immediately traps (`rpc_fault`): reachability and success are different +questions, and the report distinguishes them. + +### 4.3 Output + +```text +<out>/ + reach-report.json machine-readable report (stable keys) + reach-report.md rendered report + probe.json suggested repeatable probe spec (on a hit) + sessions/<method>/ optional recorded session (--record) +``` + +The report records: + +* `reachable`, `method`, and every `methods_tried` with its miss reason; +* `entry`: the register state at the target (args `a0..a3`, `ra`, `sp`) and + `dispatch_args` — the authoritative command arguments when the target was + reached through the dispatcher; +* `prior_events`: the ordered hook events that led to the hit (bounded by + `--max-prior`); +* `required_args`: discovered arguments plus their provenance (`call-args` or + `firmware-dispatch`) and the caller `ra`; +* `effects`: field deltas, watchpoint stores, the return value, and whether a + trap/fault followed (`rpc_fault`: trap `ebreak` at `pc 0`); +* `trace_window`: hook counts and, when recorded, session event counts; +* `probe_spec`: the exact spec to replay the route. + +### 4.4 Demo results + +Measured on 2026-10-02 (each run ~0.5 s, QEMU): + +| Target | Method | Result | +|---|---|---| +| `rpc_ping` | `call` with `--args 0x41` | reachable; returns `0x41 ^ 0xbeef = 0xbeae` | +| `rpc_sum` | `run` | reachable in the self-test; entry args `(buf, 16, 32, ...)`; path passes through `rpc_ping` -> `rpc_echo` -> `rpc_fill` first | +| `rpc_fault` | `trigger` | reachable via mailbox command 6 (also aliased at 21); dispatch args `cmd=6, a0=0, a1=0`; immediate `ebreak` trap | + +The self-test path is a useful reminder that compile-time inlining can bypass +the dispatcher: the `run` route reaches `rpc_sum` from an inlined call site, +so there are no `rpc_dispatch` hits before it. The `trigger` route does pass +through the dispatcher and exposes the control arguments. + +--- + +## 5. Sandsifter for RM (the future surface) + +A Sandsifter-style tool for RM needs three more capabilities than the demo +has today, in this order. + +### 5.1 The `Surface` abstraction (already in place) + +`harness/control_discovery.Surface` separates: + +* `probe(session, id, args)` — how one call is applied (mailbox write today; + RM method call / message write later); +* `observer()` — the effect model (control-block fields today; RM reply + buffer and object state later); +* `handler_map()` / `command_for_addr()` — how IDs map to code, which drives + both discovery and reach's `trigger` method. + +Adding a real RM surface is a new subclass, not a rewrite of the sweep, +classification or reporting. + +### 5.2 RM sweep design + +1. **Control/method inventory.** Start from `rmtool`/`rmq` (docs/17) for the + host-visible control list and class IDs; import the Ghidra RM-core dispatch + table into the symbol DB. The sweep ranges are then control IDs by class + (`--rm-class`) and method opcodes within a control, mirroring the + `nv2080 method call` surface. +2. **Entry-point modelling.** Hook the RM dispatch routine, the object + constructor/method tables and the status/notifier writes. For each swept + ID, capture the handler entry, the object it selects and the status it + returns. +3. **Effect observation.** Point `--rm-reply` at the reply buffer for the + control and hash/snapshot it; add watchpoints on the object handle table + and status queue. Compare the reply field-by-field against the host oracle + (`rmtool` JSON) for M2 (docs/06). +4. **Branch exploration.** The native simulator's snapshots (H3) make it cheap + to fork at the dispatcher and sweep IDs without re-booting; the QEMU + surface uses fresh sessions exactly like today. +5. **Corpus and gates.** Findings become an inventory corpus (`findings.jsonl`) + that CI diffs between firmware versions; new IDs, new handlers, or changed + default-path results are the regression signal (docs/21). + +### 5.3 Rollout gates + +* Entry gate: H2.2/H1.3 — a simulator session reaches the RM message loop and + exposes the dispatch table (otherwise `--surface rm` exits cleanly with the + message below). +* Exit gate: sweep a real control space, reach one previously unknown control, + and replay its probe spec through `reach.py` with a recorded session. + +When unavailable the tool is explicit, not mysterious: + +```text +$ python3 harness/control_discovery.py --surface rm --out dumps/discovery-rm +[discovery] skipped: surface 'rm' unavailable: no RM control/method entry point is exposed +by the simulator or a live session yet (roadmap H1.3). Pass --rm-entry SYMBOL once the +RM method dispatcher exists; the demo surface works today with --surface demo. +``` + +--- + +## 6. Feeding the simulator/device model and CI + +* **Device model.** Each discovered class is a candidate row in the + simulator's dispatch/control model: the command ID, the entry symbol, the + argument pattern that reaches it, and the effect snapshot are exactly the + data the H2/H3 device map needs. Unknown IDs (`default-path`) document the + fail-closed/default behaviour the emulation must reproduce. +* **Accuracy metrics.** The `dispatch_pc`/`handler_entry`/effects fields feed + M1 (inventory coverage) and M4/M6 (state and memory side effects, + docs/06); compare two firmware versions by diffing `discovery-report.json` + (new handler IDs, moved handler entries, changed default results). +* **CI (docs/21).** The intended gates are: + 1. `control_discovery.py --max-id 32 --patterns zero` as a fast inventory + smoke test: known handlers must resolve, `rpc_fault` must trap, and the + default-path result must stay `~id`; + 2. `reach.py` on a pinned list of targets, asserting `reachable` and the + method (`call` for `rpc_ping`, `run` for `rpc_sum`, `trigger` for + `rpc_fault`) so a compiler/inlining or dispatch change is caught; + 3. diff `findings.jsonl`/`discovery-report.json` against a golden run. + Findings contain no wall-clock data, so the diff is stable for a fixed + seed/params (the same property the mutation harness documents in docs/18). +* **Probe specs are the interface.** `reach.py --emit-probe` and + `--probe` turn a discovered route into an artifact that any agent or CI job + can replay without re-deriving it. + +--- + +## 7. Tests + +```bash +python3 -m unittest tests.test_discovery -v +make test # includes the discovery tests with the rest of the suite +``` + +`tests/test_discovery.py` covers the pure functions (pattern determinism, +probe ordering/caps, classification priority), a 32-probe demo sweep that +must attribute IDs 1..6, find an unknown ID and an injected-slot alias and +write the report, byte-identical replay for a fixed seed, the clean RM skip, +and the three reach demonstrations (`rpc_ping` call, `rpc_sum` run, +`rpc_fault` trigger). It skips cleanly when QEMU or the demo ELF is absent. +The suite completes in well under a minute. + +--- + +## 8. Safety and ethics + +* **Simulator only.** The tools launch their own QEMU (or the native + simulator with `--backend native`) against the bundled demo or an + explicitly allowed re-hosted ELF. They never attach to hardware, a guest, a + kernel module or an existing gdbstub. Ctrl-C leaves no target running: each + session is closed in a `finally`. +* **Bounded probes.** Every probe has a settle budget and an observation + window; `--max-id`/`--iterations` cap the sweep; QEMU log reads are capped. +* **No hardware writes.** All memory writes go through the debug stub into + emulated RAM; the only host writes are under `--out`. +* **Evidence, not truth.** A `default-path` result is evidence about this + firmware at this fidelity (L0/L1, docs/06), not a statement about silicon. + Never use a simulator observation to bypass a hardware protection; escalate + anything that would change the product to a physical capture (docs/14 + sections 6-7). +* **No injected payloads here.** Discovery only *observes* injection slots; it + never loads a blob. Injection itself stays behind `greeneye inject` and the + docs/12 contract. + +--- + +## 9. Limits and fidelity + +* QEMU wall-clock slicing bounds when a handler is interrupted, so a very long + handler may be classified from its entry hit without its final result; the + finding says `known-handler` and leaves `effects` empty rather than guessing. +* The demo is not RM: there is no object model, no status queue and no + per-control reply. The RM design in section 5 is deliberately unbuilt until + the simulator exposes an RM dispatch table (H1.3/H2.2). +* `trap` vs `fault` uses the guest exception cause; QEMU and the native + simulator agree on the demo, but x86/RISC-V mapping differences are a + documented fidelity gap (docs/06 section 8). +* The sweep is single-threaded and deterministic; concurrent/multi-core + dispatch interleavings (H2.4) are future work. + +--- + +## 10. Roadmap mapping + +| Item | Contribution | +|---|---| +| **H4.adversarial navigation** | unknown-call discovery and trigger discovery over control surfaces | +| **H4.1 semantic diffing** | `discovery-report.json` is a version-diffable inventory (new/changed IDs, handlers, default paths) | +| **H4.2 capability inference** | `effects.watch` store PCs are access evidence for the capability table | +| **H4.5 reimplementation loop** | `reach.py` output plus probe spec is the observe -> reproduce artifact for a new behaviour | +| **H3 native simulator** | `--backend native` reuses the same sweep once parity lands; snapshots will remove the fresh-session cost | +| **V1 golden data / docs/21 CI** | stable findings and the fast 32-probe gate are the golden corpus | diff --git a/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md new file mode 100644 index 0000000..a3b6111 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/20-MCP-SERVER.md @@ -0,0 +1,277 @@ +# 20 — MCP server (green-eye agent interface) + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status. + +## 1. What it is + +`plugins/mcp_server.py` is the green-eye **MCP (Model Context Protocol) +server** — roadmap [`R3`](08-ROADMAP.md#r3-mcp-server-plugin) and +[`H7`](../GREEN-EYE-ROADMAP.md). It exposes the *active* `Session` as MCP +tools so an AI agent (Claude Code, opencode, any MCP client) can inspect +symbols, set breakpoints/watchpoints, run the target, read memory, snapshot, +analyse/compare recordings, inject services, and drive the harnesses. + +* **Stdlib-only**: JSON-RPC 2.0 over newline-delimited stdin/stdout, no MCP + library, no network listener unless explicitly asked. +* **Plugin first**: `register(ge)` adds an `mcp-serve` REPL/CLI command that + serves the session already loaded by green-eye. +* **Standalone too**: the same file runs as a script that launches its own + session (`--kernel`, `--backend`, `--record`) and then serves MCP. +* **Read-only by default**: memory writes, remote calls and injection require + explicit startup flags. +* **Defensive capabilities**: tools owned by other components (mutation and + portability harnesses, native trace, injection) return a structured + `not_available` tool error when the module/file is absent instead of + crashing. The tool list always advertises every capability. + +The core green-eye package is untouched; everything lives in the plugin. + +## 2. Running it + +All commands run from `open-iov/debug/risc-v-gsp-rm-vm/`. + +### 2.1 Standalone (the usual MCP client mode) + +```bash +# bundled demo, QEMU, recorded session +python3 plugins/mcp_server.py --stdio --record + +# native simulator, recorded +python3 plugins/mcp_server.py --stdio --backend native --kernel firmware/uproc_demo/build/uproc_demo.elf --record + +# enable writes / injection (never on by default) +python3 plugins/mcp_server.py --stdio --allow-write --allow-inject + +# local loopback TCP instead of stdio (for clients that cannot spawn a process) +python3 plugins/mcp_server.py --tcp 127.0.0.1:8765 --record +``` + +Flags: `--stdio` (default), `--tcp HOST:PORT` (loopback only), +`--kernel ELF`, `--backend qemu|native`, `--record [PATH]`, `--name NAME`, +`--plugins PATH` (repeatable), `--allow-write`, `--allow-inject`. +Logs go to stderr; **stdout carries protocol frames only**. + +### 2.2 Plugin mode (serve a live session) + +```bash +./greeneye.sh attach --kernel firmware/uproc_demo/build/uproc_demo.elf \ + --plugin plugins/mcp_server.py +greeneye> mcp-serve # read/observe +greeneye> mcp-serve --allow-write # + memory writes / remote calls +greeneye> mcp-serve --allow-inject # + service injection +``` + +`mcp-serve` blocks until the MCP client disconnects or calls the `close` +tool, then returns to the REPL. In plugin mode `close` stops the server but +**does not** close the session (the REPL still owns it); in standalone mode +`close` also closes the launched session. + +Both `--stdio` and `--tcp` behave identically. The bundled plugin directory is +loaded by default, so `mcp_server.py` is auto-discovered; the command only +serves when invoked. + +## 3. Client configuration + +The MCP server must be started **before** the session it should serve, so +client configs use standalone mode. Use absolute paths. + +### 3.1 Claude Code / `.mcp.json` + +```json +{ + "mcpServers": { + "green-eye": { + "command": "python3", + "args": [ + "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py", + "--stdio", + "--backend", "native", + "--kernel", "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/firmware/uproc_demo/build/uproc_demo.elf", + "--record" + ] + } + } +} +``` + +Or, one line: `claude mcp add green-eye -- python3 +<tool-root>/plugins/mcp_server.py --stdio --record`. + +### 3.2 opencode (`opencode.json`) + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "green-eye": { + "type": "local", + "command": [ + "python3", + "/home/rmcore/Git/integdev_gpu_drv/open-iov/debug/risc-v-gsp-rm-vm/plugins/mcp_server.py", + "--stdio", "--backend", "native", "--record" + ], + "enabled": true + } + } +} +``` + +For an agent that must write memory or inject, add `--allow-write` / +`--allow-inject` to `args` and keep the session recorded (the recording is +the audit trail). + +## 4. Protocol + +| Request | Behaviour | +|---|---| +| `initialize` | returns `protocolVersion` (echoes `2024-11-05`, `2025-03-26` or `2025-06-18`; otherwise the server default), `serverInfo`, `capabilities.tools` | +| `notifications/initialized` | accepted silently (notifications never get a response) | +| `ping` | `{}` | +| `tools/list` | the 24-tool catalogue with JSON input schemas | +| `tools/call` | `{"name": ..., "arguments": {...}}` → `{"content":[{"type":"text","text":"<json>"}]}`, or `isError: true` for tool errors | + +JSON-RPC errors: `-32700` parse error, `-32600` invalid request, `-32601` +unknown method, `-32602` unknown tool / bad params. Tool-level failures +(bad address, symbol not found, gating, capability absent) are returned as +`isError: true` results whose text is a JSON object with `error` and +`message` — the server never dies on a bad call. + +Tool result payloads are always a single JSON text block so agents can parse +them deterministically. + +## 5. Tool catalogue + +| Tool | Arguments | Class | Notes | +|---|---|---|---| +| `session_info` | — | read | backend, PC, symbols, recording, breakpoints/watches, enabled capabilities | +| `symbols_resolve` | `target` | read | symbol, `symbol+0xNN` or hex address | +| `bp_set` | `target`, `kind=sw\|hw` | observe | hits recorded as `bp_hit` | +| `bp_clear` | `target` | observe | removes every breakpoint at that address | +| `watch_set` | `target`, `size=8`, `kind=w\|r\|a` | observe | hits recorded as `watch_hit` | +| `run` | `duration=1.0`, `max_stops`, `timeout` | observe | bounded; services hooks/watchpoints | +| `run_until` | `target`, `timeout=5.0` | observe | temporary breakpoint | +| `step` | `count=1` (max 10000) | observe | dispatches hooks/watchpoints | +| `read_mem` | `addr`, `size=64` (max 1 MiB) | read | lowercase hex | +| `write_mem` | `addr`, `hex` | **write** | requires `--allow-write`; recorded as `mem_write` | +| `hexdump` | `addr`, `size=64` (max 1 MiB) | read | symbol-annotated text | +| `snapshot` | `addr`, `size` (max 16 MiB), `label=mcp` | read | zlib + sha256 into the session | +| `call_function` | `target`, `args=[]`, `timeout=5.0` | **write** | requires `--allow-write`; Frida-like remote call | +| `inject_service` | `service`, `slot=6`, `load_addr`, `cmd=1`, `arg0`, `arg1`, `path=remote\|firmware\|both`, `settle=0.25` | **inject** | requires `--allow-inject`; service ELF built with `make sdk` | +| `trace_start` | `exec=true`, `start`, `end`, `max_events` | trace | native backend + recording session only | +| `trace_stop` | — | trace | flushes `blobs/trace.jsonl`, returns counts | +| `analyze_current` | `max_rows=40` | read | `render_summary` for the active recording | +| `compare_sessions` | `a`, `b`, `symbols=true`, `max_rows=40` | read | A/B recorded sessions | +| `export_hits` | `session`, `out` | read | Ghidra `greeneye_hits.tsv` | +| `probe_firmware` | `elf`, `instructions=2000`, `duration=1.0`, `no_relocate`, `symbols`, `timeout=300` | harness | QEMU-only probe subprocess; first divergence | +| `mutation_run` | `kernel`, `out`, `iterations=10`, `seed=1`, `timeout=0.25`, `settle=0.2`, `memory_region`, `allow_foreign_elf` | harness | `harness/mutation_harness.py`, QEMU-only | +| `portability_matrix_run` | `targets[]`, `out`, `probe_instructions=2000`, `scenario_duration=0.4`, `symbols`, `max_rows` | harness | `harness/portability_matrix.py`, QEMU-only | +| `plugins_list` | — | read | bundled plugins, `GREENEYE_PLUGINS`, REPL commands | +| `close` | — | session | stop serving; standalone also closes the session | + +Capability classification follows the manifest in +[15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) §5: read / observe are +unattended-safe; write and inject require the explicit startup flags and a +recorded session. `bp_set`, `watch_set`, `run` and `step` mutate target +*observation state* only and stay in the observe class. + +Harness and probe tools shell out to the existing CLIs with bounded budgets +and return their stdout/stderr; they never touch hardware, a guest or a +kernel module. + +## 6. Example agent sessions + +### 6.1 Monitor an automated firmware-release run + +```json +{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"session_info","arguments":{}}} +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"symbols_resolve","arguments":{"target":"rpc_ping"}}} +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"bp_set","arguments":{"target":"rpc_ping"}}} +{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"run","arguments":{"duration":0.5,"max_stops":100}}} +{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"analyze_current","arguments":{"max_rows":20}}} +``` + +The agent watches `bp_hit`/`watch_hit` counts in the report, snapshots the +control block, runs the release stimulus, then diffs the new recording +against the golden one: + +```json +{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"snapshot","arguments":{"addr":"gsp_ctrl","size":392,"label":"release"}}} +{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"compare_sessions","arguments":{"a":"dumps/golden","b":"dumps/release","symbols":true}}} +``` + +### 6.2 Dump memory + +```json +{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hexdump","arguments":{"addr":"0x80000840","size":128}}} +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_mem","arguments":{"addr":"0x80000840","size":16}}} +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"snapshot","arguments":{"addr":"0x80000000","size":4096,"label":"ram-window"}}} +``` + +### 6.3 Inject a service + +Start the server with `--allow-write --allow-inject --record`, then: + +```json +{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"inject_service","arguments":{"service":"sdk/build/injected_demo.elf","slot":6,"cmd":1,"arg0":65,"arg1":66,"path":"both"}}} +``` + +The result contains the load address/entry and both the direct (`remote`) and +firmware-mailbox (`firmware`) return values; every segment write and call is +recorded, and the session can be compared like any other run. + +## 7. Security rules + +1. **Local only.** Default transport is stdin/stdout. `--tcp` binds loopback + addresses only (`127.0.0.1`, `localhost`, `::1`); any other host is + refused at argument parse time. There is no auth, so never expose it. +2. **No guest, no hardware.** The server drives the simulator/QEMU session + only. It does not touch `/dev`, a kernel module, an mdev, a guest or a + physical GPU; the agent runs against the simulator by default + ([H7](../GREEN-EYE-ROADMAP.md), [15-AGENTIC-GSP-IDE](15-AGENTIC-GSP-IDE.md) §5). +3. **Read-only by default.** `--allow-write` enables `write_mem` and + `call_function`; `--allow-inject` enables `inject_service`. The agent's + client config decides which manifest class it gets; a read/observe agent is + configured without those flags. +4. **Sessions are the audit trail.** Writes, calls, injections and snapshots + land in `events.jsonl`; run with `--record` (the trace tools require it). + A finding without a session is not a finding. +5. **Budgets everywhere.** `run` defaults to 1 s, `step` is capped, reads and + snapshots have size caps, and harness subprocesses have timeouts. A wedged + target is left stopped and restarted clean, never poked. +6. **Protocol hygiene.** stdout is reserved for JSON-RPC; diagnostics go to + stderr. One bad tool call returns `isError` and the server keeps serving. +7. **No product changes.** MCP can propose evidence, not merge emulation + changes; human approval still gates `open-iov-vfio`. + +## 8. Absent capabilities + +`probe_firmware`, `mutation_run`, `portability_matrix_run`, `trace_*`, +`inject_service`, `analysis` and the Ghidra export are checked at call time. +If a module/file is missing the tool returns: + +```json +{"content":[{"type":"text","text":"{\"error\": \"not_available\", \"message\": \"mutation harness not present: ...\"}"}],"isError": true} +``` + +so an agent can plan around a capability instead of seeing the server crash. +The tool list is stable regardless of what is present. + +## 9. Testing + +```bash +python3 -m unittest tests.test_mcp -v # 34 tests, no external deps +``` + +The suite drives `initialize`/`tools/list`/`tools/call` over in-memory +streams (including unknown method/tool, parse errors, write/inject gating, +no-stdout-pollution and structured `not_available`), plus a standalone +subprocess test on the native backend that skips cleanly when the native +simulator is absent. + +## 10. Roadmap + +* **R3 (done)** — MCP plugin exposing sessions as agent tools; local stdio, + no network by default; capability gating. +* **H7 remaining** — named harness entry points and budgets, a session index + for agents, enforced capability manifest files, and the physical-escalation + gate. Those build on this server, not around it. diff --git a/debug/risc-v-gsp-rm-vm/docs/21-CI-RELEASE-PIPELINE.md b/debug/risc-v-gsp-rm-vm/docs/21-CI-RELEASE-PIPELINE.md new file mode 100644 index 0000000..d687cca --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/21-CI-RELEASE-PIPELINE.md @@ -0,0 +1,320 @@ +# 21 — CI and release pipeline (version diff + evidence bundle) + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status. + +## 1. Purpose + +This is the release-triage path for a new GSP firmware image (roadmap R6 / +H4.1, docs/23 C5/C7). Two stdlib-only tools turn the manual docs/07 P1/P2 +playbook into one repeatable run and one attachable bundle: + +| Tool | What it does | +|---|---| +| `harness/version_diff.py` | old vs new target, section by section: image/shape, symbol inventory, decompiled corpus hints, bounded QEMU probe, shared-scenario behavior, M1–M6 skeleton | +| `harness/ci_release.py` | runs version_diff → portability matrix → mutation smoke and hashes every artifact into `bundle-manifest.json` | + +`.github/workflows/greeneye.yml` runs the tool's test suite on pushes/PRs that +touch `debug/risc-v-gsp-rm-vm/**` or `tools/rm*/**`. The repository-level +`.github/workflows/ci.yml` is independent and unchanged. + +Everything here is local: the tools never download firmware, never attach to +hardware, a guest or a kernel module, and use their own QEMU processes. + +--- + +## 2. Version diff + +### 2.1 Usage + +Self-diff of the demo (a determinism/regression check; must be all-zero): + +```bash +python3 harness/version_diff.py \ + --old firmware/uproc_demo/build/uproc_demo.elf \ + --new firmware/uproc_demo/build/uproc_demo.elf \ + --out dumps/vdiff-selfcheck \ + --probe-instructions 200 --behavior-duration 0.2 +``` + +Old vs new firmware with per-version symbol DBs and Ghidra exports: + +```bash +python3 harness/version_diff.py \ + --old firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin \ + --new firmware/57X-GSP/570.211/gsp-570.211.fwimage.bin \ + --old-label 570.144 --new-label 570.211 \ + --old-ghidra-exports analysis/570.144/symbols.tsv \ + --new-ghidra-exports analysis/570.211/symbols.tsv \ + --corpus-diff analysis/570.144/decompiled_index.tsv analysis/570.211/decompiled_index.tsv \ + --out dumps/vdiff-570 +``` + +Attaching a fixed stimulus and a pre-produced corpus report: + +```bash +python3 harness/version_diff.py --old <rehosted-old.elf> --new <rehosted-new.elf> \ + --stimulus examples/scripts/trace_rpc.py \ + --corpus-report run/corpus-delta-570.144-570.211.txt \ + --out dumps/vdiff-rehosted +``` + +Targets are RISC-V ELF files or raw images (`.fwimage`). A raw target is +relocated to `0x80000000` as one implicit load; its firmware-vaddr translation +is `runtime - 0x80000000`. Symbols may be absent for raw images; the symbol +section then reports `skipped` with a reason unless Ghidra/JSON sources are +supplied. + +Flags: `--old/--new` (required), `--out` (required), `--old-label/--new-label/ +--release`, `--old-symbols/--new-symbols` (green-eye JSON), `--old-ghidra-names`, +`--new-ghidra-names`, `--old-ghidra-index`, `--new-ghidra-index`, +`--old-ghidra-exports`, `--new-ghidra-exports` (`--old-ghidra-bias`/ +`--new-ghidra-bias`), `--corpus-diff OLD NEW`, `--corpus-report FILE`, +`--stimulus`, `--probe-instructions` (`0` = `--probe-duration` wall clock), +`--behavior-duration`, `--max-stops`, `--max-rows`, `--approval-token`. + +### 2.2 Sections + +Every section reports `status: ok | skipped | error`, and a skipped section +never aborts the others. A failing QEMU probe or scenario is recorded as +`error` for that section only. + +1. **Image / shape.** Entry, span, file size/hash, PT_LOAD layout (vaddr, + filesz/memsz, flags), and the ELF section table (name/type/addr/size). + Raw images have no section table; that is stated, not guessed. +2. **Symbol inventory.** ELF `.symtab` plus Ghidra `names.tsv`, + `decompiled_index.tsv`/bucket TSVs, 4-column `exports.tsv` and the + 3-column `ExportForGreenEye` form (`addr<TAB>name<TAB>size`), plus + green-eye `greeneye-symbols/1` JSON DBs. Matching follows + `userspace/cuda/tools/diff_corpus.py`: named functions match by name + (duplicates in address order), the address shift is the median of named + deltas, and `FUN_*` functions match at the shifted address. The report + lists added/removed/moved/resized (named and anonymous), the shift and its + dominance, match rates and the named-transfer rate. Merged per-side DBs are + saved under `<out>/symbols/`. +3. **Decompiled-change hints.** `--corpus-diff A B` runs the same matching + discipline over two corpus indexes and writes `<out>/corpus/corpus-diff.json`; + `--corpus-report FILE` attaches a previously produced report (hash + + preview). With neither flag the section is `skipped` with reason + `not supplied: ...`. +4. **Probe first-touch.** Both targets are flattened, relocated to + `0x80000000`, stepped for the bounded budget, and their QEMU logs parsed + with `greeneye.analyze.parse_qemu_faults`. The first fault is reported with + cause/desc, `epc` **and** `epc_firmware`, the access address **and** + `access_firmware` (translated only when the access address falls inside the + image). The diff says whether the stopped PC/first fault changed. +5. **Behavior.** The shared demo scenario from `portability_matrix` (hooks on + every resolvable `rpc_*`, a write watchpoint on `gsp_ctrl.rpc_count`, the + self-test, one deterministic PING) or a `--stimulus` script is run on both + targets and recorded as normal `greeneye-session/1` directories. The report + includes `analyze.compare_sessions` output under `<out>/compare/`, explicit + hit/watch/written-page deltas, `all_zero`, and the docs/06 M1–M6 skeleton + with every agreement metric marked `not measured` (a reference capture is + required, docs/06 §5). + +### 2.3 Output + +```text +<out>/ + version-diff.json stable machine-readable report + version-diff.md rendered report + symbols/<target>.json merged symbol DB per side (when non-empty) + sessions/<target>-probe/ recorded probe session + sessions/<target>-behavior/ recorded scenario session + compare/<old>--<new>.md compare_sessions report + corpus/corpus-diff.json when --corpus-diff is used +``` + +Exit status: `0` for a normal report (skips included), `2` for a tool error, +`3` for an RM 610 gate refusal. + +--- + +## 3. RM 610 gate (hard, testable) + +The gate is enforced by `version_diff` and `ci_release` **before any target +file is opened** and before `--out` is created: + +* a target/release label matching `rm610` / `RM-610` / `rm_610` etc. (the + `--release`, `--old-label`, `--new-label` values and the target basenames) + is refused unless + * `--approval-token RM610-APPROVED` is passed, or + * `GREENEYE_RM610_APPROVED=1` is set; +* the refusal text points at [`docs/23-RM610-READINESS.md`](23-RM610-READINESS.md) + and states that nothing was downloaded and no target file was opened; +* the same check runs in `--dry-run` mode. + +Nothing in either tool performs network access, and `--dry-run` never touches +a target. Current-version rehearsal runs use `570.144`-labelled inputs; 610 +inputs require the §6 approval message in docs/23. + +--- + +## 4. Release pipeline + +### 4.1 Dry run first + +```bash +python3 harness/ci_release.py --old <old> --new <new> --out dumps/release-570 \ + --old-label 570.144 --new-label 570.211 --dry-run +``` + +`--dry-run` prints a deterministic JSON plan (the three step commands and the +artifact paths) and exits `0` without creating `--out` or opening a target. +This is the docs/23 §4 step 6 rehearsal. + +### 4.2 Real run + +```bash +python3 harness/ci_release.py --old <old> --new <new> --out dumps/release-570 \ + --old-label 570.144 --new-label 570.211 \ + --old-symbols symbols-570.144.json --new-symbols symbols-570.211.json \ + --stimulus examples/scripts/trace_rpc.py \ + --mutation-kernel firmware/uproc_demo/build/uproc_demo.elf \ + --mutation-iterations 8 --mutation-seed 7 +``` + +Steps, in order: + +1. **version_diff** — writes `<out>/version-diff/` (section flags above); +2. **portability_matrix** — old and new as two targets, same budgets, writes + `<out>/portability-matrix/portability-report.json|.md` and per-pair compare + reports; the self-comparison must show `all_zero`; +3. **mutation_smoke** — `mutation_harness` on the demo (or `--mutation-kernel`, + with `--allow-foreign-mutation` for a re-hosted ELF) with a small budget; + writes `<out>/mutation-smoke/findings.jsonl|summary.md`. + +A step that raises is recorded as `status: error` and the later steps still +run; the process exits `1` when any step errored, `0` otherwise (`skipped` +steps do not fail the run, but their reasons are in the report). Exit `3` is +the RM 610 refusal. + +`ci_release` flags: the `version_diff` inputs above plus `--matrix-symbols` +(one JSON DB applied to both matrix targets), `--mutation-kernel` (default: +the bundled demo), `--mutation-iterations`/`--mutation-seed`/ +`--mutation-timeout`/`--mutation-settle`, and `--allow-foreign-mutation` for a +non-demo re-hosted ELF. + +### 4.3 Evidence bundle format + +```text +<out>/ + release-report.json pipeline summary (steps, gate, labels) + bundle-manifest.json sha256 + byte size of every other file + version-diff/ docs/21 §2.3 tree + portability-matrix/ portability-report.json|.md, symbols/, sessions/, compare/ + mutation-smoke/ findings.jsonl, summary.md, sessions/ +``` + +`bundle-manifest.json`: + +```json +{ + "format": "greeneye-release-bundle/1", + "tool": "harness/ci_release.py", + "files": [{"path": "version-diff/version-diff.json", "bytes": 12345, "sha256": "..."}] +} +``` + +`release-report.json` shape: + +```json +{ + "format": "greeneye-ci-release/1", + "dry_run": false, + "rm610_gate": {"status": "passed", "labels": ["570.144", "570.211"]}, + "old": {"label": "570.144", "path": "gsp-570.144.fwimage.bin"}, + "new": {"label": "570.211", "path": "gsp-570.211.fwimage.bin"}, + "steps": [{"name": "version_diff", "status": "ok", + "sections": {"image": "ok", "symbols": "ok", "corpus": "ok", + "probe": "ok", "behavior": "ok"}}], + "errors": [], + "manifest": "bundle-manifest.json" +} +``` + +The manifest is written last, so its hashes cover the final release report. +Attach the directory (or its manifest) to the docs/07 evidence checklist and +the docs/14 §6 pack; the docs/23 §4 step 7 expected-artifact list maps 1:1. + +--- + +## 5. CI workflow + +`.github/workflows/greeneye.yml` (new; `ci.yml` is untouched): + +* triggers on push/PR paths `debug/risc-v-gsp-rm-vm/**` and `tools/rm*/**`; +* `ubuntu-latest`, no secrets, no hardware; +* installs `qemu-system-misc clang lld-18`; +* builds `make -C debug/risc-v-gsp-rm-vm sdk demo`; +* runs `python3 -m unittest discover -s tests -t .` from the tool root (QEMU + integration tests run; if QEMU were absent they skip cleanly). + +The intended release gates this workflow covers are the fast inventory smoke +from docs/19 §6 (`control_discovery --max-id 32 --patterns zero`), the harness +suites, and the docs/21 dry-run. A full `ci_release` run against real firmware +is driven manually/agentically because it needs the versioned inputs and +symbol DBs; CI-adjacent automation should call it with `--dry-run` first. + +--- + +## 6. MCP monitoring + +`plugins/mcp_server.py` (docs/20) exposes the same machinery to an agent: +`probe_firmware`, `portability_matrix_run`, `mutation_run`, `compare_sessions` +and `analyze_current`. The agent pattern for a release: + +1. `portability_matrix_run` with the old/new targets and fixed budgets; +2. `mutation_run` on the demo with the pinned seed and budget; +3. `compare_sessions` for the recorded evidence pair; +4. read `version-diff.json` / `release-report.json` / `bundle-manifest.json` + from the run directory (agent reads the bundle, it does not recompute it). + +The MCP server remains read/observe by default and simulator-only (docs/20 §7). + +--- + +## 7. Determinism, limits and safety + +* Fixed probe budgets, fixed scenario phases and the same stimulus make the + self-diff zero: an unchanged target reports zero symbol/probe/behavior + deltas (verified by `tests/test_version_diff.py`). +* Agreement metrics M1–M6 are **not measured** without a reference capture + (docs/06 §5); the skeleton carries local `observed` facts only. L2/L3 claims + (timing, interrupts, device model) are out of scope. +* Raw `.fwimage` targets execute as flat blobs; a firmware linked differently + may fault earlier than on silicon — that is first-touch data, not a boot + (docs/06 §3). +* Only temporary work directories and `<out>/` are written; QEMU is always + launched and killed by the tools; log reads are bounded + (`--log-bytes`). +* No downloads, no `/dev`, no kernel module, no existing gdbstub, no physical + GPU. The 610 gate is part of both CLIs, not documentation-only. + +--- + +## 8. Tests + +```bash +python3 -m unittest tests.test_version_diff -v +make test # includes these with the rest of the suite +``` + +`tests/test_version_diff.py` covers the gate (default refusal, token/env +approval, gate-before-open), the `diff_corpus.py` matching discipline +(moved/resized/added/removed, anonymous shift), the 3-column export parser, +missing/incompatible/raw target handling, corpus hints, `ci_release --dry-run` +side-effect freedom, clean `--help`, and a demo self-diff asserting zero +inventory and behavior deltas. QEMU integration tests skip cleanly when QEMU +or the demo ELF is absent. + +--- + +## 9. Roadmap mapping + +| Item | Where this doc/tool contributes | +|---|---| +| **R6 — multi-version orchestration** | `version_diff` + `ci_release` prefix the matrix with a version-scoped diff | +| **H4.1 — semantic diffing** | symbol/corpus inventory deltas and behavior profiles for two firmware versions | +| **V1 — metrics/golden data** | deterministic self-diff and stable JSON reports support golden comparisons | +| **docs/23 C5/C7** | release pipeline implemented and exercisable; dry-run rehearsal path | +| **H7 — agentic harness** | self-describing bundle + docs/20 MCP tools for monitored runs | diff --git a/debug/risc-v-gsp-rm-vm/docs/22-PORTING-CUDA-AND-DEVICES.md b/debug/risc-v-gsp-rm-vm/docs/22-PORTING-CUDA-AND-DEVICES.md new file mode 100644 index 0000000..30f4720 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/22-PORTING-CUDA-AND-DEVICES.md @@ -0,0 +1,353 @@ +# 22 — Porting with green-eye: CUDA userspace, GSP firmware and new GPU devices + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00-OVERVIEW for status. +> This is the combined porting guide for the three churn axes the project +> faces. It joins the existing CUDA/GSP/firmware procedures with green-eye's +> runtime layer. Items marked "planned (docs/xx)" do not exist yet; do not +> invent commands for them. + +## 1. The three churn axes and the anchor discipline + +NVIDIA ships three things on independent schedules, and any one of them can +break `open-iov-vfio` while the other two stay still: + +| Axis | Examples | Primary evidence path | +|---|---|---| +| **CUDA userspace version** (`libcuda`) | 570.144 → 570.211.01; future 580/610 families | CUDA corpus tooling + guest smoke (`userspace/cuda/docs/07`) | +| **GSP firmware / RM version** | 570.144 → 570.211 GSP image; RM dispatcher and control-set changes | `greeneye probe` + `compare`, host oracle (`docs/05-operations/02` §4.1) | +| **GPU device / architecture** | TU104 → GB202 (RTX 5090), GB10 (Grace-Blackwell), aarch64 hosts | Device profile + pairing check + portability matrix (docs/07 P2/P3, docs/18) | + +They interact: + +* a userspace bump usually implies an emulation-anchor decision (guest driver + and emulated firmware move together) even when the firmware binary is + byte-identical; +* a device change can drag a different firmware family (TU10x vs GB100 vs + `gb10`) and a different partition/boot model (FMC/MP-SK; Init → ACR → RM for + confidential compute) with it; +* an architecture change (aarch64 host, newer NV-RISCV core) can invalidate + environment assumptions that the other two axes assume are stable + (PMU/SOE provisioning, coherent memory, ISA extensions). + +**Anchor discipline.** The lock-step that must stay coherent is the +**guest driver ↔ emulated firmware profile** (the GSP generation whose protocol +the host emulates). The host's distribution driver is **not** part of that +lock-step: it is the oracle used for A/B comparisons, and the optional OpenRM +host modules are only for native vGPU-management workflows. The current anchor +is **570.144**; a version bump moves the guest driver and the emulation, not +the host distro driver (see `docs/05-operations/02-debugging-and-maintenance-workflows.md` +§4.1). Porting rules that follow: + +1. One anchor per port; never evaluate a new `libcuda` against an old emulated + firmware profile and call a mismatch an emulation bug. +2. Every input is versioned and hashed: `firmware/<SERIES>-GSP/<VER>/`, + `userspace/cuda/<version>/`, `config/versions/<MAJOR>.yaml`. The `config/` + YAML files are **design targets, not live configuration** until the module + load path reads them (`docs/06-portability/hardware-software-matrix.md` §6). +3. New versions live in parallel directories; the previous anchor stays + untouched so rollback never destroys a working setup. +4. A port is a claim with evidence attached (M1–M6, §4), not a merged patch. + +## 2. Joint workflow A — CUDA userspace version port (`libcuda` bump) + +### 2.1 Decide what kind of port it is (minutes) + +Run the cheap triage before any decompilation or emulation work: + +```bash +cd userspace/cuda +tools/cudatool.py info <new>/run/libcuda.so.<new> +tools/cudatool.py delta <old>/run/libcuda.so.<old> <new>/run/libcuda.so.<new> > <new>/run/delta.txt +``` + +| `delta` reading | Port class | What it implies | +|---|---|---| +| no export moves, small `.text` growth | patch | transfer the name map and spot-check touched subsystems | +| new/removed exports | feature | decode the new API surface before touching the emulation | +| many moves | reshuffle (toolchain/linker) | plan a full re-analysis; a fraction of names will not transfer | + +### 2.2 Static analysis vs dynamic confirmation + +| Step | Existing project tooling | green-eye addition | +|---|---|---| +| Export/interface diff | `tools/cudatool.py delta` | — | +| Function matching / name-transfer measurement | `tools/cudatool.py fdematch --names <names.tsv>` | — | +| Corpus-level diff (added/removed/resized functions) | `tools/diff_corpus.py <old_index> <new_index>` | — | +| Name transfer + corpus annotation | `ApplyNameMap.java` (`--dry-run` first), `tools/annotate_corpus.py` | `./greeneye.sh symbols export` builds a runtime-address symbol DB from the same index/names | +| Driver-API smoke | `tools/test/cuda_drv_test.c` compiled as `/tmp/cdt` (prints `ALL OK`) | `./greeneye.sh symbols resolve --db ... <addr>` spot-checks transferred addresses per call path | +| Runtime workload | `tools/cuda-ab/ab.sh all` (`vector_add.cu`; `RESULT`/`STATUS` must match host) | session-format evidence and `./greeneye.sh compare` when firmware-side sessions exist | +| Guest-side control observation | `tools/guest-diag/ioctl_trace.c`, host module trace (`gsp-resman-vfio:`) | annotation of the firmware-side RPC path with the per-version symbol DB | +| Firmware-side confirmation | — | `./greeneye.sh probe` / `record` / `compare` against the paired firmware | + +**Be honest about the execution boundary.** `cdt` and `vectorAdd` are x86_64 +guest binaries; the RISC-V simulator does not execute them. green-eye's +contribution to a userspace port is the **symbol/evidence layer**: a +runtime-address DB per `libcuda` version, per-address resolution checks for +transferred names, and a recorded comparison format for the GSP-side behaviour +the driver drives. The print-and-verify path (`cdt`, `ab.sh`, ioctl trace) +remains the dynamic oracle. + +### 2.3 Step-by-step runbook + +1. **Acquire and version.** Fetch the new `libcuda.so.<version>` + (`570.144/scripts/fetch_from_guest.sh`, or the driver package with + `--extract-only`). Work in a new `userspace/cuda/<new>/` tree copied from + `570.144/`; never overwrite an analysed version. +2. **Cheap triage.** `cudatool.py info` + `delta`; classify the port (§2.1); + record `.text` growth, build-id and ABI note. +3. **Measure transfer before copying.** Run `cudatool.py fdematch` with the + old `names.tsv`. The known result from 570.144 → 570.211.01: all 659 + exports transfer exactly, verified internals 5/7, wrapper-callee internals + 21/29, but only **~1% of generated `wrapper_*` addresses** transfer. Never + move wrapper names by address arithmetic; re-resolve them with + `cudatool.py wrappers` and re-run `annotate_corpus.py`. +4. **Full analysis when needed.** For a feature/reshuffle port run + `ghidra_full_analysis.sh`, `ghidra_export_index.sh`, + `ghidra_batch_decompile.sh`, `ghidra_index_merge.sh` per + `userspace/cuda/docs/07-version-upgrade-playbook.md` §2, then + `diff_corpus.py` for the measured corpus delta. +5. **Transfer names and knowledge.** Apply the map with `ApplyNameMap.java` + `--dry-run` first, then for real; translate the subsystem index; re-validate + entries the delta marked as moved. +6. **Build the green-eye symbol DB** for the new version so runtime addresses + resolve: + + ```bash + cd debug/risc-v-gsp-rm-vm + ./greeneye.sh symbols export \ + --names ../../userspace/cuda/<new>/annotations/names.tsv \ + --index ../../userspace/cuda/<new>/run/analysis/decompiled/decompiled_index.tsv \ + --callgraph ../../userspace/cuda/<new>/run/analysis/elf-index/callgraph.tsv \ + --bias 0x100000 -o symbols-<new>.json + ./greeneye.sh symbols resolve --db symbols-<new>.json 0x<transferred-addr> + ``` + + `--bias 0x100000` is the Ghidra image base convention for `libcuda` + (docs/02 §6); use `0` for firmware linked at its load address. +7. **Dynamic confirmation on the emulated stack.** Compile and run `cdt` + (`tools/test/cuda_drv_test.c`), with the PTRACE ioctl tracer attached, on + the new userspace version against the **same** emulated firmware profile: + every `CHECK(...)` line and the final `ALL OK` is a per-entry-point + assertion (`cuInit`, `cuDeviceGetCount`, `cuDeviceGet`, `cuCtxCreate`, + `cuMemAlloc`, `cuMemcpyHtoD`, `cuMemcpyDtoH`, `cuMemFree`, `cuCtxDestroy`). + Then run the runtime workload end-to-end: + + ```bash + tools/cuda-ab/ab.sh all # host oracle + guest run + compare + ``` + + `DEVICE` lines are normalised; `RESULT`/`STATUS` must match exactly. Any + new or reordered control in the ioctl trace is the interesting delta. +8. **Compare across versions.** Diff `delta.txt`, the `fdematch` report, the + corpus diff, and the ioctl/control sequences old vs new. Where the firmware + side is recorded, use the session compare primitive: + + ```bash + ./greeneye.sh compare dumps/<old-fw-run> dumps/<new-fw-run> --symbols + ``` + + For multi-target orchestration use the portability matrix (docs/18); note + it targets firmware ELFs, not `libcuda`. +9. **Only then touch the emulation** — and only if the behaviour diff requires + it, with the docs/07 P4 escalation rule: any change that would change + emulation behaviour is reproduced against the physical/vfio-mdev reference + first. +10. **Update documents**: the per-version decompilation map, `names.tsv` / + `subsystems.tsv`, the version-upgrade playbook notes, and the interface + doc only if controls/capability bits changed. + +### 2.4 Acceptance evidence (CUDA userspace port) + +* [ ] `delta` report (exports added/removed/moved, `.text` growth, build-id); +* [ ] `fdematch` transfer table with counts per source class; +* [ ] wrapper re-resolution evidence; no wrapper transferred by address; +* [ ] `ApplyNameMap.java --dry-run` then real run; corpus diff report; +* [ ] `symbols-<version>.json` with provenance/bias and spot-check resolutions; +* [ ] `cdt` output ending `ALL OK`, with the ioctl trace attached; +* [ ] `ab.sh` host/guest comparison with `RESULT`/`STATUS` matched; +* [ ] ioctl/control diff old vs new explained line by line; +* [ ] firmware anchor unchanged (or explicitly changed in the same PR); +* [ ] M1–M6 table when firmware-side behaviour moved (§4); +* [ ] docs updated; unexplained-delta list empty (empty is a claim). + +## 3. Joint workflow B — new GPU device (RTX 5090 / GB10-class) + +### 3.1 Device profile collection + +| Field | Where it comes from | Artifact | +|---|---|---| +| PCI IDs | `lspci`, vendor docs, `config/hardware/*.yaml` (design target) | device profile | +| BAR layout / sizes | guest `lspci -vvv`, host sysfs | device profile | +| FB geometry (VRAM, BAR1) | host oracle `rmtool mem stats --format=json` | capture in session | +| Engine inventory | host oracle `rmtool engine list --format=json`, guest `guest_engines.c` | inventory table | +| Interrupt topology | MSI-X tables, guest `lspci -vvv` | profile notes | +| ROM/VBIOS quirks | ROM read, laptop/Surface variants | profile overrides | +| Probe-time register values | guest driver init trace (`gsp-resman-vfio:`) | expected-value list | + +The host oracle (`rmharness`, docs/17) is read-only and safe alongside a live +CUDA job; capture it into a session so the numbers carry SHA-256 provenance: + +```bash +cd debug/risc-v-gsp-rm-vm +./rmharness/harness.sh --session dumps/real-<device> --name before \ + -- rmtool engine list --format=json +./rmharness/harness.sh --session dumps/real-<device> -- rmtool mem stats --format=json +``` + +### 3.2 Firmware pairing first + +Verify the image family (TU10x / GH100 / LS10 / `gb10`), the FMC partition +manifest and the version string **before** debugging anything. A mismatched +pairing presents as an emulation bug — the project learned this with 570.144 +vs 570.211. Record firmware hash, family, version string and pairing notes at +the top of the evidence pack. + +### 3.3 Probe first-touch diff and symbol inventory + +```bash +./greeneye.sh symbols export --index <new-export.tsv> --bias 0 -o symbols-<new>.json +./greeneye.sh probe <new-fwimage> --symbols symbols-<new>.json --record +./greeneye.sh probe <reference-fwimage> --symbols symbols-<ref>.json --record +``` + +The probe loads the segments, runs bounded and reports the first divergence +(faulting instruction, access address/size, stopped PC annotated from the +symbol DB). Raw `.fwimage` targets fault early by design — that first-touch +map is the data, not a failure. Changed first-touch addresses across devices +usually point at profile data (register windows, boot args), not logic +(docs/07 P2.2). The symbol inventory is the merged ELF/Ghidra DB per target; +provenance (export file, bias) is recorded in the DB meta. + +### 3.4 Device-model implications (H2/H5) + +What the probe reveals feeds the roadmap, not the product directly: + +* **H2 (platform device model)** — falcon/SEC2 windows, CSB/TRANSCOM mailboxes, + WPR2/FWSEC read-back state, PTIMER, PMU stub, DMA/IOMMU. A device that + touches a different first register set changes the modelled register map + before the image can reach its message loop. +* **H5 (adjacent microcontrollers)** — PMU/SEC2/ACR/SOE mailbox protocols and + boot ordering; a new device family may change identity/clock provisioning or + the Init → ACR → RM boot sequence. +* Entry gate for device-model work is the portability task itself (docs/08 R1); + until then use probe + rehosted uproc paths. + +### 3.5 Folding into the hardware-software matrix + +The project's support ladder is `docs/06-portability/hardware-software-matrix.md` +(Reference / Supported / Experimental / Target / Out of scope). A device is +"tested and documented", not "should work". The fold-in: + +1. Run `harness/portability_matrix.py` across the reference and candidate + device firmware with the same scenario options (docs/18 §1); keep + `<out>/portability-report.json`, per-pair compare reports and sessions. +2. The planned version-diff/CI pipeline (docs/21-CI-RELEASE-PIPELINE.md, + planned) is where those reports become release-gated artifacts; until it + exists, attach them to the matrix process manually. +3. Update the device profile (documentation today; `config/hardware/*.yaml` is + not read by code — do not claim configuration-driven portability). +4. Validate on the physical device with the supervised harness before claiming + support; simulator-only evidence never justifies the claim (docs/06 §6). + +### 3.6 aarch64 host / GB10-class design notes + +* **Host tooling** — green-eye is Python + QEMU; on aarch64 install + `qemu-system-misc` (provides `qemu-system-riscv64`) and the same commands + run. Keep host-specific paths out of session metadata (docs/07 P3.5). + `GREENEYE_QEMU` is the existing escape hatch if the binary lives elsewhere. +* **Firmware family** — GB10-class devices pair a different firmware family + (`gb10`/GB100-era) than TU104's `57X-GSP`; treat the fwimage, FMC manifest + and PMU/SOE images as a new family, not a re-badged TU10x image. +* **Boot/partition model** — expect the newer FMC/manifest partition model and, + for confidential-compute paths, the Init → ACR → RM flow; the rehost shim + needs the matching entry/return convention (docs/09 §4, docs/07 P3). +* **PMU/SOE deltas** — identity and clock provisioning flow through PMU/SOE; + any change there invalidates prior environment assumptions and must be + recorded before probe results are interpreted. +* **ISA/core** — confirm the image is still RV64IMAC or has moved to RV64GC + (`file`/`readelf`); update rehost `-march` flags accordingly. QEMU supports + RV64GC even where the bundled demo uses IMAC. +* **Memory model** — Grace/GB10 coherence and page-size/cache-attribute + differences are memory-model risks (hardware-software-matrix §8); test on + aarch64 early rather than assuming x86 results transfer. + +## 4. Evidence and metrics contract for any port + +The metric contract is `docs/06-ACCURACY.md` §5; it is authoritative for every +port type: + +| # | Metric | How it is measured in a port | +|---|---|---| +| M1 | Inventory coverage | observed controls/RPCs vs emulated handled set; host oracle inventory | +| M2 | Reply agreement | field-by-field reply diff, canonical-size and consumed-field checks | +| M3 | Ordering agreement | sequence alignment of the control/event stream (LCS/edit distance) | +| M4 | State agreement | firmware state transitions vs the emulation state model | +| M5 | Error-path agreement | injected faults take the same status/notifier path | +| M6 | Memory side-effects | read/write map vs shadow state; snapshot hashes at fixed points | +| M7 | Timing | **not claimed**; order-of-events budgets only | + +Rules: at least an A/B pair per conclusion; every claim carries its fidelity +level (L0–L3) and provenance; a simulator observation is evidence about the +firmware, not silicon; any product change is confirmed against a physical +capture first. The evidence pack is docs/07's checklist plus docs/14 §6. + +**Checklist per port type** + +CUDA userspace (A): +* [ ] §2.4 items complete; firmware anchor documented. + +GSP firmware version (P1): +* [ ] firmware hash + version string + pairing notes; +* [ ] symbol DB hash and Ghidra export provenance; +* [ ] probe first-touch old vs new, every changed address explained; +* [ ] portability matrix report + per-pair `compare`; +* [ ] M1–M6 table with reference-capture method and L2/L3 limits; +* [ ] existing guest ladder has no new regression; +* [ ] every new/changed control has a version-keyed handler or explicit + "not used" note. + +Device (P2): +* [ ] profile fields (§3.1) recorded with host-oracle captures; +* [ ] pairing verified before any debugging; +* [ ] probe first-touch diff explained as profile vs logic; +* [ ] engine inventory complete and validated against the host oracle; +* [ ] matrix entry and evidence published; physical validation done before the + support claim. + +Architecture (P3): +* [ ] ISA/image shape confirmed (`file`/`readelf`), rehost flags updated; +* [ ] partition/boot model identified; shim convention matched; +* [ ] PMU/SOE deltas recorded; host portability verified on aarch64; +* [ ] all P2 items. + +## 5. Common failure modes and decided responses + +| Failure mode | Signature | Decided response | Source | +|---|---|---|---| +| Mismatched guest driver ↔ firmware pairing | failures indistinguishable from emulation bugs (e.g. 570.144 image vs 570.211 driver) | verify hash/version string/pairing before debugging; fix the pairing, not the emulation | docs/07 P2.2; docs/05-operations §4.1 | +| Runfile/metapackage version drift | a different major resolves than requested | check the candidate version; prefer the exact-version runfile | docs/05-operations §4.1 pitfalls | +| Silent control-shape change | message layout changes break the emulator with no error | M2 canonical-size + consumed-field checks; per-control probes; decode against the exact-version headers | docs/05-operations §4.1 table | +| Engine-inventory change blocking context creation | `cuCtxCreate` fails because an expected engine (e.g. `COPY14`) is absent | complete the inventory per device/driver profile, validate against `rmtool engine list`; treat inventory as profile data | hardware-software-matrix §6.2 | +| Wrapper re-layout on a patch bump | wrapper names resolve to wrong addresses | transfer exports/internals only; re-resolve wrappers with `cudatool.py wrappers`; never add a delta to old wrapper addresses | userspace/cuda/docs/07 §3 | +| Firmware address drift per build | stale Ghidra addresses produce nonsense | re-derive addresses per firmware build; never carry addresses into a new configuration | docs/05-operations §4.1 pitfalls | +| Host driver assumed to be in lock-step | A/B oracle "updated" and treated as the target | host distro driver is the oracle only; the anchor is guest driver ↔ emulated firmware | docs/05-operations §4.1 | +| Simulator-only divergence | `compare` delta that physical capture cannot reproduce | record as a fidelity gap; do not change the product from it | docs/06 §6; docs/07 P4.5 | +| QEMU missing on an aarch64 host | `qemu-system-riscv64 not found` | install `qemu-system-misc` or set `GREENEYE_QEMU` | docs/02 §9; docs/07 P3.5 | + +## 6. Roadmap hooks + +| Port activity | Roadmap item | What it adds | Gate | +|---|---|---|---| +| First-touch map and boot of real firmware | H2 / R1 | device model so ports boot beyond the first unmapped access | portability/accuracy task requires it | +| Semantic diff of dispatch/handler/control sets | H4.1 | automated portability delta report; per-version symbol corpora | H1+H2 | +| Capability inference across new devices | H4.2 | access map vs capability model; least-privilege findings | watchpoints (done); TCG-session fold (R4) | +| Mutation corpus for new input surfaces | H4.3 / R5 | fail-closed rules validated per version | R1/R2 | +| Differential execution and reimplementation loop | H4.5 | every port lands with A/B sessions and the M1–M6 table | reference-capture pipeline | +| Adjacent-controller work for GB10-class bring-up | H5 | PMU/SEC2/ACR/SOE models, cross-controller sessions | H2.2 | +| Multi-target orchestration | R6 / docs/18 | one report across firmware/device targets | v0.1 stable | +| Version diff and release gating | planned (docs/21) | CI pipeline that gates a port on the evidence bundle | pipeline implemented | +| Control discovery | planned (docs/19) | systematic control/class inventory across versions | document exists | + +Program-level success criterion served here: a new GSP version (or device) can +be triaged — inventory diff + behaviour diff + metric table — in **hours**, +with recorded evidence. diff --git a/debug/risc-v-gsp-rm-vm/docs/23-RM610-READINESS.md b/debug/risc-v-gsp-rm-vm/docs/23-RM610-READINESS.md new file mode 100644 index 0000000..18d93bd --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/23-RM610-READINESS.md @@ -0,0 +1,259 @@ +# 23 — RM 610 port readiness and approval gate + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00-OVERVIEW for status. +> This document defines the gate, not a claim: readiness is **not** met today +> (2026-10-02). It exists so no one starts the port early. + +**The RM 610 port begins only after explicit human approval.** That is a hard +gate, not a scheduling preference: until a named human approves it in writing, +nothing in this repository fetches, downloads, extracts or analyses a 610 GSP +firmware image, and no code or document treats 610 as an anchor or a supported +target. The gate is repeated in the sign-off checklist (§6) and is the first +thing the approval message must acknowledge. + +## 1. Hard gate + +| Rule | Meaning | +|---|---| +| No acquisition | No 610 runfile, GSP `.fwimage`, driver package or firmware blob is fetched, extracted, copied or mirrored before approval. | +| No analysis | No decompilation, symbol export, probe, corpus diff or pairing comparison against a 610 image before approval. | +| No emulation edits | No version-keyed handler, signature entry or profile edit justified by "610" before approval; changes require current-version evidence. | +| No physical-GPU work | No unsupervised physical-GPU cycle, mdev claim or driver bind/unbind for 610; the project rule is no unattended physical-GPU cycles (GREEN-EYE-ROADMAP H7). | +| No support claims | 610 is not entered in the hardware/software matrix or the version matrix until it passes the docs/22 port checklists with evidence. | + +Precision notes: + +* The pinned `nvidia-open-gpu-kernel-modules` checkout used as build glue + (recorded in `SBOM.md`) is **not** a GSP firmware image and is **not** a 610 + port; it neither authorises acquisition nor counts as analysis. +* This document, docs/22 and the current-version test runs are the only + 610-adjacent work permitted today. Everything else waits for approval. +* Approval is scoped and revocable: it covers named inputs and named + activities, on a named date, by a named approver. + +### 1.1 What approval does and does not cover + +| Approval covers | Approval does not cover | +|---|---| +| Acquiring the named 610 inputs through the named channel | Any other 610 artifact or channel not listed in the approval message | +| Analysis, probing, diffing and evidence capture on the 610 inputs | Merging an emulation change without the docs/22 A/B evidence | +| Running the docs/22 port workflow and the pipeline dry-run | Physical-GPU execution without a separate supervised plan | +| Publishing 610 evidence into the port log | Entering 610 as "Supported" in the matrix before validation | +| Re-asking for a scope extension | Retroactively blessing work done before approval | + +An approval with an open-ended scope is invalid; ask again with the specific +inputs and activities. A denied or expired approval leaves the repo in the +state described in §5. + +## 2. Readiness criteria (all must be true) + +Every row must pass on the **current anchor** (570.144) before approval is even +requested. "All must be true" is literal: a single open item keeps the gate +closed. + +| # | Criterion | How it is verified | Evidence | Status today | +|---|---|---|---|---| +| C1 | All tools implemented and tested on the current driver/firmware | full test suites green (documents/demo/probe/inject/Ghidra/harness) | test logs attached to the request | partial (MCP/CI missing) | +| C2 | Tests green | `make test` and the harness/rmharness suites pass | transcript with date and commit | green at v0.1 (55 tests) | +| C3 | Demo, probe, inject, Ghidra work | `./greeneye.sh demo`, `probe`, `inject`, `ghidra` all demonstrated | session dirs + transcript | working per docs/00 | +| C4 | MCP monitoring works | MCP plugin implemented and exercised (roadmap R3, docs/08; docs/15) | tool list + a monitored run | **not implemented (planned R3)** | +| C5 | CI pipeline works | release/CI pipeline implemented and exercised (planned docs/21) | pipeline run with artifacts | **not implemented (planned docs/21)** | +| C6 | Docs complete and accurate | doc review checklist (§3.2) signed | review record | to complete | +| C7 | Discovery/version-diff/CI exercised on the current version | control discovery (planned docs/19) + `harness/portability_matrix.py` + `compare` + pipeline dry-run on 570.144 | report + session hashes | partial (matrix/compare exist; docs/19 planned) | +| C8 | Harness produces a full evidence bundle | docs/07 checklist + docs/14 §6 pack produced end to end for a current-version port rehearsal | bundle directory with SHA-256 inventory | to complete | +| C9 | Human has approved | signed message (§6) | approval ID recorded in the request thread | pending | + +C4 and C5 are the long poles. Do not silently downgrade them: if the human +chooses to waive a criterion, the waiver is written into the approval message +with its risk and the affected scope. + +## 3. What "ready" looks like concretely + +### 3.1 Exact commands that must pass + +Run from `open-iov/debug/risc-v-gsp-rm-vm/` (docs/02 §8 has the make targets): + +```bash +make demo +make test +python3 -m unittest tests.test_harness -v +python3 rmharness/tests/test_rmharness_plugin.py +./greeneye.sh info +./greeneye.sh demo +./greeneye.sh demo --backend native +./greeneye.sh probe +./greeneye.sh inject +./greeneye.sh symbols export --index <export.tsv> --bias 0 -o symbols-<ver>.json +./greeneye.sh analyze dumps/<run> --symbols symbols-<ver>.json +./greeneye.sh compare dumps/<a> dumps/<b> --symbols +``` + +Plus, on the current version: + +```bash +python3 harness/portability_matrix.py \ + --target firmware/uproc_demo/build/uproc_demo.elf \ + --target firmware/uproc_demo/build/uproc_demo.elf \ + --out dumps/matrix-selfcheck +python3 harness/mutation_harness.py --out dumps/mutation-selfcheck \ + --iterations 12 --seed 7 +python3 harness/mutation_harness.py --out dumps/mutation-replay \ + --iterations 12 --seed 7 +``` + +Required results: + +* every suite green, with installed QEMU; skips are explained, not ignored; +* `portability_matrix` self-comparison reports **zero deltas** (`all_zero`); +* mutation `findings.jsonl` is byte-identical between the two seeded runs; +* `greeneye compare` on two identical runs is empty; +* `analyze` renders a coherent session (hook/watch/breakpoint events, UART + banner liveness per docs/06 §7). + +### 3.2 Doc review checklist + +* [ ] 00–18 match the code on the current commit (commands, flags, status + tables, test counts); +* [ ] `docs/22` porting guide is accurate for the current tools; planned items + are labelled `planned (docs/xx)`; +* [ ] `docs/19-CONTROL-DISCOVERY.md` (planned) exists and has been run once on + the current version, or C7 is explicitly waived; +* [ ] `docs/21-CI-RELEASE-PIPELINE.md` (planned) exists and has run once on the + current version, or C5 is explicitly waived; +* [ ] `GREEN-EYE-ROADMAP.md` and `docs/08` status claims match reality; +* [ ] no local source paths, no invented command names, no unsupported + "supported/accurate" claims. + +### 3.3 Golden sessions and regression data in place + +* [ ] golden demo session(s) checked in or reproducible by a documented seed; +* [ ] matrix self-comparison zero-delta output archived; +* [ ] mutation replay corpus archived with its seed and budget; +* [ ] rmharness host-oracle captures for the current anchor archived + (`rmtool engine list --format=json`, `mem stats --format=json`); +* [ ] a full current-version evidence bundle (docs/07 checklist + docs/14 §6) + produced once, as the rehearsal for the 610 run. + +## 4. Dry-run plan for the day of approval (no execution today) + +Executed only after the approval in §6 exists. Steps 1–2 are acquisition and +must not happen beforehand. + +1. **Confirm the approval.** Record approval ID, approver, date, scope and any + waived criterion in the port log. +2. **Obtain the 610 inputs through the approved channel.** The 610 driver + runfile and matching GSP `.fwimage` (or the approved firmware source) are + fetched through the channel named in the approval. Record the runfile + version, firmware hash (MD5/SHA-256), version string and image family + immediately; extract the `.fwimage` per docs/05-operations §4.1 step 1. +3. **Lay down versioned directories.** `firmware/<SERIES>-GSP/610/`, + `userspace/cuda/610/` (copied skeleton), `config/versions/610.yaml` + (documentation until the load path reads it). Never overwrite a working + version; keep 570.144 untouched. +4. **Pair and probe.** Build the 610 symbol DB (`analyze_fwimage.py`, + `symbols export --bias 0`), probe the 610 image and the 570.144 reference + with the same bounded settings, and diff the first-touch maps. Pairing + mismatch stops the run here. +5. **Baseline vs candidate orchestration.** Run + `harness/portability_matrix.py` with the 610 and 570.144 targets and + identical scenario options; run the mutation harness once per version with + the same seed; archive both reports and every session. +6. **Pipeline dry-run, then real.** Run the release pipeline (planned + docs/21-CI-RELEASE-PIPELINE.md) in its dry-run mode first; inspect the + planned artifact list without publishing. Only after the dry-run is clean, + run the real pipeline on the 610 evidence bundle. +7. **Expected artifacts.** + * approval record and input hashes; + * 610 symbol DB + Ghidra export provenance; + * probe reports (610 and reference) and the first-touch diff; + * `portability-report.json`/`.md`, per-pair compare reports, sessions; + * mutation `findings.jsonl` + summary for both versions; + * M1–M6 table with reference-capture method and L2/L3 limits; + * doc updates per docs/22 §3.5/§4 and the matrix entry (if and only if the + support claim is earned). +8. **Decision points.** Stop and keep the current anchor if: pairing is + ambiguous; any unexplained probe delta remains; the engine/RPC inventory + diff is not fully explained; M1–M6 regress on the current workload; the + evidence bundle is incomplete. Forward only when every delta has a decided + response. +9. **Rollback / keep-old-version rules.** The 570.144 directories, + configuration and guest driver stay untouched; rollback = return to the old + anchor commit/tag and the old guest driver (docs/05-operations §4.1 step 6). + A forward attempt never destroys the working setup; do not delete the old + version tree and do not reuse 570.144-derived addresses for 610. + +## 5. What must NOT happen before approval + +| Prohibited action | Why it is prohibited | +|---|---| +| Download/fetch 610 blobs or runfiles | Version discipline and evidence integrity: an unapproved input hash cannot anchor a later claim; acquisition is the act the gate exists to control. | +| Probe or decompile a 610 image | Analysis before readiness contaminates the evidence chain and tempts ad-hoc emulation edits. | +| Edit emulation code "for 610" | No current-version evidence justifies it; version-keyed handlers must be driven by a measured inventory diff (docs/05-operations §4.1 step 4). | +| Touch the physical GPU for 610 | Project hard rule: no unattended physical-GPU cycles; physical validation is a supervised step after approval. | +| Try the 610 driver in a guest | The guest never contains Open-IOV code and the anchor is fixed; an unpaired driver failure looks like an emulation bug (docs/07 P2.2). | +| Record 610 in the support/version matrix | Support means tested and documented; an untested entry violates the matrix contract (hardware-software-matrix §1/§6). | +| Treat the pinned open-module tag as approval | Build glue is not a GSP image and not an authorised port input (SBOM.md). | + +Permitted before approval: reading this document and docs/22, running all +current-version tests, producing and archiving the current evidence bundle, +and preparing the approval request. + +## 6. Sign-off checklist and approval message + +### 6.1 Human sign-off (one page) + +* [ ] **The RM 610 port begins only after explicit human approval.** +* [ ] C1–C9 all true, or each waiver is listed with its scope and risk. +* [ ] Test transcript attached (date, commit, `make test` green). +* [ ] `portability_matrix` self-comparison zero deltas; mutation replay + byte-identical. +* [ ] Doc review checklist (§3.2) complete. +* [ ] Current-version evidence bundle complete with SHA-256 inventory. +* [ ] Golden sessions/regression data in place (§3.3). +* [ ] Approval message (§6.2) sent and answered with an approval ID. +* [ ] Named approver accepts the acquisition scope and the permitted channel. + +``` +Approver: ______________________ Role: ______________________ +Approval ID: ____________________ Date: ______________________ +Scope limits / waivers: ________________________________________ +Signature: ______________________ +``` + +### 6.2 Approval request message (exact format) + +Send exactly this shape; fill every `<...>` and leave "Waivers" as "none" if +empty: + +``` +RM 610 PORT APPROVAL REQUEST + +Anchor today: 570.144 (firmware <sha256>; guest driver <md5>) +Target: RM 610 (requested input: <runfile version / approved source>) +Requested scope: acquire the 610 driver + GSP firmware through <approved +channel>; lay down versioned dirs; analyse, probe, diff, and run the docs/22 +port workflow; no product change before A/B evidence. + +Readiness, docs/23 §2: + C1 tools tested .......... <pass / waived> + C2 tests green ........... <pass / waived> + C3 demo/probe/inject/Ghidra <pass / waived> + C4 MCP monitoring ........ <pass / waived> + C5 CI pipeline ........... <pass / waived> + C6 docs complete ......... <pass / waived> + C7 discovery/diff/CI ..... <pass / waived> + C8 evidence bundle ....... <pass / waived> + C9 human approval ........ requested by this message + +Waivers: <none / list with reason> +Evidence bundle: <path> +Commit: <git rev>; transcripts: <paths> +Signed: <name/role>, <date> + +Decision (human): APPROVED / DENIED +Approval ID: <id>; Date: <date>; Scope limits: <text> +``` + +After approval, the 610 port proceeds only through the docs/22 workflow and +the docs/05-operations §4.1 version discipline. Until that message exists, +this repository is a 610 no-fly zone. diff --git a/debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md b/debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md new file mode 100644 index 0000000..4430bf9 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md @@ -0,0 +1,207 @@ +# 24 — Device map: first-touch MMIO and lenient native execution + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status. +> This is the H2.1 data layer and the first H2.2 building block, not a device +> model. It carries the same fidelity rules as `06-ACCURACY.md`: L3 remains +> **absent**; nothing here is a hardware, timing or security claim. + +## 1. Purpose and status + +`greeneye-device-map/1` records which MMIO addresses a firmware image touched +first, at which width and direction, with observed values, hit counts and +provenance. It is JSON data, versioned with the firmware that produced it, and +consumed by the native simulator's lenient MMIO mode. It exists so the R1 +device work (H2.2) starts from measured addresses instead of guesses, and so +the real GSP image can be stepped past its first unmapped access to collect +the *next* ones. + +| Piece | Where | +|---|---| +| map model, harvest, query, CLI | `greeneye/device_map.py` | +| lenient / fault MMIO policy | `sim/machine.py` (`Machine(device_map=..., unknown_mmio=...)`) | +| backend plumbing | `greeneye/sim_backend.py` (`launch_native(..., device_map=..., unknown_mmio=..., learn_mmio=..., bias=...)`) | +| tests | `tests/test_device_map.py` | + +## 2. The model + +An entry is one `(address, width, direction)` observation: + +| field | meaning | +|---|---| +| `address` | the MMIO address as accessed (64-bit; 61-bit GSP addresses fit without allocating memory) | +| `width` | access size in bytes (1/2/4/8) | +| `kind` | `"r"` or `"w"` | +| `values` | observed values (first-seen order, deduplicated); empty for probe-harvested reads because the QEMU log records the fault, not the value | +| `hits` | number of times observed at this key | +| `first_epc` | the first observing PC as executed (relocated) | +| `first_epc_fw` | `first_epc - bias`: the firmware's own virtual address; a PC of 0 (trap-vector fetch) stays 0 | +| `source` | `probe` (harvested), `manual` (hand-written), `trace` (learned at runtime) | +| `notes` | fault/exception provenance, e.g. `fault_load reason=rejected` | + +The file carries `meta` with the schema version, harvest/learn source, the +relocation `bias`, `entry`, the firmware path, its `sha256` (when the file is +readable) and `gsp_version.txt` (sanitised), plus `qemu_fault_count` when +harvested. Entries are always sorted by `(address, width, kind)` and the file +is written with `sort_keys=True`, so harvesting the same log twice is +byte-identical; there are no timestamps. + +`DeviceMap.lookup(addr, size, write)` returns a behaviour spec: + +* `value:0x...` — a read observed to return this value; +* `zero` — the address is known (or the direction is unknown) and reads back 0; +* `unimplemented` — no evidence at this address. + +Exact `(address, width, kind)` wins; a different width at the same address is +still evidence for the register. A write to a read-observed address is +absorbed; a read of a write-only register reads back zero. + +## 3. Harvesting from `greeneye probe` + +`greeneye probe` (unchanged) records `blobs/qemu.log` and the parsed +`qemu_first_fault` in `session.json`. Harvest reads either: + +```bash +# 1) run a bounded probe against the real image (QEMU path, existing command) +./greeneye.sh probe # writes dumps/<stamp>-probe-<image>/ + +# 2) turn that session into a device map +python3 -m greeneye.device_map harvest dumps/<stamp>-probe-<image> \ + --out dumps/570.144-device-map.json + +# 3) inspect / merge maps later, e.g. across firmware versions +python3 -m greeneye.device_map show dumps/570.144-device-map.json +python3 -m greeneye.device_map merge a.json b.json --out merged.json +``` + +Harvesting the recorded TU104 570.144 probe session +(`dumps/20261002-091222-probe-gsp-570.144.fwimage`) gives two entries — the +first-touch MMIO read and the `mtvec=0` fetch loop QEMU falls into afterwards: + +```json +{"schema": "greeneye-device-map/1", "entries": [ + {"address": 0, "width": 2, "kind": "r", "hits": 367551, "first_epc": 0, + "first_epc_fw": 0, "source": "probe", "notes": "fault_fetch reason=rejected"}, + {"address": 2305843009213696512, "width": 4, "kind": "r", "hits": 1, + "first_epc": 2214629396, "first_epc_fw": 67145748, "source": "probe", + "notes": "fault_load reason=rejected"}]} +``` + +`2305843009213696512 == 0x2000000000000A00`, `67145748 == 0x4009014`; `meta` +also carries the firmware sha256 +(`2d1bb5c6…`), `firmware_version: 570.144W` and `qemu_fault_count: 367552`. +Programmatic forms: `harvest_probe_session(session_dir)` and +`harvest_from_text(log_text, bias=...)` for arbitrary logs; `DeviceMap.save`, +`DeviceMap.load`, `merge`, `lookup`. + +## 4. Lenient MMIO in the native simulator + +`sim.machine.Machine` gained three knobs (all default to today's behaviour): + +```python +Machine(memory_mb=256, ram_base=0x80000000, + device_map=None, unknown_mmio="fault", learn_mmio=False, bias=0) +``` + +* `unknown_mmio="fault"` (default) — **byte-for-byte M1 parity**: an access + that is neither RAM nor UART faults exactly as before, and nothing is logged + or counted. +* `device_map=...` — mapped entries are served in *either* mode; a mapped + address that is unknown to the map is still `unimplemented`. +* `unknown_mmio="lenient"` — an unmodelled access returns the mapped + behaviour, else zero; it is appended to `machine.mmio_log` as + `{pc, addr, size, rw}` and counted in `machine.mmio_unknown`. No logging + occurs when lenient mode is off. Debugger `read_mem`/`write_mem` stay + strict. +* `learn_mmio=True` — lenient observations are written back into the map + (`source: "trace"`, read value `0`, write value as written); with no map + given, one is created lazily. Values recorded with `pc` and `bias`. + +Through the backend: + +```python +session = ge.launch(kernel=..., backend="native", + device_map="dumps/570.144-device-map.json", + unknown_mmio="lenient") +session.target.mmio_log # [{pc, addr, size, rw}, ...] +session.target.mmio_unknown # int +``` + +`launch_native(..., device_map=..., unknown_mmio=..., learn_mmio=..., bias=...)` +takes the same options; `device_map` may be a `DeviceMap` or a saved path, and +`bias` relocates images linked for GPU physical addresses. The lenient path is +only entered on a miss, so RAM/UART/trace throughput is unchanged; recording +is a plain list append and the map lookup is a dict hit. + +**What lenient proves, honestly:** an unmodelled address can be served a +mapped/zero value and execution continues deterministically; a bounded run +lists every unmodelled access and their PCs. It does **not** prove what a real +device returns, does not model read-back, side effects, interrupts or timing, +and a zero that keeps the firmware running is a hypothesis to be checked +against the H2.2 device model (and eventually hardware), not a finding about +silicon. + +## 5. Real-image demonstration (bounded, measured 2026-10-02) + +```bash +python3 -m greeneye.device_map demo --instructions 200000 +``` + +```text +image: /home/rmcore/Git/integdev_gpu_drv/open-iov/firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin +sha256: 2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea +elf: entry=0x4009000 vaddr=[0x0,0x5bb9000) +bias: 0x80000000 relocated entry: 0x84009000 +budget: 200000 instructions +stopped: reason=budget pc=0x84009070 (fw 0x4009070) +unknown mmio accesses: 1 + first unknown: pc=0x84009014 (fw 0x4009014) addr=0x2000000000000a00 size=4 rw=r +[device-map] qemu probe first fault: fw vaddr 0x4009014 reading 0x2000000000000a00 (load access fault) +[device-map] verdict: first-touch address reproduced; lenient execution continued for 200000 instructions (199994 past the qemu trap) +``` + +Comparison against the QEMU probe (`docs/06 §3`, still current): QEMU executes +six instructions and traps at firmware vaddr `0x4009014` on the 4-byte read of +`0x2000000000000A00`, then loops on `mtvec=0` fetch faults. The lenient native +run reproduces the same first access at the same PC (the `pc` in `mmio_log` is +the accessing instruction, not the run start), returns 0, and continues +through the entire 200 000-instruction budget, ending at firmware vaddr +`0x4009070` with **exactly one** unmodelled access logged. It does not fault. + +**61-bit addresses.** `0x2000000000000A00` is a valid 64-bit value in the +interpreter's address computation; no memory is allocated for it. RAM is +bounds-checked by `addr - ram_base`, the UART window by range, and everything +else goes to the device map / lenient handler. (`1 << 61` is well inside the +RV64 address width; the only care needed is that no map or device code ever +treats MMIO addresses as host indices.) `--out map.json` saves the learned +map; `python3 -m greeneye.device_map show map.json` renders it. + +## 6. How this feeds H2.2 + +* The harvested entries are the first test vectors for the `falcon`/`CSB`/ + `pmu_stub` windows in the docs/16 profile DSL: group entries by page + (`address & ~0xFFF`), take `lookup()` behaviour as the initial read-back, + and replace `zero` with real semantics one window at a time. +* `learn_mmio=True` is the bring-up loop: run lenient, inspect the learned + map + `mmio_log`, promote hand-verified entries to `source: "manual"`, and + re-run strict to see the next unmapped wall. Each promotion should carry a + note and the firmware hash, so the map is a reviewable artifact. +* The map's `bias`, firmware sha256/version and schema make it profile data + keyed by `(device, firmware-family, profile-version)`, as required by + docs/16 §7. +* The same maps are the YAML/JSON seed for the QEMU `-device` model, keeping + the two backends on one source of truth. + +## 7. API and commands + +| Call | Purpose | +|---|---| +| `harvest_probe_session(dir)` | session dir -> `DeviceMap` | +| `harvest_from_text(text, bias=0)` | arbitrary QEMU log -> `DeviceMap` | +| `DeviceMap.lookup(addr, size, write)` | `zero` / `value:0x...` / `unimplemented` | +| `DeviceMap.record(...)`, `merge(...)`, `save(path)`, `load(path)` | authoring/persistence | +| `python3 -m greeneye.device_map harvest\|show\|merge\|demo` | CLI | +| `python3 -m pytest tests/test_device_map.py` | 20 unit/parity/real-image tests | + +Testing honesty: the unit tests are synthetic (no QEMU); the real-image test +is bounded to 200 000 instructions and skips when the fwimage is absent. +`tests/test_sim_backend.py` remains the M1 parity guard and passes unchanged. 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. diff --git a/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md b/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md new file mode 100644 index 0000000..326533b --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/26-ARCHITECTURE.md @@ -0,0 +1,449 @@ +# 26 — green-eye architecture: full picture, workflow and API surfaces + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** +> The architecture map for the green-eye suite: how the pieces fit, which +> surface to call, and which document specifies each part. Every claim here is +> implemented in the tree or explicitly marked as a gated roadmap design. +> Development-only: never shipped, never a driver dependency, never in a guest. + +## 1. What green-eye is (and is not) + +green-eye runs GSP/uproc firmware in a controllable machine, observes it with +Frida-like primitives, records a durable session, changes the machine's inputs +(including injected code), and compares runs as evidence for `open-iov-vfio` +emulation changes. + +| It is | It is not | +|---|---| +| A firmware runtime microscope (execution + memory, L0/L1) | A time, interrupt or device model (L2/L3 absent — [06](06-ACCURACY.md)) | +| A scriptable `Session` API with two backends | A GDB wrapper; there is no GDB dependency | +| A recorded-evidence system with a versioned format | A product component or a guest agent | +| A static/dynamic join with Ghidra | A decompiler or a CUDA-corpus replacement | +| An automation layer (harnesses, MCP, CI) | A fuzzing service against hardware; simulator/re-hosted code only | + +## 2. The suite as a whole + +```text + front doors control plane evidence + CLI/REPL/scripts ─┐ + MCP (stdio) ──────┼─► GreenEyeAPI ─► Session ─┬─► QEMU-RSP backend ──┐ + Ghidra bridge ────┤ (plugins) Memory │ qemu-system-riscv64│ + harness/ ─────────┤ SymbolDB └─► native backend ────┼─► session dir + rmharness ────────┘ Recorder sim/machine.py │ events.jsonl + └─► analyze / compare / + export_hits / bundles +``` + +```mermaid +flowchart LR + CLI["CLI/REPL/scripts"] --> CORE + MCP["MCP server"] --> CORE + GH["Ghidra bridges"] --> CORE + HAR["harness/*.py"] --> CORE + RMH["rmharness (read-only)"] --> CORE + CORE["GreenEyeAPI + Session<br/>hooks, watchpoints, call, run_until<br/>Memory, SymbolDB, Recorder"] + CORE --> QB["QEMU-RSP backend"] --> SESS + CORE --> NB["native backend (M1/M1.5)"] --> SESS + SESS["session dir, greeneye-session/1"] --> REP["analyze / compare / export_hits / MCP / bundles"] + RMH --> SESS +``` + +Status: transport, core, recording, both backends (native M1 trace included), +injection ABI v1, both Ghidra bridges, traces, device map, harnesses, MCP (R3) +and `rmharness` are implemented and exercised. The `Backend` protocol +refactor (16 §5), the H2.2 QEMU device model, RM-core injection (H1.3) and the +H3 M2–M6 milestones are design/roadmap only ([16](16-CUSTOM-SIMULATOR.md)). + +## 3. Session core: hooks, watchpoints and dispatch + +`Session` ([greeneye/session.py](../greeneye/session.py)) owns all stop-driven +state and is backend-agnostic: every stop reads registers, resolves the PC, +records an event, invokes matching hooks/watchpoints, then resumes. + +```mermaid +sequenceDiagram + S->>FW: set breakpoint / watchpoint + loop run(duration, max_stops) + FW-->>S: stop reply (T/S/W) + S->>S: resolve PC via SymbolDB + S->>R: bp_hit / watch_hit / stop / step + alt symbol entry + S->>H: on_enter(hit) with a0..a7 + S->>S: arm return breakpoint from ra (LIFO) + S->>FW: continue + else return breakpoint + S->>H: on_leave(hit), a0 as retval + else watchpoint (Z2/Z3/Z4) + S->>FW: clear watch, step over store, re-arm + S->>H: callback + store PC + end + S->>R: on_event(kind, hit) + end + S->>R: session_end on close +``` + +| Primitive | Semantics / limits | +|---|---| +| `breakpoint(addr, kind=sw\|hw, temporary=)` | sw is stepped over transparently; hw uses the backend's `Z1` | +| `hook(target, on_enter, on_leave)` | leave is a return breakpoint on `ra`; tail calls/interrupt returns are not tracked | +| `watch(target, size, kind=w\|r\|a, callback)` | QEMU stops *at* the access; Session clears/steps/re-arms; store PC is recorded | +| `run(duration, max_stops)` | services stops until a budget or exit; the deadline path interrupts once | +| `run_until(target, timeout)` | temporary breakpoint; raises `RspTimeout` when not reached | +| `call(target, args, timeout)` | Frida-like remote call; restores `RESTORE_REGS` around it | +| `step(n)`, `note(text)`, `on_event(kind, fn)` | single-step with dispatch; annotate and observe the stream | + +Instructions between stops are invisible unless the TCG plugin +(`qemu/greeneye_trace.c`) or the native trace (`sim/trace.py`) is on +(01 §11, 16 §1.1a). + +## 4. Backend abstraction: QEMU-RSP and native + +`Session` assumes a small RSP-shaped primitive set; `run_until` and `call` +live above it and work on every backend. M1 implements the same method +surface (`greeneye/sim_backend.py`) without the planned `Backend` protocol +refactor (16 §5); the table is the M2+ target. + +```mermaid +flowchart LR + S["Session (backend-independent)"] + S -->|"read/write reg+mem, step, continue, Z0-Z4"| RC + S -->|"same surface"| ST + subgraph QEMU["QEMU-RSP backend (default)"] + QT["QemuTarget<br/>qemu-system-riscv64 virt"] --- RC["RspClient gdbstub packets"] + end + subgraph NAT["Native backend (--backend native)"] + ST["SimTarget / SimClient"] --- MC["sim.machine.Machine<br/>RAM + UART + decode loop"] + MC --- TR["sim.trace.Trace exec/access"] + MC --- DM["DeviceMap lenient MMIO"] + end +``` + +| Session need | QEMU-RSP | Native | +|---|---|---| +| registers/memory | `g`/`p`/`P`, `m`/`M`/`X` chunked to `PacketSize` | direct register file and memory map | +| breakpoints/watchpoints | `Z0`–`Z4` | dispatch-loop tables; same stop semantics | +| snapshots | region copies over RSP | direct map copy; machine moments are M2+ | +| virtual time | host wall clock | deterministic instruction count (`SIM_IPS`) | +| full-access trace | TCG plugin (`-plugin`) | in-process `sim/trace.py`, `--trace` | +| device environment | QEMU `virt` only (no TU104) | device map + lenient MMIO; real devices H2.2 | +| injection | debugger writes + `Session.call` | same SDK ABI; RM interception is M5 | + +`--backend qemu|native` is accepted by `demo`, `run`, `attach`, `record`, +`inject` and `ghidra`; `attach --port` and QEMU `--trace` are QEMU-only; +`--trace` is native-only (16 §1.2). + +## 5. Recording and the evidence format + +A session directory is the durable artifact (`greeneye-session/1`, +[greeneye/record.py](../greeneye/record.py)). The event stream is the map; the +snapshot and blob files are the dump. + +```text +dumps/<stamp>-<name>/ + session.json # format, name, created, counts, snapshot inventory, meta + events.jsonl # append-only JSON lines: {"t": monotonic, "type": ...} + snapshots/*.bin.z # zlib-compressed region captures (sha256 recorded) + blobs/* # QEMU logs, trace.jsonl, rmharness stdout, plugin dumps + summary.md # rendered analysis +``` + +```mermaid +flowchart LR + E["Session stops, memory ops,<br/>probe/plugin/injection events"] --> R["Recorder.event()"] + R --> J["events.jsonl (append-only)"] + M["Memory.snapshot()"] --> Z["snapshots/*.bin.z + sha256"] + B["Recorder.blob()"] --> BD["blobs/*: trace.jsonl, qemu.log"] + X["rmharness external runs"] --> J + J --> A["analyze: summary.md"] + J --> C["compare: A/B delta report"] + J --> G["export_hits: greeneye_hits.tsv"] + J --> S10["value_demos s10: manifest + M1-M6 skeleton"] +``` + +Event classes: core stop/memory events (`session_start`, `image_loaded`, +`hook`, `watch`, `bp_hit`, `leave`, `watch_hit`, `stop`, `step`, `mem_read`, +`mem_write`, `snapshot*`, `probe*`, `plugin*`, `note`, `exit`, `session_end` — +01 §6); injection (`service_injected`, `call_enter`, `call_return` — 12 §6); +traces (`exec`/`access`); M2/M3 native bus (`device`/`irq`/`schedule` — 16 +§10); `rmharness` (`rm_harness_run`, `t=0.0` with epoch `start`/`end` — 17 §5). + +Format invariants: append-only (partial sessions stay readable); additive +versioning with migration notes (V2); `compare` treats `t`, host paths and +backend identity as non-semantic; a claim without a session is not evidence +(06 §7, 25 §1). + +## 6. Injection SDK lifecycle + +The SDK (`sdk/greeneye_service.h`, `sdk/injected_demo.c`) builds +position-independent ELF64 `EM_RISCV` blobs; `greeneye/inject.py` loads, +registers and invokes them. ABI v1: `ge_u64 (*)(ge_u64 cmd, ge_u64 a0, +ge_u64 a1)`, magic `0x47455356`, entry `ge_demo_service_dispatch`. + +```mermaid +sequenceDiagram + Dev->>Inj: load_service(elf, slot=6, load_addr) + Inj->>FW: write PT_LOAD segments into RAM + Inj->>FW: patch gsp_ctrl.handlers[slot] entry + Inj->>Rec: service_injected + mem_write events + Dev->>Inj: invoke_remote(cmd, a0, a1) + Inj->>FW: Session.call(entry, args) + Rec-->>Rec: call_enter / call_return + Dev->>FW: mailbox = 16 + slot (arg0=cmd, arg1, arg2) + FW->>FW: dispatch handlers[slot] + FW-->>Rec: hook/watch events + result field +``` + +Demo registration: mailbox commands 16..23 select slots 0..7; slot 6 is the +SDK default; control-block offsets (`mailbox +0x20`, `handlers +0x150`) are +demo-specific and must come from `greeneye/inject.py` constants (12 §4). +Cleanup is not automatic in v1; record the injection so runs start from a +known state. Roadmap: RM-core shim, service registry and `log`/`stub`/ +`forward` interception at M5/H1.3 (12 §8, 16 §11). + +## 7. Ghidra bridges (two directions) + +The static/dynamic join uses three address spaces — file vaddr, Ghidra program +addr, runtime addr — with a bias per source (01 §5, 05 §1). + +```mermaid +sequenceDiagram + G->>B: {"id":n,"cmd":"run_until","args":{"target":"0x..."}} + B->>S: run_until(addr) (temporary breakpoint) + S->>T: continue; service intervening stops + T-->>S: stop at addr + S-->>B: {addr, symbol, kind} + B-->>G: JSON response + G->>B: read_mem / read_reg / call / snapshot / hits / export_hits + B->>R: hits and notes recorded in the session + R-->>G: greeneye_hits.tsv (via MarkGreenEyeHits.java) +``` + +| Tool | Purpose | Doc | +|---|---|---| +| `symbols export` + `SymbolDB` loaders | Ghidra TSV/JSON → runtime symbol DB | [05](05-GHIDRA-BRIDGE.md) | +| `greeneye.ghidra.export_hits` | session hits → `addr<TAB>hits<TAB>symbol<TAB>annotation` | 05 | +| `ExportForGreenEye.java` | Ghidra → `addr<TAB>name<TAB>size` TSV | 05 | +| `MarkGreenEyeHits.java` | annotate functions with runtime hits + bookmarks | 05 | +| `GreenEyeControl.java` + `greeneye.sh ghidra` | drive a live session from GhidraScripts over localhost JSON-lines | [13](13-GHIDRA-CONTROL.md) | + +Bridge commands: `ping`, `info`, `resolve`, `bp_set`, `bp_clear`, `bp_list`, +`watch_set`, `run_until`, `run`, `step`, `read_mem`, `write_mem`, `read_reg`, +`write_reg`, `call`, `snapshot`, `note`, `hits`, `export_hits`, `close` +(13 §3). Unauthenticated by design; localhost only. + +## 8. MCP server tool surface + +`plugins/mcp_server.py` serves the active `Session` over JSON-RPC 2.0 on +stdio (or loopback TCP) with no MCP library; read/observe by default, +`write_mem`/`call_function` require `--allow-write`, `inject_service` +requires `--allow-inject` ([20](20-MCP-SERVER.md)). + +```mermaid +flowchart LR + AG["AI agent (Claude Code, opencode)"] -->|"JSON-RPC 2.0 over stdio"| M["plugins/mcp_server.py"] + M --> READ["read: session_info, symbols_resolve,<br/>read_mem, hexdump, snapshot, plugins_list"] + M --> OBS["observe: bp_set/clear, watch_set,<br/>run, run_until, step (recorded)"] + M --> WR["write (gated): write_mem, call_function"] + M --> INJ["inject (gated): inject_service"] + M --> TR["trace: trace_start/stop"] + M --> AN["analysis: analyze_current, compare_sessions, export_hits"] + M --> HN["harness: probe_firmware, mutation_run,<br/>portability_matrix_run"] + M --> CL["session: close"] + READ & OBS & WR & INJ & TR & AN & HN --> S["Session + Recorder"] +``` + +All results are one JSON text block; absent modules return a structured +`not_available` error; stdout carries protocol frames only (20 §4, §8). Full +argument schemas for all 24 tools are in 20 §5. + +## 9. Automation harness + +`harness/` turns the playbooks into repeatable, self-describing runs. Modules +bootstrap the tool root and use the QEMU backend by default (18, 19, 21); +`common.py` provides flat-image relocation, session helpers and bounded log +reads. + +```mermaid +flowchart TB + FW["firmware ELF / .fwimage"] --> PM & MU & CD & RE & VD & LV + SYM["Ghidra exports + symbol DBs"] --> PM & MU & CD & RE & VD & LV + STIM["stimulus / probe spec"] --> PM & MU & CD & RE & VD & LV + SEED["seed + budgets"] --> PM & MU & CD & RE & VD & LV + subgraph H["harness/"] + PM["portability_matrix.py R6/H4.1"] + MU["mutation_harness.py R5/H4.3"] + CD["control_discovery.py H4"] + RE["reach.py H4.1/H4.5"] + VD["version_diff.py H4.1"] + CR["ci_release.py"] + LV["value_demos/ s01..s10"] + end + VD --> CR + PM --> CR + MU --> CR + PM & MU & CD & RE & VD & LV & CR --> OUT["reports + sessions + findings.jsonl<br/>+ compare/ + bundle-manifest.json"] +``` + +| Entry point | Purpose | Outputs | Doc | +|---|---|---|---| +| `portability_matrix.py` | probe + symbols + fixed scenario + compare across targets; M1–M6 skeleton | `portability-report.json/.md`, `symbols/`, `sessions/`, `compare/` | 18 §1 | +| `mutation_harness.py` | seeded one-input mutation, outcome classes, per-attempt session | `findings.jsonl`, `summary.md`, `sessions/` | 18 §2 | +| `control_discovery.py` | sweep command/call surfaces; classify handler/default/trap/fault | `findings.jsonl`, `discovery-report.json`, `control-discovery.md` | 19 §3 | +| `reach.py` | reachability, trigger discovery, required args and effects for one symbol | `reach-report.json/.md`, `probe.json`, sessions | 19 §4 | +| `version_diff.py` | old vs new image: shape, symbols, corpus hints, probe, behavior, M1–M6 | `version-diff.json/.md`, `corpus/`, `compare/` | 21 §2 | +| `ci_release.py` | version_diff → matrix → mutation smoke, then hash the bundle | `release-report.json`, `bundle-manifest.json` | 21 §4 | +| `value_demos/run_all.py` | ten fixed scenarios (reply, counters, discovery, reach, diff, inject, version, mutation, Ghidra, evidence) | `artifacts/results.json` + per-demo JSON | 25 | +| `common.py` | shared setup (relocate to `0x80000000`, open session, bounded logs) | library | 18 §1.2 | + +Harness limits are intentional: QEMU/re-hosted/demo code only; no hardware, +guest, kernel module or existing gdbstub; bounded budgets; deterministic +findings for a fixed seed (18 §5, 19 §8, 21 §7). + +## 10. Device map and lenient MMIO data flow + +`greeneye-device-map/1` is versioned JSON keyed by +`(address, width, direction)` with values, hit counts and firmware PCs +([24](24-DEVICE-MAP.md)). It records what the firmware touched first and lets +the native interpreter step past unmodelled registers to collect the next +ones; it feeds the H2.2 QEMU device model and the H3 profile DSL. + +```mermaid +flowchart LR + P["greeneye probe (raw .fwimage in QEMU)"] -->|"blobs/qemu.log + qemu_first_fault"| H["device_map.py harvest"] + H --> MAP["greeneye-device-map/1<br/>entries + meta (bias, sha256)"] + MAP --> LOOK["DeviceMap.lookup<br/>zero / value / unimplemented"] + LOOK --> LEN["Native Machine(unknown_mmio=lenient)"] + LEN --> LOG["machine.mmio_log / mmio_unknown"] + LEN --> LEARN["learn_mmio=True: source trace entries"] + LEARN --> MAP + MAP --> PROF["docs/16 profile DSL<br/>falcon/csb/ptimer/pmu stubs"] + PROF --> H22["H2.2 QEMU device model"] + LOG --> NEXT["next unmapped wall; promote to manual"] + NEXT --> MAP +``` + +Honest scope: lenient execution proves an unmodelled access *can* be served +deterministically; it does not model read-back, side effects, interrupts or +timing, and a zero that keeps the firmware running is a hypothesis, not a +silicon fact (24 §4). + +## 11. Workflow architecture: scenario → evidence → emulation change + +The canonical loop (H4.5, 06 §5, 07 P4, 25 §7). Each stage names its gate. + +| Stage | Action / instrument | Success criterion | Failure signal | +|---|---|---|---| +| 1. Scenario | state the firmware question and the smallest stimulus (one command/control/boot phase) | question maps to a symbol or surface | no observable exists → add one (probe/plugin) | +| 2. Choose instrument | hooks/watchpoints for semantics; TCG/native trace for coverage; `run_until`/`call` for reachability; injection for new code | stop set answers the question without flooding | session too large/noisy → filter PCs, watch fields only | +| 3. Run | fresh bounded session; fixed stimulus, seed and budgets | run completes; UART/banner liveness holds | timeout, trap loop, wedged target → capture and retry clean | +| 4. Record | `greeneye-session/1` with snapshots and notes | events + snapshots + hashes written | missing `session.json`/empty analyzer → recording off | +| 5. Compare | `compare` against a reference (old firmware, hardware/VFIO capture, self-diff) | deltas explained symbolically; self-diff is zero | unexplained hit/watch/write delta → investigate first | +| 6. Decide | fill M1–M6 with the reference method (06 §5); mutation/triage for error paths | metric table improves or stays equal | metric claimed without a reference capture | +| 7. Implement | change `open-iov-vfio` (never the simulator for a product fix); keep the stimulus | physical confirmation for behaviour changes (06 §6, 14 §7) | product change from a simulator-only observation | +| 8. Regression | re-run step 3 with an unchanged stimulus; golden sessions/CI (`make test`, `greeneye.yml`) | profile improves or stays equal; CI green | regression → revert and re-derive | + +```mermaid +flowchart LR + Q["Question"] --> OBS["Observe<br/>probe / hooks / watch / trace"] + OBS --> ORI["Orient<br/>analyze + symbol decode + snapshots"] + ORI --> DEC["Decide<br/>compare + M1-M6 + mutation"] + DEC --> ACT["Act<br/>implement in open-iov-vfio"] + ACT --> REG["Regression<br/>same stimulus, same budgets"] + REG -->|"improved / equal"| SHIP["Evidence pack<br/>sessions + report + hashes"] + REG -->|"regressed"| Q + SHIP --> OBS +``` + +OODA compression evidence (analog, measured) is in 25 §7: hardware cycles are +minutes and campaigns hours/days; the green-eye path for the same question +class is sub-second to seconds plus scripting. + +## 12. API surfaces by tool + +### 12.1 CLI (`greeneye.sh`, `greeneye/cli.py`; usage in [02](02-USAGE.md)) + +| Command | Purpose | Not for | +|---|---|---| +| `info` | environment/tooling check | validating a firmware image | +| `demo` | canned boot + hooks + watch + record on the bundled uproc | booting real GSP (use `probe`) | +| `run <script.py>` | run a Python instrumentation session | shell pipelines (write Python) | +| `attach` / `launch` | interactive REPL / keep a target alive by port | injecting into a live product | +| `record` | boot and record with watches/snapshots/trace | post-processing an existing session | +| `probe <image>` | bounded run of a raw image; first divergence report | booting the image (no TU104 model) | +| `inject` | load/register/invoke an SDK service | shipping or hot-patching signed firmware | +| `ghidra` | serve a live session to GhidraScripts | network exposure (localhost only) | +| `analyze <session>` / `compare <a> <b>` | markdown report / symbolic A/B delta | proving accuracy without a reference | +| `symbols export\|resolve` | build/inspect a symbol DB | recovering types/decompilation | +| `plugins` | list discoverable plugins | loading untrusted code into the core | +| `python3 -m greeneye.device_map harvest\|show\|merge\|demo` | MMIO first-touch map data | a device model or timing source | + +### 12.2 Python entry points (recipes in [03](03-SCRIPTING.md)) + +| Entry point | Purpose | Not for | +|---|---|---| +| `ge.launch/attach`, `Session.hook/watch/run/run_until/call/step/note` | session lifecycle and instrumentation | in-process C instrumentation | +| `Memory.read/write/hexdump/search_*/snapshot/diff/render_diff` | memory inspection and region diff | full-machine snapshots (M2+) | +| `SymbolDB.load_elf/*_tsv/*_json`, `resolve/annotate` | address-space-aware symbols | type recovery from Ghidra | +| `structs.schema_from_json/decode/render` | declarative struct decoding | automatic struct discovery | +| `probes.load_spec/install` | JSON probe specs (no arbitrary code) | complex control flow (write a script) | +| `inject.load_service/invoke_remote/invoke_via_firmware` | ABI v1 service load/invoke | product features or persistence | +| `ghidra.build_symbol_db/export_hits/corpus_location` | Ghidra TSV in/out and corpus location | replacing Ghidra analysis | +| `analyze.render_summary/compare_sessions/parse_qemu_faults/load_trace` | offline reports and QEMU fault parsing | live control | +| `record.Recorder/load_events/load_meta` | session writing/reading | a database (files are the interface) | +| `device_map.harvest_*/DeviceMap.*` | MMIO map authoring/merge | device semantics | +| `sim_backend.launch_native`, `sim.trace.Trace/make_trace` | native backend + trace knobs | timing/concurrency claims | +| `errors.*` (`GreenEyeError`, `RspTimeout`, ...) | typed failure surface | retrying a wedged target blindly | + +### 12.3 Plugin API (`register(ge)`, [04](04-PLUGINS.md)) + +`ge` exposes live objects (`session`, `memory`, `symbols`, `recorder`), +instrumentation (`hook`, `watch`, `on_event`, `resolve`), CLI extension +(`add_command(name, fn, help)`, `commands`) and audit logging (`log`). A +raising plugin is recorded as `plugin_error` and skipped. Not for: shipping +dependencies, assuming a specific firmware, or hiding unrecorded state. + +## 13. Extensibility and invariants + +Where extension lives: + +* **Plugins** (`plugins/`, `GREENEYE_PLUGINS`) — runtime RE tools, harnesses, + environment updates, agent integrations (04). +* **Harness modules** (`harness/`) — new sweeps/reports over the public + `Session`/analysis primitives; a new call surface is a `Surface` subclass + (19 §5.1). +* **SDK services** (`sdk/`) — PIC blobs behind ABI v1; v2 adds an RM-core call + table (12 §8). +* **Device maps/profiles** — versioned data (`greeneye-device-map/1`, the + `greenmachine-profile/1` DSL in 16 §7), never core special cases. +* **MCP tools** (`plugins/mcp_server.py`) — add tools in the plugin, keep the + core stdlib-only (20 §10). + +Invariants (enforced by tests or review): + +1. **stdlib-only core**: `greeneye/` and `sim/` import only the standard + library; optional deps live in plugins/harnesses. +2. **Session format**: `greeneye-session/1` is append-only and evolves + additively with migration notes (V2). +3. **Backend parity**: every backend satisfies the same `Session` primitives, + so scripts, probes, plugins, recordings and comparisons survive backend + changes (16 §4, R0). +4. **ABI**: blobs declare an ABI version and capability metadata; breaking + changes need a shim and a doc note (12 §8, V3). +5. **Markers**: the `TOOL IN DEVELOPMENT` banner stays until the acceptance + list in 00 passes; never shipped, never in a guest, never a driver + dependency. +6. **Evidence**: no accuracy, portability or security claim without a recorded + A/B session and an explicit L2/L3 fidelity statement (06 §5–6, 25 §10). +7. **Provenance**: tracked docs never name local source trees; public + descriptions first, local provenance in the local notes (V6). + +## 14. Diagram and document index + +Section diagrams and their backing docs: §2 suite (00, 01, README); §3 +dispatch (01, 03); §4 backends (01 §8, 16 §4–5); §5 recording (01 §6, 09, 12 +§6, 16 §10, 17 §5); §6 injection (11, 12); §7 Ghidra (05, 13); §8 MCP (20); +§9 harness (18, 19, 21); §10 device map (24, 16 §7); §11 OODA (06, 07 P4, 25 +§7). Roadmap keys: R3 = MCP (08), R5 = mutation (18), R6 = multi-version (18, +21), H1.3 = RM service injection (GREEN-EYE-ROADMAP), H2.2 = QEMU device +model, H3/M1–M6 = native simulator (16), H4 = adversarial navigation (14), +H7 = agentic platform (15, 20). diff --git a/debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md b/debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md new file mode 100644 index 0000000..e208253 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/docs/27-TOOL-AUDIT.md @@ -0,0 +1,252 @@ +# 27 — green-eye tool audit + +> **# TOOL IN DEVELOPMENT - NOT READY YET!** See [00-OVERVIEW](00-OVERVIEW.md) +> for status and [06-ACCURACY](06-ACCURACY.md) for fidelity limits. +> Development-only: never shipped, never a driver dependency, never in a guest. + +Audit date: 2026-10-02. Scope: every runnable surface in +`debug/risc-v-gsp-rm-vm/` (CLI, Python package, `sim/`, `harness/`, +`plugins/`, `qemu/`, `ghidra/`, `rmharness/`, `sdk/`, firmware demo, +`examples/`, `tests/`, docs). The goal is that every tool is either exercised +somewhere, documented well enough to be unmistakable, or explicitly marked +`not used yet`. + +## 0. Method (what was actually run) + +Environment: QEMU 8.2.2 (`qemu-system-riscv64`), Python 3.12, clang/lld. +Every safe smoke below was executed from the tool root; device/WPR paths were +not touched. + +* all 13 `./greeneye.sh <cmd> --help` parsers, `--version`; +* `info`, `demo --backend native`, `record --backend native --trace`, + `analyze`, `compare`, `plugins`, `inject --backend native`; +* `probe --instructions 400` against the real 570.144 `.fwimage`; +* `launch` (bounded, QEMU killed afterwards) and the attach REPL path via + `tests/test_injection.py`; +* `symbols export` from a synthetic Ghidra index TSV, then `symbols resolve`; +* `run` of every `examples/scripts/*.py` as documented, plus the declarative + probe spec `examples/probes/demo_probes.json`; +* `make demo`, `make sdk`, `make -C qemu` (plugin already built) and a live + QEMU run with `qemu/greeneye-trace.so` (8,012,129 JSONL events in ~1 s); +* `harness/*.py --help`; `harness/value_demos/run_all.py --only s02,s05`; + `examples/scripts/sim_bench.py`, `sim_diff.py`; +* `plugin --help` / MCP suite; `rmharness/*.sh --help`, + `rmharness/harness.sh` capture with `rmtool gpu list` (read-only `/proc`); + `python3 rmharness/tests/test_rmharness_plugin.py` (3 tests); +* `python3 -m greeneye.device_map --help` and `demo` (bounded 50k + instructions, lenient MMIO); +* full suite: `python3 -m unittest discover -s tests -t .` → **155 tests, OK**; +* `python3 scripts/check-docs-links.py` from `open-iov/` → **OK, 55 files**; +* `git check-ignore -v` for the local-only ignore patterns. + +Status vocabulary: `verified useful` (exercised and/or wired into +tests/demos/docs), `not used yet` (no caller found; decision recorded), +`needs improvement` (concrete weakness; fixed where safe), `prune candidate` +(clearly dead; none found). + +## 1. Master table — `greeneye.sh` CLI + +| surface | purpose | API / entry points | tests | used by | status | notes/actions | +|---|---|---|---|---|---|---| +| `info` | environment/tooling check | `greeneye/cli.py:173`, `greeneye.sh info` | indirect (smoke) | README, 00, 02 | verified useful | ran; native sim + fwimage + CUDA names detection all correct | +| `demo` | canned demo boot + hooks/watch/diff/trap | `cli.py:209` | smoke via `test_smoke_qemu.py` | README, 00, 02; `make demo-run` | verified useful | ran on `--backend native`; 17 hook lines, diff + trap work | +| `run` | run a Python instrumentation script | `cli.py:272` | `test_injection.py` (REPL), `test_sim_trace.py` | 02, 03, examples scripts | needs improvement (fixed) | the exec namespace lacked `__file__`, so the documented `dump_diff.py` failed; namespace now sets `__file__`/`__name__`, doc 03 notes it | +| `attach` | interactive REPL | `cli.py:486`, `GreenEyeRepl` | `test_injection.py:68` | README, 02 | verified useful | REPL commands all back onto tested Session APIs | +| `launch` | start target, keep alive for attach | `cli.py:509`; `qemu/launch.sh` | smoke (bounded run) | README, 02 | verified useful | prints gdbstub endpoint; QEMU survives Ctrl-C by design | +| `probe` | run arbitrary/real firmware, report first divergence | `cli.py:551`; `test_probe.py` unit | `test_probe.py` (ELF/flat/fault parse) | README, 00, 06, 24 (`harvest` input) | verified useful | ran vs real fwimage: first fault `fault_fetch @0x0`, recorded session evidence | +| `record` | record a memory-activity session (+ native trace) | `cli.py:529` | `test_sim_trace.py` | README, 02, 03 | verified useful | ran with `--trace`; trace blob + watch events rendered by `analyze` | +| `analyze` | session → symbol-annotated report | `cli.py:690`, `greeneye/analyze.py` | `test_sim_trace.py`, `test_smoke_qemu.py` | 02, 03, 05, 13, 14, 25 | verified useful | ran on recorded session | +| `compare` | A/B recorded sessions | `cli.py:704`, `analyze.compare_sessions` | `test_sim_backend.py`, `test_value_demos.py` | 02, 03, 14, 25 | verified useful | ran demo-vs-record; correct profile deltas | +| `symbols export\|resolve` | build/inspect symbol DB | `cli.py:711` | `test_core.py` (loaders) | 05, README | needs improvement (fixed) | bad `--db` JSON produced a traceback; `SymbolDB.load_json` now raises a clean `GreenEyeError` (exit 2) | +| `plugins` | list discoverable plugins | `cli.py:733` | smoke | README, 04 | verified useful | lists all three bundled plugins | +| `inject` | load + call an injected RV64 service | `cli.py:630`; `greeneye/inject.py` | `test_injection.py` | 11, 12, 14 | verified useful | ran native: direct call and firmware mailbox both `0xbeae` | +| `ghidra` | serve session to GhidraScripts | `cli.py:660`; `greeneye/gdbridge.py` | `test_bridge.py` (full protocol) | 05, 13, 14 | verified useful | bridge protocol covered live over a socket; Java side needs Ghidra | + +## 2. Master table — Python API (`greeneye/*.py`) + +| surface | purpose | API / entry points | tests | used by | status | notes/actions | +|---|---|---|---|---|---|---| +| `__init__.py` | public API: `launch`, `attach`, exports | `ge.launch(backend=, trace=, ...)` | all integration tests | README, 02, 03 | verified useful | stable-ish v0.1 surface, documented | +| `errors.py` | exception taxonomy | `GreenEyeError` + subclasses | everywhere | 03 §13 | verified useful | small and meaningful | +| `rsp.py` | GDB-free RSP client | `RspClient`, `StopReply` | `test_rsp.py`, `test_mcp.py` | 01, 03 | verified useful | fake-stub unit tests | +| `elf.py` | ELF64/32 reader, flat kernel writer | `load_elf`, `write_flat_kernel`, `write_relocated_elf`, `FirmwareImage` | `test_core.py` (`write_relocated_elf` test added), `test_probe.py`, `test_device_map.py` | 06, 12 | verified useful | `write_relocated_elf` had no caller; a relocation test now locks its entry/PT_LOAD shift | +| `symbols.py` | symbol DB, address translation, TSV/JSON loaders | `SymbolDB.get/lookup/resolve/annotate/by_subsystem`, `Symbol` | `test_core.py` (`by_subsystem` + list/bad JSON tests added), `test_mcp.py`, `test_memory_record.py` | 03, 05 | verified useful | `load_json` list payloads worked by accident? no — they crashed; fixed and tested; parse errors now `GreenEyeError` | +| `memory.py` | read/write/hexdump/search/snapshot/diff | `Memory`, `Diff` | `test_memory_record.py` | 03 §5/§7 | verified useful | full docs in 03 | +| `structs.py` | schema-driven struct decode/render | `schema_from_json`, `Schema`, `render` | `test_core.py` | 03 §6 | verified useful | docs example matches demo schema | +| `record.py` | durable session: events.jsonl/snapshots/summary | `Recorder`, `NullRecorder`, `load_events` | `test_memory_record.py` | 01, 02, 03 | verified useful | `rmharness/harness.sh` also appends compatible events | +| `session.py` | hooks/bp/watchpoints/events/step/call | `Session`, `hook`, `watch`, `run`, `call` | `test_injection.py`, `test_mcp.py`, `test_bridge.py` | 01, 03, everywhere | verified useful | `Session.symbol_at` is a thin alias of `symbols.resolve` with no caller yet; docstring added, kept for API symmetry | +| `target.py` | QEMU process lifecycle + UART/dlog | `QemuTarget`, `find_qemu`, `free_port` | `test_smoke_qemu.py`, `test_injection.py` | 01, 02 | verified useful | `launch`/`probe` both exercise it | +| `sim_backend.py` | in-process native backend (M1) | `launch_native`, `SimClient`, `SimTarget` | `test_sim_backend.py`, `test_sim_trace.py`, `test_device_map.py`, `test_mcp.py` | README, 16, 24 | verified useful | QEMU-parity differential test | +| `analyze.py` | report renderer, fault parser, A/B compare | `render_summary`, `compare_sessions`, `describe_fault` | `test_sim_trace.py`, `test_probe.py`, `test_smoke_qemu.py` | 02, 03, 06, 25 | verified useful | consumed by CLI and value demos | +| `device_map.py` | versioned first-touch MMIO map + lenient policy | `python3 -m greeneye.device_map {harvest,show,merge,demo}` | `test_device_map.py` | 24 | verified useful | `demo` reproduced the documented first GSP access (lenient run 50k insns) | +| `gdbridge.py` | JSON-lines Ghidra control server | `GhidraBridge`, `cmd_*` | `test_bridge.py` | 05, 13 | verified useful | 25 commands; protocol tested live | +| `ghidra.py` | symbol import / hit export | `build_symbol_db`, `export_hits`, `corpus_location`, `render_ghidra_command` | `test_core.py` (TSV loaders), `test_value_demos.py` s09 (`export_hits`), `test_bridge.py` (`render_ghidra_command` test added) | 05, 13, 25 | verified useful | `render_ghidra_command` now has a test and is cited from 05 §4 | +| `inject.py` | position-independent service load/call | `load_service`, `invoke_remote`, `invoke_via_firmware` | `test_injection.py` | 11, 12 | verified useful | both call paths run in tests and in the CLI smoke | +| `plugins.py` | plugin discovery/API | `PluginManager`, `GreenEyeAPI` | `test_injection.py`; plugin smoke | 04, plugins/README | verified useful | bundled + `GREENEYE_PLUGINS` paths | +| `probes.py` | declarative JSON probes | `load_spec`, `install`, `compile_condition` | `test_smoke_qemu.py`; `demo_probes.json` smoke | 03 §9, 14 | verified useful | 3 probes installed, fired as expected; no standalone runner needed (documented snippet) | +| `cli.py` | argparse entry points (section 1) | `main`, `build_parser` | CLI smokes + integration tests | everywhere | needs improvement (fixed) | docstring now lists `launch`/`inject`/`ghidra`; `run` sets `__file__` | + +## 3. Master table — simulator, harness, plugins, host tooling + +| surface | purpose | API / entry points | tests | used by | status | notes/actions | +|---|---|---|---|---|---|---| +| `sim/decode.py` | RV64IMAC decoder | `decode32`, `decode16` | `test_sim_backend.py` | 16 | verified useful | compressed canonicalisation tested | +| `sim/machine.py` | deterministic machine, UART, lenient MMIO | `Machine`, `Uart16550` | `test_sim_backend.py`, `test_sim_trace.py`, `test_device_map.py` | 16, 24 | verified useful | no wall-clock claims (06) | +| `sim/trace.py` | native exec/access trace (R4) | `Trace` | `test_sim_trace.py` | 16, README | verified useful | trace blob rendered by `analyze` | +| `harness/common.py` | shared harness helpers | `load_target`, `write_json`, `read_faults_window`, … | `test_harness.py`, `test_discovery.py`, `test_version_diff.py` | 18–23 | verified useful | internal, well factored | +| `harness/ci_release.py` | release triage orchestration | `python3 harness/ci_release.py --old … --new …` | `test_version_diff.py` | 21 | verified useful | dry-run supported; manifest/mini-gate | +| `harness/control_discovery.py` | unknown-call discovery ("Sandsifter for RM") | CLI, `--surface demo\|rm` | `test_discovery.py` | 19, 21 | verified useful | deterministic probe generation + effect observer | +| `harness/mutation_harness.py` | bounded deterministic mutation | CLI `--iterations/--seed` | `test_harness.py` (integration, seed replay byte-identical) | 18, 20, 23 | verified useful | replayed identically in test | +| `harness/portability_matrix.py` | multi-target portability runs | CLI `--target` (repeatable) | `test_harness.py` (self-comparison all-zero deltas) | 18, 22 | verified useful | includes probe/scenario/compare sections | +| `harness/reach.py` | reachability answers for a target | CLI `--method auto\|stimulus\|trigger\|run\|call` | `test_discovery.py` | 19 | verified useful | emits suggested probe spec | +| `harness/version_diff.py` | old-vs-new firmware diff | CLI `--old/--new` | `test_version_diff.py` | 21, 25 | verified useful | composes ci_release | +| `harness/value_demos/*` | s01–s10 value validation + `run_all.py` | `python3 harness/value_demos/run_all.py [--only]` | `test_value_demos.py` | 25, MCP tools | verified useful | ran s02/s05 from the CLI; artifacts under `value_demos/artifacts/` | +| `plugins/example_plugin.py` | plugin authoring reference | `register(ge)` | plugin smoke (`--plugin`) | 04, plugins/README | verified useful | loaded via `greeneye run --plugin` | +| `plugins/mcp_server.py` | MCP server (stdio/loopback) | `--stdio`, `--tcp`, `--allow-write`, `--allow-inject` | `test_mcp.py` (431 lines) | 08/R3, 15, 20 | verified useful | read-only default; structured `not_available` fallbacks | +| `plugins/rmharness_plugin.py` | REPL commands for host RM oracle | `register(ge)`; commands `rmtool/rmtop/rmq` | `rmharness/tests/test_rmharness_plugin.py` (3 tests) | 17, 23 | verified useful | write-named args refused (`rm_harness_skip`) | +| `qemu/greeneye_trace.c` | full TCG exec/access JSONL trace | `make -C qemu`; `-plugin greeneye-trace.so,path=…` | none automated; **live-verified in this audit** | 10, 16, 14 | verified useful | 8M exec events in 1 s on the demo; vendored `qemu-plugin.h` builds out of the box | +| `qemu/launch.sh` | one-line `greeneye launch` wrapper | `qemu/launch.sh --kernel <elf>` | smoke | qemu/README | verified useful | thin `exec` of `greeneye.sh launch` | +| `ghidra/*.java` (Export/Mark/Control) | static/dynamic join inside Ghidra | `analyzeHeadless -postScript …` | protocol counterpart covered by `test_bridge.py`; no Ghidra installed here | 05, 13 | verified useful (static) | README documents exact command lines; Java not executed in this environment | +| `rmharness/rmtool.sh` | RM oracle wrapper | passthrough to `tools/rmtool` | `harness.sh` capture smoke (`gpu list`) | 17, 23 | verified useful | builds tool on first use; read-only | +| `rmharness/rmtop.sh` | dashboard wrapper | passthrough to `tools/rmtop` | `--help` | 17 | verified useful | `--once` for captures | +| `rmharness/rmq.sh` | pre-existing rmq wrapper | passthrough to `tools/rmq` | `--help` | 17 | needs improvement (fixed) | `--help` used to fall through and open the RM; wrapper now answers help itself | +| `rmharness/harness.sh` | capture tool run into a green-eye session | `--session DIR -- TOOL ARGS` | capture smoke (verified event + blobs) | 17, 22 | verified useful | additive recording; read-only allowlist enforced | +| `rmharness/tests/` | plugin registration check | `python3 rmharness/tests/test_rmharness_plugin.py` | 3 tests, run and green | 17 | needs improvement (open) | not part of `tests/` discovery nor CI; kept separate by design, documented in 17/README — consider CI step when the workflow is next edited | +| `sdk/injected_demo.c` | position-independent service SDK | `make sdk` | `test_injection.py`; `inject` smoke | 11, 12 | verified useful | direct + firmware call paths | +| `firmware/uproc_demo/` | bundled RV64 uproc (mailbox RPC/state/trap) | `make demo` | many integration tests | README, 00, 02 | verified useful | self-test + scripted stimulus work | +| `examples/scripts/dump_diff.py` | schema decode + snapshot diff demo | `./greeneye.sh run examples/scripts/dump_diff.py` | verified after `__file__` fix | 02 | needs improvement (fixed) | was broken by the `run` namespace gap; now prints decoded ctrl + changed bytes | +| `examples/scripts/trace_rpc.py` | hook every RPC handler | `./greeneye.sh run …` | ran | 02, 16, 18 | verified useful | 17 hook events from the demo | +| `examples/scripts/sim_bench.py` | native throughput benchmark | standalone CLI | ran (0.92 MIPS untraced, pure Python) | 16 | verified useful | self-bootstraps `sys.path`, works from anywhere | +| `examples/scripts/sim_diff.py` | QEMU-vs-native differential | standalone CLI | ran: `PARITY: OK` | 16 | verified useful | exit 1 on divergence, 0 on skip | +| `examples/probes/demo_probes.json` | declarative probe spec sample | loaded by `greeneye.probes.install` | installed + fired via `greeneye run` | 03 §9 | verified useful | 3 probes, hits `[1, 3, 0]` on the demo | +| `tests/` (15 files) | unit + live QEMU integration | `python3 -m unittest discover -s tests -t .` | **155 tests OK** | CI (`greeneye.yml`) | verified useful | includes live QEMU, native parity, MCP, bridge, harness, value demos | + +## 4. Scripting API purpose review + +* **Entry points and lifetime.** `ge.launch()` (QEMU or `backend="native"`) + and `ge.attach()` return a `Session`; `close()` clears breakpoints and kills + owned QEMU unless `keep_alive=True`. Documented in 03 §1 and the + `__init__` docstring. +* **`greeneye run` namespace.** Preloads `ge`, `session`/`s`, `memory`, + `symbols`, `recorder`, plus `__file__`/`__name__` (fixed here so scripts can + locate bundled assets; `examples/scripts/dump_diff.py` depends on it). + `docs/03-SCRIPTING.md` §intro now states this. +* **Static/dynamic join.** `SymbolDB` understands ELF, Ghidra name/index/ + export/callgraph TSVs and native JSON; `resolve()/annotate()` power every + report. `by_subsystem` now has a unit test; `Session.symbol_at` is an + untested-but-documented alias of `symbols.resolve`. +* **Safety.** Probe conditions are whitelisted (`compile_condition`), and the + RSP/MCP surfaces never eval user text except through that whitelist. +* **Errors.** All library errors derive from `GreenEyeError`; `load_json` now + reports file/parse failures as `GreenEyeError` instead of leaking + `JSONDecodeError` through the CLI. +* **Gaps (deliberate).** No session rewind/replay API; TCG JSONL import into + sessions is still roadmap (R4). + +## 5. Plugin API purpose review + +* **Contract.** `register(ge)`; `ge` exposes `session`, `memory`, `symbols`, + `recorder`, `hook`, `watch`, `on_event`, `add_command`, `log` + (`greeneye/plugins.py:36`, `plugins/README.md`). A raising plugin is + recorded as `plugin_error` and cannot break the session. +* **Discovery order.** `--plugin PATH` → `GREENEYE_PLUGINS` → bundled + `plugins/`. Documented in `plugins/README.md` and 04. +* **Bundled plugins.** + * `example_plugin.py` — authoring reference; loaded in this audit. + * `mcp_server.py` — the AI/agent surface; tested by `tests/test_mcp.py`, + documented in 20. The old "MCP is planned/do not add it" notes in + `plugins/README.md`, 04 and 08 are now corrected to point at 20. + * `rmharness_plugin.py` — host oracle REPL commands; tested separately. +* **Gaps.** Plugins cannot yet declare tool capabilities for MCP discovery + beyond `add_command`; `run_command` is REPL-only. Noted, not blocking. + +## 6. Unused tools and decisions + +No `prune candidate` was found: every unreferenced item was a small, coherent +public helper. Three were cheaply wired into unit tests; one remains +`not used yet` with a docstring. Nothing was deleted. + +| item | decision | rationale | +|---|---|---| +| `greeneye.symbols.SymbolDB.by_subsystem` | **wired**: docstring + `test_core.test_by_subsystem` | natural read-only query alongside `names()` | +| `greeneye.elf.write_relocated_elf` | **wired**: `test_core.test_write_relocated_elf` verifies entry/PT_LOAD shift | complete ELF utility next to `write_flat_kernel` | +| `greeneye.ghidra.render_ghidra_command` | **wired**: `test_bridge.GhidraCommandTests` + cited in 05 §4 | renders the `MarkGreenEyeHits.java` command for scripts | +| `greeneye.session.Session.symbol_at` | `not used yet`: docstring only; kept | thin alias of `symbols.resolve`; external scripts may prefer the Session namespace | + +Nothing was deleted. + +## 7. Portability / extensibility / discoverability checks + +* **Portability.** Stdlib-only Python; `greeneye.sh` pins `PYTHONPATH` to the + tool root. Harness tools and `run_all.py` bootstrap their own `sys.path`. + QEMU is optional (integration tests skip cleanly); the native backend makes + the suite largely hardware-free. `qemu-plugin.h` is vendored so the TCG + plugin builds without a QEMU source tree (verified). +* **Extensibility.** Three documented extension axes with stable contracts: + Python scripts (`run`, doc 03), JSON probe specs (`probes`, doc 03 §9), and + plugins (`register(ge)`, docs 04/20). Device behaviour is data, not code + (`greeneye-device-map/1`, doc 24). +* **Discoverability.** `README.md` + `docs/00-OVERVIEW.md` doc maps were + completed for docs 17–27 (previously they stopped at 16); `plugins/` and + `harness/` are now listed in the README layout. `greeneye.sh --help` and + `plugins/mcp_server.py --help` are self-describing. +* **CI.** `.github/workflows/greeneye.yml` builds SDK+demo and runs the main + suite on Linux; `rmharness/tests` is the only suite the workflow misses. + +## 8. How to tell if a tool is misbehaving (symptom → check → fix) + +| tool | symptom | check | fix | +|---|---|---|---| +| `greeneye.sh` / CLI | `greeneye error: …` on a valid path | `./greeneye.sh info` shows resolved paths; `--help` for arguments | correct the path/arg; check `GREENEYE_QEMU`/`GREENEYE_DUMPS`; bad symbol DBs now fail with a clear message | +| `run` scripts | `NameError: __file__` (pre-fix) or names missing | `./greeneye.sh run` preloads `ge/session/s/memory/symbols/recorder/__file__`; run with `--backend native` to remove QEMU | keep scripts inside the tool tree and invoke through `greeneye.sh run`, not bare `python3` | +| hooks/watchpoints | callback never fires | hook an obviously-hit symbol (`rpc_sum`), `record` + `analyze`; check the session event counts | re-run demo to confirm firmware self-test; ensure the target is resumed (`run`) and the symbol exists (`symbols.get`) | +| `probe` | 0 faults reported | check `dumps/<run>/blobs/qemu.log` and `recorder.meta["qemu_fault_count"]` | expected for images that stay in mapped memory; use `--instructions` for a bounded step instead of `--duration` | +| native backend / trace | QEMU-vs-native diff fails | `python3 examples/scripts/sim_diff.py`; `analyze` on the trace blob | divergence means a real backend bug: capture both sessions and diff `exec`/`access` counts (see 16) | +| `inject` | service not found or call returns garbage | `make sdk`; check slot (`--slot 0..7`) and load address; hook `rpc_dispatch` | rebuild the SDK, restore the default slot, verify `dispatch` ABI (12 §) | +| harness mutation/portability | findings not reproducible | `--seed` must be fixed; compare `findings.jsonl` byte-for-byte | if a seed diverges, the target or environment changed: capture a session and diff the baseline | +| discovery / reach | everything "unreachable" | run `--surface demo` on the built demo first; check `--timeout/--settle` | raise settle time; confirm the stimulus hook matches the firmware's mailbox | +| version diff / CI release | huge spurious diff | check symbol biases/TSVs (`--*-symbols`, `--bias`) and labels | fix the bias first; use `--dry-run` to inspect the plan before writing artifacts | +| MCP server | client sees no tools / writes refused | `python3 plugins/mcp_server.py --help`; stdout must carry only protocol frames (logs go to stderr) | remove stdout prints from plugins; pass `--allow-write`/`--allow-inject` only when intended | +| `rmharness` wrappers | `--help` opens the device (pre-fix) or permission errors | `--help` is now device-free; `rmtool gpu list/topology` read `/proc`/sysfs only | run `rmtool.sh --help` to check the build; device-table queries need the driver's permission model (17 §Not implemented) | +| `qemu/greeneye-trace.so` | empty trace file | run `make -C qemu`; QEMU must reach the `start/end` window | widen `start/end`, set `exec=on`, and resume the target (plugin records only executed code) | +| device map | unexpected `unimplemented` read | `greeneye.device_map show <map>`; check `bias` and schema version | harvest from a `probe` session with the same bias; use `unknown_mmio="lenient"` for first-touch exploration | + +## 9. Provenance discipline + +Tracked tool-tree docs describe components and behaviour; no non-public +paths, filenames or source locations appear in them. The audit re-ran the +marker scan across this tree and found none. No further action. + +## 10. Summary + +| status | count | items | +|---|---:|---| +| `verified useful` | 57 | all 13 CLI commands (2 with a fixed gap); all 19 package modules; all `sim/`; all `harness/` incl. value demos; 3 plugins; QEMU plugin + launch helper; Ghidra scripts (static); all `rmharness/` (1 fixed gap); SDK, demo firmware; 5 example assets; tests | +| `not used yet` | 1 | `session.symbol_at` — docstring added, kept (section 6) | +| `needs improvement` | 5 | `run` `__file__` namespace (**fixed**), `symbols resolve --db` traceback (**fixed**), `rmq.sh --help` device touch (**fixed**), stale doc maps/MCP/status text in README/00/04/08/14 + `plugins/README` (**fixed**), `rmharness/tests` outside main suite/CI (**open**, documented) | +| `prune candidate` | 0 | nothing deleted | + +Files changed by this audit: + +* `greeneye/cli.py` — command docstring completed; `run` namespace sets + `__file__`/`__name__`. +* `greeneye/symbols.py` — `load_json` raises `GreenEyeError` and now handles + list payloads; docstrings for `by_subsystem`, `nearest`. +* `greeneye/session.py` — docstring for `symbol_at`. +* `greeneye/ghidra.py` — docstring for `render_ghidra_command`. +* `tests/test_core.py` — tests for list/bad JSON payloads, `by_subsystem`, + `write_relocated_elf`. +* `tests/test_bridge.py` — test for `render_ghidra_command`. +* `rmharness/rmq.sh` — device-free `--help`. +* `README.md`, `docs/00-OVERVIEW.md` — doc maps completed (17–27), layout + rows for `harness/`/`rmharness/`, status-table corrections. +* `docs/03-SCRIPTING.md` — documents `__file__`. +* `docs/04-PLUGINS.md`, `docs/08-ROADMAP.md`, `plugins/README.md` — MCP is + implemented, not planned; point at 20. +* `docs/05-GHIDRA-BRIDGE.md` — documents `render_ghidra_command`. +* `docs/14-ADVERSARIAL-INTEROP-GUIDE.md` — capability table updated to + implemented harness/device-map/MCP status. +* `docs/27-TOOL-AUDIT.md` — this document. + +Verification after the changes: 155/155 unit tests OK; docs-link check OK +(55 files); every smoke in section 0 re-run green. |
