summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 08:36:35 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 08:36:35 -0400
commit31e27d455969d33190d28917ef98f034888d217b (patch)
treec0c4dc18e78e6a1702fcda43bc2806e7cfb7c931 /debug/risc-v-gsp-rm-vm/docs
parentc155ee8de5b642cbd407bf2231041f17ebe43734 (diff)
downloadopen-iov-31e27d455969d33190d28917ef98f034888d217b.tar.gz
open-iov-31e27d455969d33190d28917ef98f034888d217b.zip
debug: add green-eye GSP/uproc RISC-V VM runtime analysis tooling
Adds open-iov/debug/risc-v-gsp-rm-vm: a GDB-free instrumentation stack that boots uproc/GSP firmware in qemu-system-riscv64 and provides Frida-like hooks, breakpoints, watchpoints, memory dumps, structure decoding, snapshot diffs, recorded sessions, declarative probes, a plugin API and a Ghidra symbol/hit bridge. Verified: 25 tests green (including a live QEMU smoke test); the demo uproc boots with correct hook/leave/watch/schema/snapshot/fault evidence; the optional TCG trace plugin builds and recorded 15M trace lines in 3 s; the TU104 570.144 fwimage probe reports its first exception (load access fault at firmware vaddr 0x4009014 reading 0x2000000000000A00). Documentation: lean tool docs under debug/risc-v-gsp-rm-vm/docs/00-10, a new docs/05-operations/03-risc-v-gsp-rm-vm.md debugging document, and '# TOOL IN DEVELOPMENT - NOT READY YET!' markers in docs/README.md, the onboarding tooling table and the debugging workflows doc. Vendors qemu-plugin.h from QEMU v8.2.2 (GPL-2.0-or-later), with its license header intact, so the optional TCG plugin builds without a QEMU source tree.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md98
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/01-DESIGN.md179
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/02-USAGE.md168
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md219
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md123
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md105
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md136
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/07-PORTABILITY-PLAYBOOKS.md126
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md116
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/09-GSP-FIRMWARE-NOTES.md104
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/10-RELATED-WORK.md76
11 files changed, 1450 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
new file mode 100644
index 0000000..a9f4251
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
@@ -0,0 +1,98 @@
+# 00 — green-eye overview
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!**
+> Experimental tooling for simulating and inspecting the GSP firmware's
+> runtime memory, for `gsp-rm-vfio` accuracy measurements and GSP/uproc
+> reverse engineering. Interfaces and file formats may change without notice.
+> It is development-only: never shipped, never a dependency of the emulation,
+> never present in a guest.
+
+## What problem it solves
+
+Open-IOV emulates the GSP interface and (in the physical backend) boots the
+real GSP firmware. Both efforts need the same missing capability: to *watch
+the firmware do things* — which handler runs, what it reads and writes, what
+it returns, how its state machine evolves — without a physical GPU, without
+WPR2/hazard exposure, and repeatably.
+
+green-eye runs uproc/GSP firmware in `qemu-system-riscv64` and gives the
+developer Frida-like instrumentation over its memory and execution:
+
+* breakpoints and enter/leave **hooks** (RV64 ABI aware),
+* **watchpoints** on firmware variables,
+* **memory** read/write/hexdump/search/scan,
+* **structure decoding** from schemas,
+* **snapshot + diff** of memory regions,
+* **sessions**: a recorded, replayable dump of runtime memory activity,
+* **post-run analysis** with symbol annotation,
+* **plugins** for new tools, harnesses and virtualization updates,
+* a **Ghidra bridge** joining static addresses to runtime hits,
+* A/B tooling for **bare-metal vs vfio-mdev** result comparison.
+
+It is deliberately not a GDB wrapper: a pure-Python RSP client makes the
+whole stack scriptable, and QEMU is just the first execution backend.
+
+## Status (v0.1, 2026-10-02)
+
+| Capability | State |
+|---|---|
+| RV64 QEMU boot + gdbstub transport (no GDB) | working, live-verified |
+| Hooks / breakpoints / watchpoints / step / continue | working, live-verified |
+| Memory tools + struct decode + snapshot diff | working, live-verified |
+| 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) |
+| 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) |
+
+## Quick start
+
+```bash
+cd open-iov/debug/risc-v-gsp-rm-vm
+make demo # build the bundled RV64 uproc demo
+./greeneye.sh demo # canned instrumentation pass, records a session
+./greeneye.sh info # environment check
+./greeneye.sh probe # run the real GSP fwimage and report first divergence
+```
+
+Interactively:
+
+```bash
+./greeneye.sh demo --no-build --duration 0 # launches; prints the gdbstub port
+./greeneye.sh attach --port <port> --kernel firmware/uproc_demo/build/uproc_demo.elf
+```
+
+## Document map
+
+| Doc | Read it for |
+|---|---|
+| [01-DESIGN](01-DESIGN.md) | how the pieces fit, transport, data model, limits |
+| [02-USAGE](02-USAGE.md) | CLI and REPL recipes |
+| [03-SCRIPTING](03-SCRIPTING.md) | writing programs against the API |
+| [04-PLUGINS](04-PLUGINS.md) | extending green-eye |
+| [05-GHIDRA-BRIDGE](05-GHIDRA-BRIDGE.md) | static/dynamic address join |
+| [06-ACCURACY](06-ACCURACY.md) | what the simulation does and does not prove |
+| [07-PORTABILITY-PLAYBOOKS](07-PORTABILITY-PLAYBOOKS.md) | new GSP / device / architecture |
+| [08-ROADMAP](08-ROADMAP.md) | MCP, custom simulator, device model |
+| [09-GSP-FIRMWARE-NOTES](09-GSP-FIRMWARE-NOTES.md) | uproc/falcon/NV-RISCV facts used here |
+| [10-RELATED-WORK](10-RELATED-WORK.md) | Frida/GDB/QEMU-plugin comparison |
+
+## Where this fits in the Open-IOV workflow
+
+green-eye is a **development and research** tool, alongside the Ghidra corpus
+and the guest diagnostics. It does not replace either:
+
+* **Ghidra** answers *what is the code*; green-eye answers *what did it do*.
+* **Guest diagnostics** (`tools/guest-diag`) answer *what did the guest
+ driver do*; green-eye answers *what did the firmware server do*.
+* **The physical backend** (PLAN B) answers *what does real silicon do*;
+ green-eye provides a safe place to repeat, mutate and single-step the same
+ firmware when the hardware path is hazardous or unavailable.
+
+The intended accuracy loop is: run the same workload against the real
+GSP-RM (hardware or mdev), record its RPC/state behaviour, then run the
+firmware in green-eye, record the same events, and compare with `compare`
+(see 06 and 07).
diff --git a/debug/risc-v-gsp-rm-vm/docs/01-DESIGN.md b/debug/risc-v-gsp-rm-vm/docs/01-DESIGN.md
new file mode 100644
index 0000000..c5de2da
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/01-DESIGN.md
@@ -0,0 +1,179 @@
+# 01 — green-eye design
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+## 1. Why this shape
+
+Two facts drive the design:
+
+1. **Frida does not run here.** Frida upstream ships no RISC-V target
+ (verified against the 17.20 release assets: x86/x86_64, arm/arm64, mips,
+ macOS/Windows/QNX/Prospero — no riscv64), and its agent model requires a
+ hosted process. The GSP image is bare-metal RV64IMAC with its own libos.
+ So the instrumentation layer is built from QEMU's gdbstub and QEMU TCG
+ plugins instead (see 10 for the full comparison).
+2. **The project needs scripting, not a debugger.** The recurring workflow is
+ "run firmware, capture a symbol-annotated map of behaviour, diff it against
+ another run". A pure-Python RSP client makes that a library call; GDB
+ would make it a subprocess and a parsing problem.
+
+Design goals, in order: direct time-to-value, scriptability, extensibility,
+robustness, and honest fidelity reporting.
+
+## 2. Component map
+
+```
+ +-----------------------------+
+ | qemu-system-riscv64 (virt) |
+ | -bios none -kernel <elf> |
+ | gdbstub <-- TCP |
+ +--------------+--------------+
+ | RSP
+ greeneye/rsp.py pure-Python RSP client
+ | |
+ | registers, memory, Z0-Z4, continue/step/interrupt, qRcmd, qXfer
+ v
+ greeneye/target.py QEMU process lifecycle, loader segments, UART/qemu logs
+ greeneye/elf.py ELF64/32 parse: segments, symbols, entry
+ greeneye/symbols.py symbol DB (ELF + Ghidra TSV + JSON) + address translation
+ greeneye/session.py hooks, breakpoints, watchpoints, stop dispatch, run loop
+ greeneye/memory.py read/write/hexdump/search/snapshot/diff
+ greeneye/structs.py declarative structure schemas + render
+ greeneye/probes.py declarative probe specs + conditions
+ greeneye/record.py session dir: events.jsonl + snapshots + session.json
+ greeneye/analyze.py summary/compare reports (symbol annotated)
+ greeneye/plugins.py plugin discovery + API
+ greeneye/ghidra.py Ghidra export/import bridge
+ greeneye/cli.py info/demo/run/attach/probe/record/analyze/compare/symbols
+```
+
+## 3. Transport: the RSP client
+
+`rsp.py` implements the gdbstub subset QEMU needs:
+
+* packet framing `$<data>#<checksum>`, ack/no-ack, retransmit on bad checksum;
+* `QStartNoAckMode` (QEMU acknowledges with an **empty packet**, then
+ switches once the client acks — a bring-up bug worth remembering);
+* `qSupported`, including the stub's `PacketSize` (memory payloads are chunked
+ so hex expansion never exceeds it);
+* `qXfer:features:read:target.xml`, recursively inlining `xi:include` register
+ files; RV64 register layout with a 33×8-byte fallback;
+* `g`/`p`/`P`, `m`/`M`/`X`, `Z0-Z4`, `c`/`s`, interrupt `0x03`, `qRcmd`;
+* stop-reply parsing (`T`/`S`/`W`/`X`, `watch:`/`rwatch:`/`awatch:`,
+ `thread:`).
+
+The client is deliberately blocking and simple; async/concurrency belongs in
+the plugin layer, not the transport.
+
+## 4. Session semantics (the Frida-like core)
+
+* **Breakpoint kinds**: `sw` (default; stepped over transparently) and `hw`.
+* **Hooks**: `hook(target, on_enter, on_leave)`. `on_leave` is implemented by
+ reading `ra` on entry and arming a *return breakpoint*; nested calls at the
+ same return address are queued (LIFO). Tail calls and asynchronous
+ interrupts are not tracked — documented limitation.
+* **Watchpoints**: `Z2/Z3/Z4` (write/read/access). QEMU stops *at* the
+ accessing instruction, so green-eye temporarily clears the watch, steps one
+ instruction, and re-arms it. Without this the target livelocks on the same
+ store.
+* **Stop dispatch**: every stop reads the register file, resolves the PC to a
+ symbol, records an event, invokes matching hooks/watchpoints, and resumes.
+* **Run loop**: `run(duration=…, max_stops=…)` services events until the
+ wall-clock budget, stop budget, or target exit; the deadline path interrupts
+ **once**, dispatches that stop, and returns (never interrupt twice).
+* **Faults**: `ebreak`/traps surface as plain stops; `step()` walks past them.
+ The demo uses this to demonstrate trap capture.
+
+## 5. Address spaces and the symbol layer
+
+Three address spaces must never be confused (the CUDA docs learned this the
+hard way):
+
+| Space | Meaning | Example |
+|---|---|---|
+| file vaddr | offset in the binary file (`libcuda+0x…`) | `0x353ef0` |
+| program addr | what Ghidra shows (file vaddr + image base) | `0x453ef0` (base `0x100000`) |
+| runtime addr | link address + relocation bias | `0x80000048` |
+
+`SymbolDB` stores runtime addresses. Loaders take a `bias`:
+
+```python
+db.load_elf(image, bias=relocation) # demo firmware: bias 0
+db.load_names_tsv("names.tsv", bias=0x100000) # libcuda: file vaddr -> runtime
+db.load_index_tsv("decompiled_index.tsv") # program+file addr columns
+db.load_callgraph("callgraph.tsv") # caller/callee extras
+```
+
+`resolve()` returns the containing symbol (or nearest preceding within a
+delta), and `annotate()` renders `addr name+0xoff`.
+
+## 6. Recording format (`greeneye-session/1`)
+
+A session directory is the durable artifact:
+
+```
+dumps/<stamp>-<name>/
+ session.json # format, name, created, counts, snapshot inventory, meta
+ events.jsonl # one JSON object per event, monotonic t + fields
+ snapshots/*.bin.z # zlib-compressed region captures (sha256 recorded)
+ blobs/* # plugin/probe dumps
+ summary.md # rendered analysis (analyze/render_summary)
+```
+
+Event types: `session_start`, `image_loaded`, `hook`, `watch`, `bp_hit`,
+`leave`, `watch_hit`, `stop`, `step`, `mem_read`, `mem_write`, `snapshot`,
+`snapshot_saved`, `probe_log`, `probe_dump`, `plugin*`, `note`, `exit`,
+`session_end`. The format is append-only JSON lines so partial sessions stay
+readable, and `compare` can profile two sessions symbolically.
+
+This is the "complete map and recorded dump of runtime memory activity": the
+event stream is the map, the snapshot/blob files are the dump.
+
+## 7. Structures
+
+Schemas are JSON data (`structs.schema_from_json`) with integer, float,
+`bytes`, `cstr` and `ptr` fields plus optional `enum` names and notes. The
+demo control block schema lives at
+`firmware/uproc_demo/gsp_ctrl.schema.json`. Schemas are intentionally
+declarative so they can come from a Ghidra type export or a plugin later.
+
+## 8. Execution backends
+
+* `QemuTarget` (`virt`, RAM, NS16550A UART) is the default and the only
+ supported backend today.
+* The **kernel fast path** passes `-kernel <elf>`; QEMU loads segments and
+ sets PC.
+* The **probe path** passes each PT_LOAD segment as a raw loader device and
+ green-eye sets PC through the gdbstub — this lets a firmware linked for GPU
+ physical addresses be relocated into RAM for observation.
+* A **custom simulator** can be added as a session backend later (roadmap,
+ not v0.1) because `Session` only assumes the RSP-like operations.
+
+## 9. Ghidra bridge
+
+* Out: `greeneye symbols export --names … --index … --exports … --callgraph …`
+ builds `greeneye-symbols.json` from the project's existing TSV exports.
+* In: `greeneye.ghidra.export_hits(session_dir, out_tsv)` writes
+ `addr<TAB>hits<TAB>symbol`, and `MarkGreenEyeHits.java` annotates those
+ functions in a Ghidra project (plate comments + bookmarks).
+* Mapping a hit to source: symbols carrying `bucket`/`line` extras resolve to
+ the decompiled corpus via `greeneye.ghidra.corpus_location()`.
+
+## 10. Plugins
+
+`plugins.py` discovers modules and calls `register(ge)` with a `GreenEyeAPI`
+(`session`, `memory`, `symbols`, `recorder`, `hook`, `watch`, `on_event`,
+`add_command`, `log`). Errors are recorded as `plugin_error` and never break a
+session. MCP is a planned plugin, not an implementation (08).
+
+## 11. Known limits (be honest)
+
+1. QEMU `virt` models **no TU104**: GPU MMIO, falcon windows, WPR2, PMU,
+ timers and FB DMA do not exist. The real GSP image will fault early; the
+ `probe` command reports exactly where. Not a boot environment (06).
+2. The transport is blocking; no replay of async interrupts mid-run.
+3. Return hooks miss tail calls and interrupt-driven returns.
+4. `qRcmd` monitor commands are host-tooling only and not portable.
+5. Recorded events describe *observed* stops; instructions executed between
+ stops are invisible unless the TCG plugin is enabled or a step trace is
+ taken.
diff --git a/debug/risc-v-gsp-rm-vm/docs/02-USAGE.md b/debug/risc-v-gsp-rm-vm/docs/02-USAGE.md
new file mode 100644
index 0000000..53cf19f
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/02-USAGE.md
@@ -0,0 +1,168 @@
+# 02 — green-eye usage
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+All commands are run from `open-iov/debug/risc-v-gsp-rm-vm/`.
+
+## 0. Requirements
+
+* `qemu-system-riscv64` (Ubuntu: `apt install qemu-system-misc`);
+* Python 3.10+ (stdlib only);
+* for the demo firmware: `clang` and `lld` (`apt install lld-18` provides
+ `/usr/lib/llvm-18/bin/ld.lld`).
+
+Check everything:
+
+```bash
+./greeneye.sh info
+make test # builds the demo then runs unit + QEMU smoke tests
+```
+
+## 1. The canned demo (fastest way to see it work)
+
+```bash
+make demo
+./greeneye.sh demo
+```
+
+It boots `firmware/uproc_demo/build/uproc_demo.elf`, installs hooks on all
+`rpc_*` handlers and a watchpoint on `rpc_count`, runs the firmware self-test,
+injects a `PING` from the host, decodes and diffs the control block, then
+injects `FAULT` to capture the `ebreak` trap. A session is recorded under
+`dumps/` and a `summary.md` is written.
+
+Options: `--no-build`, `--duration SECONDS`, `--kernel PATH`, `--record PATH`.
+
+## 2. Interactive REPL
+
+```bash
+./greeneye.sh attach --kernel firmware/uproc_demo/build/uproc_demo.elf
+```
+
+`attach` launches the target itself when `--port` is omitted. To share a
+target between terminals, launch it separately and attach by port:
+
+```bash
+./greeneye.sh launch --kernel <elf> # prints: gdbstub: 127.0.0.1:<port>
+./greeneye.sh attach --port <port> --kernel <elf>
+```
+
+REPL commands:
+
+| Command | Meaning |
+|---|---|
+| `regs` / `reg <name> [value]` | read/write registers |
+| `x <addr> [len]` | annotated hexdump |
+| `mem <addr> <len>` | raw bytes (hex) |
+| `w <addr> <hex>` / `wstr <addr> <text>` | write memory |
+| `bp <addr\|symbol> [sw\|hw]` | breakpoint that prints on hit |
+| `hook <addr\|symbol>` | enter/leave hook with printing |
+| `watch <addr\|symbol> [size] [w\|r\|a]` | watchpoint |
+| `s [n]` / `c [seconds]` | step n; continue for N seconds |
+| `snap <label> <addr> <len>` | snapshot a region into the session |
+| `sym <addr\|name>` | symbol resolution |
+| `events [n]` | tail the recorded event stream |
+| `note <text>` | annotate the session |
+| `quit` | close and exit |
+
+Plugin-provided commands appear automatically (e.g. `gehits` from the example
+plugin when loaded with `--plugin plugins/example_plugin.py`).
+
+## 3. Scripted sessions
+
+```bash
+./greeneye.sh run examples/scripts/trace_rpc.py
+./greeneye.sh run examples/scripts/dump_diff.py
+./greeneye.sh run my_script.py --kernel <elf> --plugin plugins/example_plugin.py
+```
+
+The script runs with `ge`, `s`/`session`, `memory`, `symbols`, `recorder` in
+scope and is recorded by default. See [03-SCRIPTING](03-SCRIPTING.md).
+
+## 4. Recording, analysis, comparison
+
+```bash
+./greeneye.sh record --duration 2.0 --watch gsp_ctrl+0x10:8 --snapshot gsp_ctrl:392
+./greeneye.sh analyze dumps/20261002-082721-demo
+./greeneye.sh analyze dumps/<a> --symbols symbols.json > a.md
+./greeneye.sh compare dumps/<run-a> dumps/<run-b> --symbols
+```
+
+`analyze` prints a markdown report: event counts, breakpoint hits by symbol,
+watchpoint hits, written pages, stepped coverage, snapshot inventory and a
+timeline. `compare` diffs two sessions symbolically (hit-count deltas, watch
+deltas, written-page deltas) — the A/B primitive for bare-metal vs
+vfio-mdev and old-firmware vs new-firmware runs.
+
+## 5. Probing an arbitrary firmware image
+
+```bash
+./greeneye.sh probe # the project's TU104 570.144 fwimage
+./greeneye.sh probe path/to/fw.elf --duration 0.5
+./greeneye.sh probe --instructions 2000 --no-relocate
+```
+
+Segments are flattened into a raw kernel loaded at `0x80000000`; because QEMU
+starts raw kernels at the load base, a tiny PC-relative trampoline at offset 0
+jumps to the image's real (relocated) entry. The run is bounded and the report
+shows the **first exception** (faulting instruction, access address/size,
+cause) plus the QEMU log tail. For the real GSP image expect an early
+load-access fault on an unmodelled GPU register — that is the honest result
+(see [06-ACCURACY](06-ACCURACY.md)).
+
+## 6. Symbol databases
+
+```bash
+./greeneye.sh symbols export \
+ --names ../../userspace/cuda/570.144/annotations/names.tsv \
+ --index <run>/analysis/decompiled/decompiled_index.tsv \
+ --callgraph <run>/analysis/elf-index/callgraph.tsv \
+ --bias 0x100000 -o symbols-570.json
+./greeneye.sh symbols resolve --db symbols-570.json 0x453ef0
+```
+
+`--bias` normalises file vaddrs to runtime addresses (`0x100000` is the
+libcuda Ghidra image base; use 0 for firmware linked at its load address).
+
+## 7. Plugins
+
+```bash
+./greeneye.sh plugins
+./greeneye.sh run examples/scripts/trace_rpc.py --plugin plugins/example_plugin.py
+GREENEYE_PLUGINS=/path/to/plugins ./greeneye.sh demo
+```
+
+See [04-PLUGINS](04-PLUGINS.md).
+
+## 8. Make targets
+
+| Target | Action |
+|---|---|
+| `make demo` | build the demo firmware |
+| `make test` | build + unit tests + live QEMU smoke |
+| `make demo-run` | `greeneye demo --no-build` |
+| `make probe-real` | run the TU104 fwimage probe |
+| `make plugin` | build the optional TCG plugin |
+| `make clean` | remove builds, dumps and `__pycache__` |
+
+## 9. Troubleshooting
+
+* **`qemu-system-riscv64 not found`** — install `qemu-system-misc` or set
+ `GREENEYE_QEMU`.
+* **`gdbstub port not ready`** — QEMU exited early; the error includes its
+ stderr. Check the ELF actually loads (`./greeneye.sh info`).
+* **A run seems stuck** — sessions never `pkill -f greeneye` (it matches the
+ invoking shell). Use `pkill -9 -f '[q]emu-system-riscv64'` to clean up
+ leftover emulators.
+* **Target stops immediately at `0x1000`** — the firmware was not passed to
+ QEMU; check the `-kernel` path (known historical bug, fixed).
+* **`Permission denied` on `/dev/kvm`** — not needed here; QEMU `virt` may
+ fall back to TCG. For speed, ensure the user can use KVM.
+
+## 10. Safety
+
+green-eye touches no GPU, no mdev, no kernel module and no host driver. It
+runs unprivileged QEMU instances on loopback gdbstub ports. The only host
+side effects are temporary directories under `/tmp` and `dumps/` under the
+tool tree. It is safe to run alongside the CUDA/PLAN-B work, but keep an eye
+on RAM/CPU if large snapshots are recorded.
diff --git a/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
new file mode 100644
index 0000000..c677c8d
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
@@ -0,0 +1,219 @@
+# 03 — green-eye scripting
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+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.
+
+## 1. Attach or launch
+
+```python
+import greeneye as ge
+
+# Launch the bundled demo (QEMU -kernel fast path), recording by default.
+s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", record=True, name="my-run")
+
+# Or attach to an already-running gdbstub (e.g. started by `greeneye launch`).
+# s = ge.attach(port=4444, record="dumps/attach-run")
+
+try:
+ ...
+finally:
+ s.close() # clears breakpoints/watchpoints, kills owned QEMU
+```
+
+`launch(..., keep_alive=True)` leaves QEMU running after `close()`.
+
+## 2. Symbols
+
+```python
+sym = s.symbols.get("rpc_sum") # Symbol | None
+addr = s.symbols.lookup("rpc_sum") # int, raises if missing
+s.symbols.annotate(0x80000048) # "0x80000048 rpc_ping"
+s.symbols.resolve(0x80000050) # Symbol (containing or nearest)
+s.resolve("rpc_sum") # int | int passthrough | "0x..." prefix
+```
+
+Symbols come from ELF (demo), Ghidra `names.tsv`/`decompiled_index.tsv`
+(`symbols export`), or native JSON.
+
+## 3. Hooks and breakpoints
+
+```python
+events = []
+
+def on_sum_enter(hit):
+ events.append(("enter", hit.arg(0), hit.arg(1))) # a0, a1
+
+def on_sum_leave(hit):
+ events.append(("leave", hit.retval)) # a0 at return
+
+handle = s.hook("rpc_sum", on_enter=on_sum_enter, on_leave=on_sum_leave)
+handle.hits, handle.label, handle.addr
+handle.detach()
+
+bp = s.breakpoint(0x800000fe, callback=lambda hit: print(hit), temporary=True, kind="sw")
+s.remove_breakpoint(bp)
+```
+
+`Hit` carries `kind`, `addr`, `regs` (full register dict), `symbol`,
+`retval` (leave hits), `watch`, `stop`, `session`, plus helpers
+`hit.arg(n)` and `hit.label()`.
+
+A hook's `on_leave` is realised as a return breakpoint; the callback's
+`hit.addr`/`hit.symbol` are the hooked function (not the return site), and
+`hit.retval` is `a0`. Tail calls and interrupt returns are not tracked.
+
+## 4. Watchpoints
+
+```python
+def on_write(hit):
+ print(f"{hit.label()} written; pc={hit.pc:#x}")
+
+wp = s.watch("gsp_ctrl+0x10", size=8, kind="w", callback=on_write) # w|r|a
+wp.hits
+s.unwatch(wp)
+```
+
+The session transparently steps over the triggering store (required with
+QEMU; see 01 §4).
+
+## 5. Memory
+
+```python
+s.memory.read(0x80000840, 64)
+s.memory.write_u64(0x80000860, 1) # mailbox = PING
+s.memory.hexdump(0x80000840, 128) # annotated
+s.memory.search_string(0x80000000, 0x10000, "GREEN")
+s.memory.search_u32(0x80000000, 0x10000, 0x47535031)
+s.memory.read_cstring(0x800007d0)
+s.memory.snapshot(0x80000840, 392, name="ctrl")
+```
+
+`Memory.read`/`write` record `mem_read`/`mem_write` events; snapshots are
+saved and hashed by the recorder.
+
+## 6. Structures
+
+```python
+import json
+from greeneye.structs import schema_from_json, render, decode
+
+schema = schema_from_json(json.load(open("firmware/uproc_demo/gsp_ctrl.schema.json")))
+values = decode(schema, s.memory.read(ctrl, schema.size), base=ctrl)
+print(render(schema, values))
+```
+
+A schema is JSON: `{"name":…, "fields":[{"name","offset","fmt","size?","enum?","note?"}]}`
+with `fmt` in `u8..u64`, `s8..s64`, `f32/f64`, `bool`, `bytes`, `cstr`,
+`ptr`.
+
+## 7. Snapshots and diffs
+
+```python
+from greeneye import Memory
+
+before = s.snapshot(ctrl, schema.size, "before")
+s.memory.write_u64(ctrl + 32, 1)
+s.run(duration=0.4)
+after = s.snapshot(ctrl, schema.size, "after")
+print(Memory.render_diff(Memory.diff(before, after, base=ctrl), s.symbols))
+```
+
+## 8. Running
+
+```python
+s.run(duration=1.0) # service events for one second
+s.run(max_stops=20) # or until 20 stops
+hits = s.step(10) # single-step and dispatch hooks
+s.stop() # interrupt and dispatch
+```
+
+`run` returns the number of stops serviced. Every stop is recorded.
+
+## 9. Declarative probes
+
+For simple instrumentation, ship a JSON spec instead of Python:
+
+```json
+{
+ "probes": [
+ {"name": "sum", "symbol": "rpc_sum", "when": "a1 > 0",
+ "on_enter": {"log": ["a0", "a1"],
+ "dump": [{"reg": "a0", "len": 32, "label": "sum-input"}],
+ "note": "sum called"},
+ "on_leave": {"log": ["retval"]}}
+ ]
+}
+```
+
+```python
+from greeneye.probes import load_spec, install
+install(s, load_spec("examples/probes/demo_probes.json"))
+s.run(duration=1.0)
+```
+
+Conditions accept register names (`a0..a7`, `pc`, `ra`, `sp`, `x0..x31`,
+`retval`), integer/hex literals and `and/or/not` with comparisons; arbitrary
+code is rejected. Actions: `log`, `dump` (save a blob), `snapshot`, `note`.
+
+## 10. Event stream and plugins
+
+```python
+s.on_event("watch_hit", lambda hit: print("wrote", hit.label()))
+s.note("starting phase 2")
+
+# In a plugin module:
+def register(ge):
+ ge.add_command("mycmd", lambda session, args: 0)
+```
+
+See [04-PLUGINS](04-PLUGINS.md).
+
+## 11. Recording and post-analysis from Python
+
+```python
+s.recorder.dir # session directory (or None)
+s.recorder.event("phase", name="p2")
+s.recorder.close() # writes session.json + summary.md
+
+from greeneye.analyze import render_summary, compare_sessions
+print(render_summary("dumps/run-a", symbols=s.symbols))
+print(compare_sessions("dumps/run-a", "dumps/run-b", symbols=s.symbols))
+```
+
+## 12. A complete worked script
+
+```python
+"""Drive the demo, capture a PING and a FAULT, and summarise."""
+import greeneye as ge
+
+s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", record=True, name="worked")
+ctrl = s.symbols.lookup("gsp_ctrl")
+hits = []
+try:
+ for name in ("rpc_ping", "rpc_sum", "rpc_fault"):
+ if s.symbols.get(name):
+ s.hook(name, on_enter=lambda h: hits.append(h.label()))
+ s.watch(ctrl + 0x10, size=8, kind="w")
+ s.run(duration=0.5) # self-test
+ s.memory.write_u64(ctrl + 0x20, 1) # PING
+ s.memory.write_u64(ctrl + 0x28, 0x1234)
+ s.run(duration=0.5)
+ s.memory.write_u64(ctrl + 0x20, 6) # FAULT -> ebreak
+ s.run(duration=1.0, max_stops=1)
+ print("pc:", hex(s.pc), s.symbols.annotate(s.pc))
+ s.step() # past the trap
+ print("hits:", hits)
+finally:
+ print("recording:", getattr(s.recorder, "dir", None))
+ s.close()
+```
+
+## 13. Errors and timeouts
+
+All green-eye errors derive from `greeneye.GreenEyeError`:
+`RspTimeout` (transport), `RspProtocolError`, `TargetError`,
+`SymbolNotFound`, `StructError`. `RspClient.timeout` (default 10 s) bounds a
+single packet read; `run`'s budget bounds the whole pass.
diff --git a/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md b/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md
new file mode 100644
index 0000000..fa0fe0c
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/04-PLUGINS.md
@@ -0,0 +1,123 @@
+# 04 — writing green-eye plugins
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+A plugin is a Python module exposing `register(ge)`. It is loaded before the
+session runs (CLI `--plugin PATH`, `GREENEYE_PLUGINS`, or the bundled
+`plugins/` directory) and can add instrumentation, CLI commands and harness
+logic without touching green-eye itself.
+
+## 1. The API object
+
+```python
+def register(ge):
+ # live objects
+ ge.session # Session
+ ge.memory # Memory
+ ge.symbols # SymbolDB
+ ge.recorder # Recorder | NullRecorder
+
+ # instrumentation
+ ge.hook(target, on_enter=None, on_leave=None, kind="sw")
+ ge.watch(target, size=8, kind="w", callback=None)
+ ge.on_event(kind, callback) # kind in {"*", "bp_hit", "watch_hit", ...}
+ ge.resolve(target) # symbol/addr -> runtime address
+
+ # CLI + logging
+ ge.add_command("name", fn, help="…") # fn(session, args: list[str]) -> int
+ ge.log("message", any="field") # recorded as a "plugin" event
+ ge.commands # dict of registered commands
+```
+
+Everything a plugin records lands in the session's `events.jsonl`, so plugin
+findings are replayable and comparable like any other event.
+
+## 2. Lifecycle and safety
+
+* `register()` runs once, immediately after the session is attached and
+ symbols are loaded; the target is still stopped, so hooks are in place
+ before the first instruction.
+* A plugin that raises is recorded as `plugin_error` and skipped; it never
+ aborts the session or the other plugins.
+* Session close detaches all hooks/watchpoints, including plugin-installed
+ ones.
+* Plugins must be stdlib-only unless the dependency is documented; they are
+ development tooling and never shipped.
+
+## 3. The four plugin classes
+
+**(a) Runtime RE tools** — add analysis that green-eye does not have:
+
+```python
+def register(ge):
+ def on_state(hit):
+ ge.recorder.event("state_machine", result=hit.retval, pc=hit.pc)
+ ge.hook("rpc_state", on_leave=on_state)
+```
+
+Useful additions: allocation graphs, ring-buffer watermarks, page-table walks,
+coverage collectors built from `on_event("stop")` plus the TCG trace, entropy
+or checksum probes over structures.
+
+**(b) Harnesses** — drive the firmware and assert:
+
+```python
+def register(ge):
+ def check(session, args):
+ ctrl = session.symbols.lookup("gsp_ctrl")
+ before = session.memory.read_u64(ctrl + 0x10)
+ session.memory.write_u64(ctrl + 0x20, 1) # PING
+ session.run(duration=0.3)
+ after = session.memory.read_u64(ctrl + 0x10)
+ assert after == before + 1, "PING did not dispatch"
+ print("PING ok")
+ return 0
+ ge.add_command("check-ping", check)
+```
+
+**(c) Virtualization-environment updates** — alter how the target is built or
+driven: extra QEMU args, a rehosting shim for uproc sources, a device-model
+stub, a replay layer that feeds recorded events back into a run. Keep such
+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.
+
+## 4. Example plugin
+
+`plugins/example_plugin.py` demonstrates (a), (b) and logging:
+
+```python
+def register(ge):
+ ge.log("example plugin loaded")
+ def on_state(hit):
+ ge.recorder.event("plugin_state_transition", result=hit.retval)
+ if ge.symbols.get("rpc_state"):
+ ge.hook("rpc_state", on_leave=on_state)
+ ge.add_command("gehits", lambda session, args: print("…and 0"), help="…")
+```
+
+Run it with:
+
+```bash
+./greeneye.sh run examples/scripts/trace_rpc.py --plugin plugins/example_plugin.py
+```
+
+## 5. Testing a plugin
+
+1. Load it against the demo firmware (`./greeneye.sh demo --plugin …`) — the
+ session must remain green and the plugin's events must appear in
+ `events.jsonl`.
+2. Unit-test pure logic outside green-eye; keep hooks small.
+3. Never let a plugin assume a particular firmware: gate on
+ `ge.symbols.get(name)` and log a skip instead of failing.
+
+## 6. Distribution
+
+Plugins can live anywhere. For in-tree plugins that document the workflow,
+put them under `plugins/` in this tool directory; for local or
+target-specific tooling, use `GREENEYE_PLUGINS` and keep paths out of tracked
+docs (project rule).
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
new file mode 100644
index 0000000..857102e
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/05-GHIDRA-BRIDGE.md
@@ -0,0 +1,105 @@
+# 05 — Ghidra bridge
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+Ghidra answers *what is the code*; green-eye answers *what did it do*. The
+bridge joins them in both directions: Ghidra symbols into the runtime session,
+runtime hit addresses back into the Ghidra project.
+
+## 1. Address conventions (read this first)
+
+| Space | Meaning | Transformation |
+|---|---|---|
+| file vaddr | offset in the binary (`libcuda+0x…`) | base |
+| Ghidra program addr | what the decompiler shows | file vaddr + image base (`0x100000` for libcuda) |
+| runtime addr | live process / emulator address | file vaddr + load bias |
+
+* Firmware linked at its load address: bias 0 (the demo, and a
+ relocated-to-RAM GSP image when using the probe bias).
+* CUDA `libcuda` in the host oracle: runtime base from `/proc/<pid>/maps`;
+ `--bias` normalises it if you want runtime addresses in the DB.
+* The project's `decompiled_index.tsv`/bucket TSVs carry **both** program and
+ file addresses; green-eye parses both and stores the program address in
+ `symbol.extras["program_addr"]`.
+
+## 2. Build a symbol DB from Ghidra exports
+
+```bash
+./greeneye.sh symbols export \
+ --names ../../userspace/cuda/570.144/annotations/names.tsv \
+ --index ../../userspace/cuda/570.144/run/analysis/decompiled/decompiled_index.tsv \
+ --exports ../../userspace/cuda/570.144/run/analysis/elf-index/exports.tsv \
+ --callgraph ../../userspace/cuda/570.144/run/analysis/elf-index/callgraph.tsv \
+ --bias 0 -o symbols-cuda.json
+```
+
+For the GSP firmware project (`tools/ghidra/gsp-fwimage-570.144/`), first run
+the existing export scripts (`ExportFunctionIndex.java`,
+`DecompileFunctions.java`, …) to produce TSVs, then:
+
+```bash
+./greeneye.sh symbols export --index <export.tsv> --bias 0 -o symbols-gsp.json
+```
+
+`load_index_tsv` is column-aware: it accepts
+`<program>\t<file>\t<size>\t<name>\t<bucket>\t<line>\t<status>`, the 4-column
+exports form, or a plain `<addr>\t<name>` two-column form.
+
+## 3. Runtime addresses in the session
+
+```python
+db = SymbolDB()
+db.load_json("symbols-cuda.json")
+db.load_names_tsv("names.tsv", bias=runtime_base - file_base)
+s.symbols = db # or pass symbols= to attach()/launch()
+```
+
+`resolve()` then annotates every hit with the Ghidra name, and
+`greeneye.ghidra.corpus_location(symbol, corpus_root)` returns
+`<bucket>:<line>` when the index carried that information — the pointer from a
+live hit to the decompiled body.
+
+## 4. Annotate hits back into Ghidra
+
+```bash
+./greeneye.sh analyze dumps/<run> --symbols symbols-gsp.json --out run.md
+python3 - <<'PY'
+from greeneye.ghidra import export_hits
+from greeneye.symbols import SymbolDB
+db = SymbolDB(); db.load_json("symbols-gsp.json")
+print(export_hits("dumps/<run>", "greeneye_hits.tsv", symbols=db))
+PY
+```
+
+`greeneye_hits.tsv` is `addr<TAB>hits<TAB>symbol<TAB>annotation`. Then run the
+bundled Java script in Ghidra headless:
+
+```bash
+$GHIDRA/support/analyzeHeadless <proj_dir> <project> -process <program> -noanalysis \
+ -scriptPath debug/risc-v-gsp-rm-vm/ghidra \
+ -postScript MarkGreenEyeHits.java /abs/path/greeneye_hits.tsv
+```
+
+`ExportForGreenEye.java` is the reverse helper: it walks the current program
+and writes a green-eye-compatible `addr<TAB>name<TAB>size` TSV.
+
+## 5. Breakpoints from Ghidra
+
+Two practical routes today:
+
+1. `greeneye decompose/decompile` → note the file vaddr → script
+ `s.hook("0x…")` (green-eye translates to runtime via the bias you loaded).
+2. Use `symbols export` from the Ghidra index, then `s.hook("<name>")` by
+ name — the most robust route across recompilations.
+
+A future Ghidra-side green-eye console (roadmap item) can call
+`greeneye` through its CLI to set breakpoints directly from the tool.
+
+## 6. Limits
+
+* Wrapper-generated functions are re-laid-out between driver patch versions;
+ transfer names, never raw wrapper addresses (the CUDA playbook's lesson).
+* The decompiler does not recover struct types; structure schemas for
+ green-eye decoding must be written or exported separately.
+* `decompiled_index.tsv` buckets are large and gitignored; keep symbol JSONs
+ small (name/size/subsystem only) if they must be tracked.
diff --git a/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md b/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md
new file mode 100644
index 0000000..1a1ae82
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md
@@ -0,0 +1,136 @@
+# 06 — Accuracy: what the green-eye test bed does and does not prove
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+> This document is deliberately conservative. Treat every claim without a
+> recorded A/B as unverified.
+
+## 1. Purpose
+
+green-eye exists to measure how accurately Open-IOV's GSP emulation
+(`gsp-rm-vfio` and the in-kernel emulation) reproduces the behaviour of the
+real GSP firmware, and to make the firmware itself analysable. That makes
+accuracy a first-class deliverable, not an assumption. This document defines
+the fidelity levels, the measurement contract, and the rules for honest
+conclusions.
+
+## 2. Fidelity levels
+
+| Level | Property | green-eye today |
+|---|---|---|
+| **L0 — instruction semantics** | RV64IMAC executes as on the device core | high (QEMU TCG, same ISA family) |
+| **L1 — memory behaviour** | reads/writes/ordering as firmware expects | high for RAM-resident code, *conditional* on mapping the same physical layout |
+| **L2 — time/concurrency** | cycle counts, DMA latency, interrupt timing | **not modelled** (do not claim) |
+| **L3 — device environment** | falcon windows, WPR2, CSB/TRANSCOM, PMU, timers, FB DMA, IRQ routing | **absent** (roadmap) |
+
+The practical reading: green-eye is an **execution and memory** microscope,
+not a timing or silicon model. Conclusions of the form "the firmware writes X
+before Y" are in scope; "the firmware takes N µs" is not.
+
+## 3. The real boot chain vs a bare QEMU VM
+
+The physical path the project already proved is:
+
+```
+FWSEC (falcon) -> WPR2 carve -> Booter Load (SEC2) -> RISC-V start
+ -> bootloader/appMain -> GSP-RM init -> RPC server
+```
+
+The GSP application's own entry supports this model: `_start` receives
+bootargs from the bootloader in `a0`–`a5` (bootargs PA/VA, ELF PA/size, load
+base PA, write-protected-region id), sets `stvec`, sets its stack, and calls
+`appMain` (see `uproc/gsp/src/kernel/start.S`). It expects falcon/CSB register
+windows, a TCM/MPU layout and a PMU-provided environment that `qemu virt`
+does not have.
+
+**Therefore:** loading the raw `.fwimage` into `qemu virt` cannot boot it.
+The `greeneye probe` command is designed for exactly this: it loads the
+segments, runs bounded, and reports the first divergence
+(`guest_errors,unimp,int` log + stopped PC + symbol). That report is a *memory
+and control-flow map* of what the firmware tries to touch first — useful data,
+but not a boot.
+
+**Measured first-touch (2026-10-02, TU104 570.144 image).** The firmware
+enters at relocated `0x84009000`, executes six instructions, and faults at
+firmware vaddr `0x4009014` on a 4-byte read of `0x2000000000000A00`
+(load access fault), after which it falls into an `mtvec=0` fetch loop. That
+address is the first entry for the R1 device model's register map.
+
+## 4. Re-hosting the uproc components (the higher-fidelity path)
+
+The GSP firmware contains the uproc components (`gsp`, `os/libos`,
+`nvriscv`, `sec2*`, `soe*`, `pmu_sw`, `conf_compute`, …). Re-hosting them
+means compiling a *subset* against a QEMU-virt platform shim:
+
+| Area | Can be faithful | Cannot be faithful |
+|---|---|---|
+| task/RPC logic, parsers, state machines | yes — this is ordinary code | — |
+| memory layout of app-owned structures | yes, if schemas/addresses are pinned | TCM/MPU protections |
+| libos scheduler semantics | approximated (round-robin, timers) | real interrupt latencies, partition switching |
+| register/CSB access, falcon windows | stubbed with recorded behaviour | true device semantics |
+| PMU/SEC2/ACR/Booter interactions | modelled as messages | boot ROM, secure state machines |
+
+Re-host fidelity must be stated per module in the session metadata, and every
+"this is what the firmware does" claim should be cross-checked against the
+physical RPC traces (PLAN B) before it is used to change the emulation.
+
+## 5. Measurement contract for `gsp-rm-vfio` accuracy
+
+Define these metrics once and use them everywhere:
+
+| # | Metric | How it is measured |
+|---|---|---|
+| M1 | **Inventory coverage** | set of controls/RPCs observed in a real run vs the emulation's handled set (and green-eye's observed set) |
+| M2 | **Reply agreement** | field-by-field diff of replies (real vs emulated); canonical-size and consumed-field checks included |
+| M3 | **Ordering agreement** | sequence alignment of the control/event stream (edit distance / LCS) |
+| M4 | **State agreement** | state-machine transitions observed in the firmware vs the emulation's internal state model |
+| M5 | **Error-path agreement** | injected faults produce the same status/notifier path |
+| M6 | **Memory side-effects** | read/write map agreement against the emulation's shadow state; snapshot hashes for fixed points |
+| M7 | **Timing** | *not claimed*; only order-of-events budgets (e.g. "reply before the 4 s poll") |
+
+Reference capture:
+* real hardware/backend: the project's existing RPC logs (module trace,
+ `gsp-phys-log-dump`, status-queue drains) and the static-config oracle;
+* green-eye: `events.jsonl` + snapshots, symbol-annotated.
+
+Comparison: `greeneye compare dumps/real-<run> dumps/greeneye-<run>
+--symbols`, then a short table of M1–M6 with counts. Never publish a single
+"accuracy %" number without the metric breakdown.
+
+## 6. Adversarial interoperability use
+
+green-eye is also a safe place to be adversarial: corrupt a reply, flip a
+capability bit, truncate a control payload, or inject a fault at a chosen
+firmware PC, then observe whether the emulation and the firmware agree on the
+failure path. The rules:
+
+1. The simulator's reaction is *evidence about the firmware*, not about
+ silicon. Confirm any conclusion against a physical cycle before it changes
+ the product.
+2. Keep the injected fault and the observed path in the session recording.
+3. Do not use simulator timing as a security proof (side-channel claims need
+ physical measurement).
+
+## 7. Validating the tool itself
+
+Before trusting a green-eye session:
+
+* the demo firmware self-test must pass (`greeneye demo`, `make test`);
+* the session must contain the expected `hook`/`watch` registrations and a
+ coherent `bp_hit` sequence (the analyzer highlights empty sessions);
+* the UART banner and control-block magic are cheap liveness checks;
+* golden sessions (checked-in expectations) for the demo detect regressions
+ in the toolchain itself.
+
+## 8. Known accuracy gaps (living list)
+
+1. No TU104 device model: no boot beyond the first unmapped access.
+2. No time/concurrency model; multi-core and partition-switch behaviour is
+ approximated.
+3. PMU/SEC2/ACR/Booter behaviour is message-level only.
+4. Re-hosted uproc code is compiled for `qemu virt`, so compiler/layout
+ differences with the shipped image must be accounted for when comparing
+ control flow.
+5. Interrupt delivery and MSI semantics differ; error-path M5 is the weakest
+ metric until the device model exists (08).
+
+Each gap maps to a roadmap item; do not silently ignore them in reports.
diff --git a/debug/risc-v-gsp-rm-vm/docs/07-PORTABILITY-PLAYBOOKS.md b/debug/risc-v-gsp-rm-vm/docs/07-PORTABILITY-PLAYBOOKS.md
new file mode 100644
index 0000000..5426bf1
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/07-PORTABILITY-PLAYBOOKS.md
@@ -0,0 +1,126 @@
+# 07 — Portability playbooks: new GSP, new device, new architecture
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+These playbooks cover the three churn axes the project expects: a new GSP
+firmware version, the same firmware on a new device, and a new GPU
+architecture (e.g. GB10 / DGX Spark / Surface-class). They reuse the
+project's existing version discipline and add green-eye's runtime layer.
+
+## 0. Common setup
+
+1. Acquire the firmware for the target (driver runfile or `linux-firmware`):
+ extract the GSP boot image (`.fwimage`) and record its hash. For TU10x,
+ `firmware/57X-GSP/<ver>/gsp-<ver>.fwimage.bin` is the extracted form.
+2. Lay down a versioned directory; never overwrite a working version.
+3. Build a symbol DB for the new image:
+ * import the fwimage into the existing Ghidra project tooling
+ (`tools/ghidra/analyze_fwimage.py`, `DecompileFunctions.java`),
+ * `./greeneye.sh symbols export --index <export.tsv> -o symbols-<ver>.json`.
+4. Record a baseline session of the previous version (see P1.4) so the
+ comparison has an anchor.
+
+## P1. New GSP firmware version (same family)
+
+**Goal:** know what changed in the firmware's runtime behaviour before the
+emulation is adjusted.
+
+1. **Static triage** — the project's existing flow: decompile the new
+ fwimage, diff the RPC dispatcher, subcommands, message layouts and version
+ strings against the previous version (`cudatool.py delta`-style discipline
+ for firmware; `diff_corpus.py` for the corpus).
+2. **Probe the new image** — `./greeneye.sh probe <new.fwimage>`: record the
+ first divergence and the first MMIO/read/write addresses. Compare with the
+ previous version's probe; changed first-touch addresses are early
+ structure-change signals.
+3. **Re-host / demo run** — if uproc sources for the family are available,
+ build the rehosted subset against the QEMU-virt shim and run the same
+ scripted stimulus used for the previous version.
+4. **Baseline + candidate sessions** — for each version capture:
+ * `greeneye record --kernel <rehosted elf> --duration N --snapshot <regions>`
+ or the scripted harness (`run`),
+ * the real-backend reference traces for the same workload.
+5. **Compare** — `./greeneye.sh compare dumps/<old> dumps/<new> --symbols`:
+ hit-count deltas per function, watchpoint deltas, written-page deltas.
+ Investigate every function whose call-count changed before touching the
+ emulation.
+6. **Metrics** — fill in M1–M6 from `06-ACCURACY.md` for the new version
+ against the real backend. Attach the table to the version-bump PR.
+7. **Acceptance** — no rung regression in the existing guest ladder, the
+ control inventory diff is explained, and every new/changed control has a
+ version-keyed handler or an explicit "not used" note.
+
+## P2. Same GSP version, new device
+
+**Goal:** prove device-profile portability (the project direction is
+"configuration + verification, not new code").
+
+1. Collect the device profile: PCI IDs, BAR layout/sizes, FB geometry,
+ engine inventory, interrupt topology, VBIOS/ROM specifics.
+2. Verify the firmware pairing: the image family (TU10x/GH100/LS10 …), FMC
+ partition manifests, and the version string; a mismatched pairing presents
+ as an emulation bug (the project learned this with 570.144 vs 570.211).
+3. `./greeneye.sh probe <device fwimage>` on the new device image; compare
+ first-divergence maps with the reference device. Differences here usually
+ point at profile data, not logic.
+4. Re-run the P1.4/1.5 loop with the device profile applied; record probe +
+ compare evidence in the portability matrix.
+5. Validate on the physical device with the supervised harness before
+ claiming support (`docs/06-portability/hardware-software-matrix.md`
+ discipline: tested and documented, not "should work").
+
+## P3. New architecture (GB10 / DGX Spark / Surface-class)
+
+**Goal:** survive a new silicon generation (likely a newer NV-RISCV core,
+newer PMU/SEC2 images and possibly RV64GC rather than RV64IMAC).
+
+1. **Confirm the ISA and image shape** — `file`/`readelf` the new fwimage:
+ ISA string, segments, entry, compressed/float extensions. Update the
+ rehost build flags (`-march=…`) accordingly; note that QEMU supports
+ RV64GC even if green-eye's demo uses IMAC.
+2. **Check the platform's boot variant** — newer platforms use FMC/manifest
+ partition models (`uproc/*_riscv/fmc`, `safety/PMU-*`, `conf_compute`
+ Init→ACR→RM flow). Record which partition model the image uses; the
+ rehost shim needs a matching entry/return convention.
+3. **PMU/SOE deltas** — diff the PMU/SOE headers and firmware
+ (`pmu_sw`, `uproc/soe*`) because the GSP environment provisions identity
+ and clock state through them. Any change here invalidates prior
+ environment assumptions.
+4. **Run the P1 loop** with the new architecture profile; expect the probe to
+ show *different* first-touch addresses (new register windows). Update the
+ device-model scope in `08-ROADMAP.md` with what the probe revealed.
+5. **Host portability** — green-eye itself is Python and QEMU; on aarch64
+ hosts install `qemu-system-misc` there and the same commands run. Keep
+ host-specific paths out of session metadata.
+
+## P4. A/B: bare metal vs vfio-mdev (the core accuracy ritual)
+
+**Goal:** quantify how closely the virtual function's emulation tracks the
+real firmware for a chosen workload.
+
+1. **Reference capture (real):**
+ * hardware path: the supervised physical backend cycle, capturing RPC
+ records/log dumps and the static-config oracle;
+ * or the vfio-mdev path with the same workload.
+ Save the reference event stream in a session-compatible form (or the
+ vendor logs plus a conversion note).
+2. **green-eye capture:** run the same workload's firmware-side stimulus in
+ the rehosted simulator; record hooks, watchpoints, snapshots.
+3. **Compare:** `greeneye compare` for hit/watch/write profiles; for RPC
+ payloads use M2 field agreement and M3 ordering (a small script or a
+ plugin can emit per-control events).
+4. **Report:** metric table (M1–M6), changed-function list, and an explicit
+ statement of L2/L3 limits from `06-ACCURACY.md`.
+5. **Escalation:** a divergence that changes emulation behaviour must be
+ reproduced on the physical backend before the fix lands; simulator-only
+ divergence that cannot be reproduced is recorded as a fidelity gap.
+
+## Evidence checklist (attach to any portability task)
+
+* firmware hash + version string + pairing notes;
+* `probe` output (first divergence, first-touch map);
+* symbol DB hash and the Ghidra export provenance;
+* session directories for baseline and candidate (or their sha256 list);
+* `compare` report;
+* M1–M6 table with the reference-capture method;
+* unexplained diffs list (empty is a claim, not a default).
diff --git a/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
new file mode 100644
index 0000000..c8b4638
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
@@ -0,0 +1,116 @@
+# 08 — green-eye roadmap
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+> Items here are **not** v0.1. Do not start them ad hoc; each has a gate.
+
+## R0. Product principles
+
+* green-eye stays a **development tool**: no shipped dependencies, no guest
+ presence, no kernel-module coupling.
+* Fidelity improvements must be *measurable* (metrics table in 06) and must
+ not slow the fast path (demo + probe) below usefulness.
+* Every new backend must satisfy the same `Session` contract so scripts,
+ probes, plugins and recordings survive.
+
+## R1. Platform device model (raise L3)
+
+**Why:** a bare `qemu virt` cannot boot the real fwimage; the probe shows the
+first unmapped access and stops.
+
+**Scope:** model the minimum GSP environment:
+falcon windows (FALCON/CSB/TRANSCOM registers), WPR2/FWSEC state registers
+with read-back semantics, PTIMER, PMU/SEC2 mailbox stubs, FB DMA/IOMMU
+address translation for the firmware's DMA descriptors, and interrupt
+routing. QEMU `qemu-system-riscv64` with a custom device (`-device`) is the
+preferred vehicle; TCG plugins provide the observation points.
+
+**Gate:** R1 starts only when a portability task (P2/P3) or an accuracy
+measurement (P1.6) requires booting real firmware in-simulator. Until then,
+use the probe + rehosted uproc paths.
+
+**Exit criteria:** `greeneye probe <fwimage>` reaches a stable address and
+reports "waiting for host message" rather than a fault; the first host↔GSP
+RPC records are captured in a session; M3 ordering can be measured against
+the physical backend.
+
+## R2. Custom simulator (explicitly requested; not first version)
+
+**Why:** QEMU is an excellent execution engine but a poor *workflow* engine
+for this project: no in-process API, RSP serialisation for every access,
+limited snapshotting, and a device model designed for whole boards rather
+than firmware servers. A custom simulator can be deterministic, replayable
+and scriptable at native speed.
+
+**Shape (to build later, not now):**
+* an RV64IMAC interpreter/JIT with a **pluggable memory map** (RAM, MMIO
+ hooks, DMA windows) and a **deterministic scheduler** (no realtime claim);
+* native `Session` integration — replace the RSP transport with a
+ `Backend` interface, keep hooks/watchpoints/memory/recording identical;
+* first-class **snapshot/restore** of full machine state (time-travel
+ debugging, branch-flip experiments);
+* an **event bus** feeding the existing `events.jsonl` schema;
+* a **stub DSL** for device registers (read/write scripts with state), so a
+ new GPU generation is configuration, matching the project's portability
+ direction.
+
+**Candidates to evaluate at that time:** extending QEMU TCG with a thin
+in-process backdoor, `libriscv`, `rv8`, or a from-scratch interpreter.
+Realtime is explicitly *not* a requirement; determinism and introspection are.
+
+**Gate:** R1 must have stabilised the device semantics (otherwise the custom
+simulator inherits an unvalidated model). Estimated multi-week work; schedule
+only after the guest-bridge work (PLAN B) no longer depends on it.
+
+## R3. MCP server plugin
+
+**Why:** expose green-eye sessions to AI harnesses and other agents under the
+plugin model the user requested.
+
+**Shape:** a plugin (`plugins/mcp_server.py` when built) that wraps the
+session/analysis API as MCP tools: `list_sessions`, `open_session`,
+`symbols_resolve`, `set_breakpoint`, `run`, `memory_read`, `memory_hexdump`,
+`snapshot`, `compare_sessions`, `export_hits`. Transport local stdio first;
+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.
+
+## R4. Full-memory trace (TCG plugin integration)
+
+The optional `qemu/greeneye_trace.c` plugin records instruction execution and
+memory accesses. Roadmap: build it against the vendored QEMU headers, stream
+its JSONL into the session format as `exec`/`access` events, and teach
+`analyze` to render coverage/heatmaps and written-page timelines. This is the
+"complete map" upgrade from stop-based observation to full access tracing.
+
+**Gate:** available once the QEMU plugin headers are vendored (or the build
+environment provides them). Does not block v0.1 — watchpoint recording plus
+step traces already cover the common cases.
+
+## R5. Fuzzing and mutation harness
+
+Use AFL++ QEMU mode (or a custom mutator) against the rehosted firmware's
+RPC input surface, with green-eye recording crash/trap paths and symbol
+annotation. Requires R1/R2 for stable input plumbing. Output: a corpus of
+malformed controls and the firmware's error paths, feeding the emulation's
+fail-closed rules (project security hygiene).
+
+## R6. Multi-version orchestration
+
+Scripts that, given a set of firmware images and device profiles, run the
+probe + rehost + compare pipeline for the whole matrix and emit the M1–M6
+tables. This automates `07-PORTABILITY-PLAYBOOKS` P1–P3.
+
+## R7. Ghidra two-way console
+
+A Ghidra script/plugin that talks to a live green-eye session (via the CLI or
+a local socket) to toggle breakpoints/watchpoints from the decompiler and to
+highlight hit addresses. Until then, the hit-TSV + `MarkGreenEyeHits.java`
+flow is the supported path.
+
+## Not on the green-eye roadmap
+
+* Graphics/display emulation, Windows guest internals, multi-tenant policy —
+ these belong to the main project roadmap, not to this analysis tool.
+* Replacing Ghidra: green-eye is the dynamic complement, never a decompiler.
diff --git a/debug/risc-v-gsp-rm-vm/docs/09-GSP-FIRMWARE-NOTES.md b/debug/risc-v-gsp-rm-vm/docs/09-GSP-FIRMWARE-NOTES.md
new file mode 100644
index 0000000..216bf93
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/09-GSP-FIRMWARE-NOTES.md
@@ -0,0 +1,104 @@
+# 09 — GSP/uproc firmware notes used by green-eye
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+> Exact local-source provenance is recorded in the local task record; this
+> tracked document intentionally keeps source locations generic and cites
+> public material.
+
+## 1. The microcontrollers around the GPU
+
+From the open-iov wiki **GPU Firmware** page and the linked **NVIDIA RISC-V
+Story** presentation (the project mirrors the wiki locally; the PDF is a
+public RISC-V summit deck):
+
+* **Falcon (FAst Logic CONtroller)** is NVIDIA's proprietary control
+ processor, used across >15 hardware engines for over a decade. It was
+ designed for flexibility, long memory latency, low area and security.
+ Limits that forced a successor: small addressing range, modest performance
+ (~0.67 DMIPS/MHz, ~1.4 CoreMark/MHz), no data cache, no rich OS support.
+* **NV-RISCV** is NVIDIA's own implementation of a RISC-V core chosen as the
+ next-generation falcon: more performance (>2× falcon claimed), wider
+ addressing, TCM, vectored interrupts, security features (TZ-style), with
+ area targets under 0.1 mm² at 16FF.
+* The GPU hosts several of these cores: the **GSP** (GPU System Processor)
+ running a rich firmware stack, plus security/boot engines (**SEC2**, ACR,
+ SOE) and the **PMU** microcontroller. The RISC-V Story deck documents the
+ falcon-to-RV transition and the DMA/mailbox lines between engines.
+
+## 2. GSP application structure
+
+The GSP firmware is an ELF64 RV64IMAC image (the TU104 570.144 `.fwimage` used
+by the probe has entry `0x4009000`, 33 program headers, stripped). Its
+application layer is a multi-task RTOS program:
+
+* `_start` (hand-written assembly) sets the trap vector to an early panic
+ handler, initialises the stack, pushes two zero words to help debugger
+ backtracing, and calls `appMain`. The **bootloader passes arguments in
+ a0–a5**: bootargs PA and VA, ELF PA and size, load base PA, and the
+ write-protected region id. A re-host harness must reproduce this ABI.
+* `init.c` creates the tasks and starts the RTOS; the included driver set
+ covers MPU, ODP (on-demand paging), memory, and the RISC-V system library.
+* The boot configuration selects `NVRISCV_PARTITION_SWITCH` variants, i.e.
+ images can run under a separation-kernel partition model.
+
+## 3. RTOS and libos layers
+
+Two OS layers appear in the vendor snapshots:
+
+* **SafeRTOS/NVRTOS** — the classic resident RTOS: priority scheduling,
+ timers, events/signals, IPC and DMA support; ~38 KiB TCM-resident kernel;
+ missing paging, task reset, capability security. Used by the GSP test
+ firmware partition model.
+* **LIBOS** (exokernel design, early draft) — designed to be TCM-resident
+ (projected 1–6 KiB), capability-based security, round-robin scheduler,
+ DMEM on-demand paging, module configuration so unused features are not
+ paid for. This is the direction the GSP stack evolves toward.
+
+**Implication for the simulator:** the scheduler is not the interesting part
+for RPC accuracy; the task/dispatcher semantics are. Green-eye's demo mirrors
+that shape (mailbox → dispatcher → handler → counters), and a rehost shim
+should stub the scheduler while keeping task entry points and message
+handling real.
+
+## 4. Boot and partition models
+
+* **MP-SK/FMC** (separation kernel): images are built as partitions. One
+ observed GSP test image has two partitions: a SafeRTOS first-stage
+ bootloader partition (clobbered after hand-off via partition switch), and a
+ high-sec test partition that reads performance counters, writes them to
+ shared memory and switches back.
+* **Confidential-compute skeleton**: six partition stubs — RM Proxy, RM
+ (mutually exclusive with RM Proxy), SPDM, ACR, Init and Attestation — with
+ the boot flow **Init → ACR → RM**. The vendor tooling side-loads images to
+ a live GSP with a debug command (`nvwatch …`) for bring-up.
+
+These models matter for portability: a new architecture may change the
+partition layout or boot order, and green-eye's probe is the quickest way to
+see the new first-touch sequence before a device model exists.
+
+## 5. Memory, DMA and registers
+
+* The firmware assumes **TCM** and an MPU (protection regions are generated
+ at link time); `qemu virt` has neither, so the probe loads segments into
+ RAM and faults at the first register access.
+* The engines exchange data over **CSB/TRANSCOM windows and mailboxes** with
+ DMA into host/FB memory; the physical backend work in Open-IOV already
+ models the message-queue side (status/command queues, doorbells) — that is
+ the layer green-eye can emulate first when R1 lands.
+* The GSP kernel's start path receives its memory permissions as boot args
+ from the bootloader rather than discovering them, which is why a
+ re-host should pass the same structure rather than re-derive it.
+
+## 6. How green-eye models this today (and honestly, does not)
+
+| Firmware aspect | green-eye v0.1 |
+|---|---|
+| Task/dispatcher/state-machine logic | modelled faithfully by the demo and by rehosted code |
+| RPC handler input/output behaviour | observable and recordable; schemas decode structures |
+| RTOS scheduling, interrupts, partition switch | approximated / absent |
+| Falcon/CSB/TCM/PMU registers | absent (probe reports first touch; R1 roadmap) |
+| Boot ROM chain (FWSEC/booter/SEC2) | out of scope for the VM; the physical backend owns it |
+
+Use the notes above when designing new rehost shims or a device model: the
+goal is to keep everything *above* the register/mailbox boundary real, and to
+model that boundary explicitly rather than pretend it away.
diff --git a/debug/risc-v-gsp-rm-vm/docs/10-RELATED-WORK.md b/debug/risc-v-gsp-rm-vm/docs/10-RELATED-WORK.md
new file mode 100644
index 0000000..a2a45db
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/10-RELATED-WORK.md
@@ -0,0 +1,76 @@
+# 10 — related work: why not Frida, and what each tool is for
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+## 1. Frida
+
+**Verdict for the firmware VM: not applicable.** Upstream Frida ships no
+RISC-V target (checked against the 17.20 release assets: x86/x86_64,
+arm/arm64, mips, macOS/Windows/QNX/Prospero — no `linux-riscv64`), and
+frida-agent requires a hosted OS process. The GSP image is bare-metal RV64
+with its own libos; there is nothing to inject into, and even a hypothetical
+RISC-V port would need an OS layer the firmware does not have.
+
+**Where the Frida *idea* lives in green-eye:** the scripting model. `Session`
+offers `hook`/`watch`/`memory`/`events` with Python callbacks, which is the
+part of Frida that matters for this workflow. The difference is transport:
+RSP to QEMU instead of an injected agent.
+
+**Where Frida is applicable in the project:** host- or guest-userland x86_64
+work (e.g. the CUDA userspace layer) if a dynamic tracer is ever wanted there.
+That is outside this tool's scope; the existing ptrace tracer covers the
+guest today.
+
+## 2. GDB / gdbstub
+
+QEMU's gdbstub is the transport both tools share. GDB itself is still useful
+for *debugging green-eye* (stub behaviour, protocol traces), and `gdb
+-multiarch` can inspect the same target interactively. It is not the
+instrumentation layer here because:
+
+* every operation becomes a subprocess/parse problem instead of an API call;
+* recording, symbol annotation, structure decoding and session diffing would
+ have to be rebuilt around GDB/MI anyway;
+* the project already uses GDB Python scripts for the CUDA corpus; green-eye
+ is the equivalent for firmware, with a stronger data model.
+
+## 3. QEMU TCG plugins
+
+The correct complement, not a competitor: plugins execute inside QEMU and can
+observe **every** instruction and memory access, which gdbstub breakpoints
+cannot. green-eye's `qemu/greeneye_trace.c` emits a JSONL access trace for
+exactly this; integrating it into the session format is R4 on the roadmap.
+Rule of thumb: use green-eye hooks for semantics ("handler X ran with these
+args") and TCG plugins for coverage/heatmaps ("every write to this window").
+
+## 4. Unicorn / emulator libraries
+
+Unicorn (and similar CPU emulators) can execute RV64 code faster to set up
+than QEMU and embed cleanly in Python, but they model no board at all and
+have weaker debug infrastructure. They are candidates for the **custom
+simulator** (R2) once the required device semantics are known — the same
+reason the roadmap defers that work.
+
+## 5. `rr` / record-replay
+
+`rr` does not support RISC-V guests and records at the host-process level.
+QEMU's own record/replay (`-icount`) can provide deterministic reruns later;
+green-eye's session recording occupies a different niche (semantic events,
+not full replay) and can coexist.
+
+## 6. Comparison table
+
+| Tool | Layer | RISC-V firmware | green-eye relationship |
+|---|---|---|---|
+| Frida | userland process injection | no | conceptual model only |
+| GDB + gdbstub | execution control | yes (transport) | debugging green-eye; same stub |
+| QEMU TCG plugin | full instruction/memory trace | yes | complement (R4) |
+| green-eye | semantic instrumentation + recording + analysis | yes | this tool |
+| Unicorn/rv8/etc. | CPU emulation library | yes | R2 candidate |
+| Ghidra | static decompilation | yes | symbol/hit bridge (05) |
+
+## 7. Reading list
+
+* `docs/01-DESIGN.md` — where each layer sits.
+* `docs/05-GHIDRA-BRIDGE.md` — the static/dynamic join.
+* `docs/08-ROADMAP.md` — R1 device model, R2 custom simulator, R3 MCP.