summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 09:13:00 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 09:13:00 -0400
commit1e701396b1b6d204dfd8e5a8ddcd3007038029ac (patch)
tree8dde895cf9d3e66ce912d01cb1a72ff674dc850f /debug/risc-v-gsp-rm-vm/docs
parentab48e44832db55fdedadcf44f55fa9904ca36f2f (diff)
downloadopen-iov-1e701396b1b6d204dfd8e5a8ddcd3007038029ac.tar.gz
open-iov-1e701396b1b6d204dfd8e5a8ddcd3007038029ac.zip
debug: green-eye wave 2 - injection SDK, Frida-like calls, Ghidra control bridge, long-term docs
Adds to debug/risc-v-gsp-rm-vm/: - Session.call / run_until: Frida-like remote function calls and run-to-cursor, with register restoration; verified live (rpc_ping -> 0xaffe). - symbol+offset target resolution (gsp_ctrl+0x10) across all APIs and the REPL. - Injection SDK (sdk/) and 'greeneye inject': position-independent RV64 service blobs (verified zero relocations), loaded into RAM, registered in the demo handler table, and invoked either directly or through the firmware mailbox dispatcher. Demo ABI extended: mailbox 16..23 selects slots, arg0 carries the service command, arg2 added (struct now 0x190; schema updated). - Ghidra control bridge (greeneye ghidra + GreenEyeControl.java): run-until-here, breakpoints, memory/register access, remote calls, hit export; verified end-to-end with Ghidra headless (run_until reached rpc_sum from a GhidraScript). - MarkGreenEyeHits.java fixed for the Ghidra 12 createBookmark API; verified headless together with ExportForGreenEye.java. - Analyzer renders a QEMU first-fault table for probe sessions (fault count, epc with firmware vaddr translation, access address/size, reason). - Tests now 35 green, including injection, Ghidra bridge, remote call, run-until and REPL command smoke tests. - Documentation: docs/11-16 (GSP RM service developer guide, injection SDK specification, Ghidra control, adversarial interoperability guide, agentic GSP IDE, custom simulator design) and GREEN-EYE-ROADMAP.md (long-term H1-H7 program); doc maps and the project RISC-V VM doc updated.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md7
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md5
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/11-GSP-RM-SERVICE-DEVELOPER-GUIDE.md181
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/12-INJECTION-SDK.md149
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/13-GHIDRA-CONTROL.md154
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md399
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/15-AGENTIC-GSP-IDE.md387
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md449
8 files changed, 1731 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
index a9f4251..f1e1946 100644
--- a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
+++ b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
@@ -79,6 +79,13 @@ Interactively:
| [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 |
+| [11-GSP-RM-SERVICE-DEVELOPER-GUIDE](11-GSP-RM-SERVICE-DEVELOPER-GUIDE.md) | writing/injecting/debugging RM services |
+| [12-INJECTION-SDK](12-INJECTION-SDK.md) | injection blob/ABI specification |
+| [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) |
+| [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/08-ROADMAP.md b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
index c8b4638..10f4f5f 100644
--- a/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
+++ b/debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md
@@ -1,6 +1,11 @@
# 08 — green-eye roadmap
> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+> This is the **near-term, committed** roadmap (rungs R0–R7 below). The
+> **long-term program** — adversarial firmware navigation, adjacent
+> microcontrollers, a non-QEMU custom simulator, a Verilog soft-processor
+> environment and the agentic platform — lives in
+> [`../GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md).
> Items here are **not** v0.1. Do not start them ad hoc; each has a gate.
## R0. Product principles
diff --git a/debug/risc-v-gsp-rm-vm/docs/11-GSP-RM-SERVICE-DEVELOPER-GUIDE.md b/debug/risc-v-gsp-rm-vm/docs/11-GSP-RM-SERVICE-DEVELOPER-GUIDE.md
new file mode 100644
index 0000000..6ba8d69
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/11-GSP-RM-SERVICE-DEVELOPER-GUIDE.md
@@ -0,0 +1,181 @@
+# 11 — GSP RM firmware service developer guide
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status and 06 for the
+> fidelity caveats. Exact proprietary API names are not reproduced in tracked
+> docs; local provenance lives in the local analysis notes.
+
+## 1. What this guide is for
+
+The GSP runs a resource manager (RM) as **services**: message handlers, state
+machines and engines that own objects (devices, address spaces, channels,
+memory, notifiers) and answer controls. Open-IOV emulates those services; the
+long-term goal of green-eye is to let a developer **write, inject, run and
+debug their own versions of these services** before (or instead of) changing
+the product emulation.
+
+This guide teaches the loop on the demo uproc — which implements the same
+shape as a GSP service — and maps each step to the future GSP-RM work in
+[`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) H1.3.
+
+## 2. Service anatomy
+
+| Concept | Demo uproc (today) | GSP-RM (roadmap H1.3) |
+|---|---|---|
+| Entry point | exported symbol `ge_demo_service_dispatch` | service entry registered with the RM core |
+| Dispatch ABI | `u64 dispatch(u64 cmd, u64 a0, u64 a1)` | same shape via the service registry shim |
+| Reachability | firmware mailbox commands 16..23 select handler slot 0..7; `arg0` is the service command, `arg1`/`arg2` are parameters | RM message/control dispatch routed by the simulator's RM-core |
+| State | service-local (stack/registers) or the control block | RM objects and service-private memory |
+| Talking to peers | control-block fields + mailbox; debugger calls | RM-core calls and inter-service messages |
+| Observability | hooks/watchpoints/sessions/TCG trace | same, plus RM-core call interception |
+
+A service must be **position-independent**: no globals, no libc, no absolute
+addresses. Everything it calls is either its own code or an address passed in
+by the injector (the future RM-core shim will pass a table of callable
+entry points, which is also the interception point).
+
+## 3. Writing a service
+
+1. Copy the SDK shape from `sdk/`:
+ * `greeneye_service.h` — the ABI header;
+ * `injected_demo.c` — a working multi-command service.
+2. Implement `ge_service_fn`:
+ ```c
+ __attribute__((noinline)) ge_u64 my_service_dispatch(ge_u64 cmd, ge_u64 a0, ge_u64 a1)
+ {
+ switch (cmd) {
+ case 1: return a0 ^ 0xbeefu;
+ case 2: return a0 + a1;
+ default: return ~cmd;
+ }
+ }
+ ```
+3. Keep it PIC-clean: no `static` data with pointers, no string globals, no
+ division (the demo build has no libc), no stack beyond a few KiB.
+4. Build:
+ ```bash
+ make -C sdk # or point the Makefile at your source
+ make -C sdk relocs # must print "no relocations in this file"
+ ```
+5. Fix any relocation the linker reports before injecting; relocations mean
+ the blob will not run at an arbitrary load address.
+
+## 4. Injecting and invoking
+
+```bash
+./greeneye.sh inject --service sdk/build/injected_demo.elf \
+ --cmd 1 --arg0 0x41 --arg1 0x42
+```
+
+What happens (all recorded):
+1. Every `PT_LOAD` is written into RAM (default `0x81000000`; auto-sized to
+ fit the target).
+2. The service dispatch entry is patched into handler slot 6
+ (`--slot` selects 0..7).
+3. A **direct debugger-side call** runs the service immediately
+ (`greeneye.inject.invoke_remote`): this is the unit-test path, independent
+ of firmware state.
+4. After the firmware settles, a **firmware-dispatched call** writes
+ `arg0=cmd, arg1=a0, arg2=a1, mailbox=16+slot` and reads `result`
+ (`greeneye.inject.invoke_via_firmware`): this is the integration path.
+
+Programmatic form:
+
+```python
+from greeneye.inject import load_service, invoke_remote, invoke_via_firmware
+svc = load_service(s, "sdk/build/injected_demo.elf", slot=6)
+print(hex(invoke_remote(s, svc, 2, 0x10, 0x20))) # 0x30
+s.run(duration=0.3)
+print(hex(invoke_via_firmware(s, svc, 2, 0x10, 0x20))) # 0x30
+```
+
+## 5. Talking to other services and the host
+
+On the demo:
+* the **control block** is the shared state (`gsp_ctrl`); services coordinate
+ through its fields and the mailbox command range;
+* the **host/debugger** is a peer via hooks, watchpoints and `Session.call`;
+* a service can call another service if it is handed the other entry address
+ (the SDK's second export `ge_demo_service_answer` demonstrates a second
+ callable point; the injector resolves any exported symbol).
+
+On real GSP-RM (H1.3) the same roles are filled by RM-core allocation and
+control calls and by messages between services. The simulator will intercept
+those calls so they can be logged, stubbed, or forwarded to the real
+implementation — which is exactly what makes the environment powerful for
+prototyping: a service you write can be exercised against recorded RM-core
+behaviour before the product emulation implements it.
+
+## 6. Resource surfaces an engine can use
+
+Category map (details are RM-version-specific and tracked privately where
+provenance requires it):
+
+| Surface | Purpose | How green-eye helps today |
+|---|---|---|
+| Memory / descriptors | allocate, map, free, register host or vidmem ranges | watchpoints on allocation state; snapshot/diff; schema decode |
+| Address spaces / page tables | GPU VA translation, mapping updates | hooks on map/unmap routines; memory diffs; TCG trace of accesses |
+| Channels / GPFIFO / USERD | submission plumbing | hooks and remote calls; observe ring state via snapshots |
+| Sync: semaphores, notifiers, interrupts | completion and signalling | watchpoints on sem values; event ordering in sessions |
+| Engine scheduling / runlists | prioritisation, preemption | call-graph and hook evidence; compare runs |
+| Controls / messages | configuration and queries | declarative probes; replay/compare across versions |
+| Device identity / caps / clocks | what the RM reports | schema decode of the static-config block; A/B vs hardware oracle |
+
+Everything here is observed through the same primitives, so an engine
+prototype is analysed exactly like firmware code.
+
+## 7. Debugging workflow
+
+1. **Unit-test with `call`:** breakpoints and hooks around the service entry,
+ `Session.call(entry, (cmd, a0, a1))`, inspect the return and watchpoints on
+ its data. Failures raise `RspTimeout` and leave the session recording.
+2. **Integration run:** `invoke_via_firmware` with the mailbox path, while
+ hooks on the firmware's dispatcher and watchpoints on the control block
+ record the full route.
+3. **Ghidra-driven:** `greeneye ghidra` + `GreenEyeControl.java` — execute
+ until a service address, set breakpoints from the decompiler, read
+ registers/memory, export hits (docs/13).
+4. **Coverage:** run with the TCG plugin filtered to the service load range
+ (`qemu/greeneye-trace.c`, `start=`/`end=` args) to see every access.
+5. **Regression:** record a session per command; `greeneye compare` detects
+ changed hit/watch/write profiles when the service or firmware changes.
+
+## 8. Rules and pitfalls
+
+* **PIC purity first.** If the blob needs relocation, it is not injectable
+ at an arbitrary address.
+* **No absolute addresses from the host.** Use exported symbols and
+ `load_service`, never hardcoded addresses.
+* **No unbounded loops.** A service that never returns blocks the call loop
+ until the timeout and then leaves the target stopped; the recorder shows
+ the last PC.
+* **Watchpoint the state, hook the entry.** Watchpoints are immediate (one
+ instruction step) and ideal for counters; hooks give arguments/returns.
+* **Never ship a service.** Injected code is a development experiment; the
+ product changes go through `open-iov-vfio` with the accuracy loop.
+* **Record everything.** A claim about a service without a session is not
+ evidence.
+
+## 9. Worked example: add a command
+
+```c
+/* sdk/injected_demo.c */
+case 7:
+ return (a0 | (a1 << 8)) & 0xFFFFFFFFull;
+```
+
+```bash
+make -C sdk
+./greeneye.sh inject --cmd 7 --arg0 0xab --arg1 0xcd
+# direct call -> 0xcdab
+# firmware call -> 0xcdab
+./greeneye.sh analyze <printed session dir> # event/hit report
+```
+
+## 10. What comes next (roadmap)
+
+* H1.3: services built against an RM shim, registered with a simulated RM
+ core, calling intercepted RM-core surfaces.
+* H2: the device model that lets real GSP-RM run in the simulator at all.
+* H4: adversarial navigation — diff service behaviour across firmware
+ versions, mutate inputs, infer capabilities.
+* H5: services that interact with PMU/SEC2/ACR models.
diff --git a/debug/risc-v-gsp-rm-vm/docs/12-INJECTION-SDK.md b/debug/risc-v-gsp-rm-vm/docs/12-INJECTION-SDK.md
new file mode 100644
index 0000000..26f1ffb
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/12-INJECTION-SDK.md
@@ -0,0 +1,149 @@
+# 12 — Injection SDK specification (v1, demo ABI)
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status and
+> [`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) §H1/H3 for the program.
+
+## 1. Goals and non-goals
+
+**Goals**
+* a repeatable, scriptable way to compile a piece of RV64 code, load it into a
+ live firmware target, register it, invoke it, and observe it with the same
+ hooks/sessions/analysis as firmware;
+* a clear ABI so services can be written once and run under the demo uproc
+ today and under a simulated RM core later;
+* an evidence trail: every load, registration and invocation is recorded.
+
+**Non-goals (v1)**
+* shipping any injected code (development-only);
+* hot-patching signed firmware on hardware;
+* bypassing security boundaries (the simulator is a study environment).
+
+## 2. Blob contract
+
+* **Format:** ELF64 `EM_RISCV`, `RV64IMAC`, soft-float ABI, one or more
+ `PT_LOAD` segments.
+* **Position independence:** built PIC (`-fPIC`, `-mcmodel=medany`,
+ `-mno-relax`, `-fno-asynchronous-unwind-tables`), no dynamic relocations.
+ The loader verifies by writing segments at the chosen load address; the SDK
+ `make relocs` target must report none.
+* **Self-sufficiency:** no libc, no global data containing addresses.
+ Caller-provided addresses (future RM-core tables) arrive as arguments.
+* **Entry symbol:** `ge_demo_service_dispatch` by default (override with
+ `--entry`/`entry_symbol=`).
+* **Optional metadata:** a `ge_service_t` block can be exported for future
+ loaders (`magic 0x47455356`, version 1, name, dispatch).
+
+## 3. ABI v1
+
+```c
+typedef ge_u64 (*ge_service_fn)(ge_u64 cmd, ge_u64 a0, ge_u64 a1);
+```
+
+* `a0`, `a1`, `a2` registers carry `(cmd, a0, a1)` on entry.
+* The return value is `a0`.
+* The service may use `a3..a7`, `t0..t6`, `s0..s11` and the stack; the
+ injector supplies a private stack below the scratch page.
+* Traps do not return: the call loop times out, records the stop, restores
+ registers and raises `RspTimeout`.
+* No calling convention for callbacks in v1; the loader registers a **single
+ dispatch entry** per slot.
+
+## 4. Demo registration model
+
+* The firmware exposes eight handler slots (`gsp_ctrl.handlers[0..7]`).
+* Mailbox commands **16..23** select slot `cmd-16`.
+* On dispatch the firmware calls
+ `handlers[slot](arg0, arg1, arg2)` — i.e. `cmd = arg0`, parameters
+ `arg1`, `arg2`.
+* Slots 0..5 are used by the built-in demo RPCs; **slot 6** is the SDK
+ default (`--slot`).
+
+### Control-block fields used (demo)
+
+| Field | Offset | Role |
+|---|---|---|
+| `mailbox` | `+0x20` | command (`16 + slot` selects an injected service) |
+| `arg0` | `+0x28` | service command |
+| `arg1` | `+0x30` | parameter 0 |
+| `arg2` | `+0x38` | parameter 1 |
+| `result` | `+0x40` | service return value |
+| `handlers` | `+0x150` | eight entry slots |
+
+Offsets are demo-firmware-specific; use `greeneye/inject.py` constants and the
+schema at `firmware/uproc_demo/gsp_ctrl.schema.json` rather than hardcoding.
+
+## 5. Lifecycle
+
+```
+compile (PIC) ──► verify relocations ──► load PT_LOADs at load_addr
+ │ │
+ │ ├─► register entry in slot
+ │ └─► record "service_injected"
+ ▼
+invoke_remote (direct call) invoke_via_firmware (mailbox dispatch)
+ └──────────────► both record call_enter/call_return events
+```
+
+* **Load address:** default `ram_base + 0x01000000`, auto-shrunk to fit if
+ the image is larger; explicit with `--load-addr`.
+* **Cleanup:** v1 does not unregister automatically. A service is removed by
+ zeroing its slot and (optionally) clearing the blob; sessions record the
+ injection so runs are reproducible from a known state.
+* **Multiple services:** one per slot; multi-entry services are one dispatch
+ with subcommands.
+
+## 6. Observability contract
+
+The injector records, in the session directory:
+* `service_injected` — path, name, load address, entry address, slot, size;
+* `mem_write` events for every segment written;
+* `call_enter` / `call_return` — address, args, return value;
+* hook/watch events from user instrumentation inside the service;
+* `qemu.log`-style blobs are not applicable (no probe here), but the TCG
+ trace can be enabled for full coverage.
+
+Comparison of injected-service runs uses the standard `greeneye compare`
+profiles, so a change in a service's behaviour is visible like any firmware
+change.
+
+## 7. Building (reference)
+
+```makefile
+CC := clang
+LLD ?= /usr/lib/llvm-18/bin/ld.lld
+CFLAGS := --target=riscv64-unknown-elf -march=rv64imac -mabi=lp64 \
+ -mcmodel=medany -mno-relax -fPIC -ffreestanding -nostdlib \
+ -fno-builtin -fno-stack-protector -fno-asynchronous-unwind-tables -O2
+$(LLD) -m elf64lriscv --no-relax -e ge_demo_service_dispatch -o service.elf service.o
+```
+
+The tracked SDK (`sdk/`) is the executable reference.
+
+## 8. Versioning and compatibility
+
+* ABI version 1 is the demo contract described here.
+* Future ABI v2 (RM-core shim) will pass a call table and a service context
+ instead of relying on firmware slots; blobs declare their ABI via the
+ optional metadata block so loaders can refuse mismatches.
+* Breaking ABI changes require a compatibility shim plus a note in the
+ session metadata and this document.
+
+## 9. Security and safety
+
+* Injected code runs with the simulator's full privileges; the bridge and
+ injector are local-only tools.
+* Never inject into a guest or a shipped environment; never use injection to
+ bypass a hardware protection.
+* Fuzzing/mutation harnesses (roadmap H4.3) must run against the simulator
+ or re-hosted code, with sessions attached as evidence.
+* Treat every injected service as untrusted code for the purposes of
+ watching: it may corrupt firmware state, so always record and restart clean
+ after crashes.
+
+## 10. Roadmap hooks
+
+* **H1.3** — RM service registration and RM-core call interception.
+* **H3** — native-simulator loader with snapshots and branch exploration.
+* **H4** — differential testing of injected services across firmware
+ versions and capability inference from their access patterns.
+* **H5** — services spanning GSP and adjacent microcontrollers.
diff --git a/debug/risc-v-gsp-rm-vm/docs/13-GHIDRA-CONTROL.md b/debug/risc-v-gsp-rm-vm/docs/13-GHIDRA-CONTROL.md
new file mode 100644
index 0000000..26e534d
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/13-GHIDRA-CONTROL.md
@@ -0,0 +1,154 @@
+# 13 — Ghidra control bridge
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+The bridge lets a GhidraScript drive a live green-eye session: execute the
+target until a point **chosen in the decompiler**, set/clear breakpoints, read
+memory and registers, make remote calls, and export hit lists back into the
+Ghidra project. Nothing about the firmware has to be rebuilt or re-hosted to
+answer "does execution actually reach this function?".
+
+```
+Ghidra (Java script) ── JSON lines over localhost ──► greeneye gdbridge ──► Session ──► QEMU
+ ▲ │
+ └──────── greeneye_hits.tsv ── MarkGreenEyeHits.java ◄──── recorded session ┘
+```
+
+## 1. Quickstart
+
+```bash
+# Terminal 1: a live target + bridge (records by default)
+./greeneye.sh ghidra --kernel firmware/uproc_demo/build/uproc_demo.elf --bridge-port 7000
+# prints: ghidra bridge on 127.0.0.1:7000
+
+# Terminal 2: Ghidra headless, run until the decompiler's address
+analyzeHeadless <proj> <name> -process <program> -noanalysis \
+ -scriptPath open-iov/debug/risc-v-gsp-rm-vm/ghidra \
+ -postScript GreenEyeControl.java 7000 0x800000ce
+```
+
+In the GUI, load `GreenEyeControl.java` from the Script Manager and supply the
+port in the script arguments; with no address argument it uses the current
+cursor (`currentAddress`).
+
+## 2. `GreenEyeControl.java` functions
+
+All functions exist on the script class and can be called from the script's
+own `run()` or from other scripts via the bridge `send()` method.
+
+| Function | Effect |
+|---|---|
+| `ping()` | `pong` if the bridge is alive |
+| `info()` | session name, pc, symbol count, recording path |
+| `resolve(name)` | symbol or `name+0xNN` → runtime address |
+| `setBreakpointAt(hexAddr)` / `setBreakpointHere()` | install a hook (prints on hit) |
+| `clearBreakpointAt(longAddr)` | remove breakpoints at an address |
+| `runUntil(hexAddr)` / `runUntilHere()` | resume and stop at the address; returns `{addr, symbol, kind}` |
+| `readMemoryHere(size)` | hex bytes at the cursor |
+| `readRegister(name)` / `writeRegister(name, value)` | register access |
+| `callFunction(hexAddr, args...)` | Frida-like remote call; returns `a0` |
+| `snapshotHere(size)` | record a memory snapshot in the session |
+| `listHits()` | breakpoint hit counts from the session |
+| `exportHits(path)` | write `greeneye_hits.tsv` for `MarkGreenEyeHits.java` |
+| `send(cmd, argsJson)` / `resultOf(response)` | raw bridge access for new workflows |
+
+Example additions inside `run()`:
+
+```java
+setBreakpointAt("0x800000ce");
+println(runUntil("0x800000ce")); // {addr:..., symbol:"rpc_sum", kind:"breakpoint"}
+println(readMemoryHere(16));
+println(callFunction("0x80000048", 0x1111L)); // rpc_ping -> 0xaffe
+println(exportHits("/tmp/greeneye_hits.tsv"));
+```
+
+## 3. Bridge protocol (specification)
+
+One JSON object per line; one response per request, same `id`.
+
+Request: `{"id": <int>, "cmd": "<name>", "args": {...}}`
+Response: `{"id": <int>, "ok": true, "result": ...}` or
+`{"id": <int>, "ok": false, "error": "..."}`.
+
+| `cmd` | args | result |
+|---|---|---|
+| `ping` | — | `"pong"` |
+| `info` | — | `{name, pc, symbols, recording}` |
+| `resolve` | `{name}` | address (int) |
+| `bp_set` | `{target, kind?}` | address |
+| `bp_clear` | `{addr}` | `true` |
+| `bp_list` | — | `[{addr, label, hits, kind}]` |
+| `watch_set` | `{target, size?, kind?}` | address |
+| `run_until` | `{target, timeout?}` | `{addr, symbol, kind}` |
+| `run` | `{duration?, max_stops?}` | stops |
+| `step` | `{count?}` | `[pc, ...]` |
+| `read_mem` | `{addr, size?}` | hex string |
+| `write_mem` | `{addr, hex}` | `true` |
+| `read_reg` | `{name}` | int |
+| `write_reg` | `{name, value}` | `true` |
+| `call` | `{target, args?, timeout?}` | return value |
+| `snapshot` | `{addr, size, label?}` | label |
+| `note` | `{text}` | `true` |
+| `hits` | — | `[{addr, hits, symbol}]` |
+| `export_hits` | `{path}` | absolute path |
+| `close` | — | `true` (closes the connection and stops the bridge) |
+
+`target`/`addr` accept `"0x..."`, a symbol name, or `name+0xNN`.
+
+## 4. Workflows
+
+### 4.1 Execute until here
+1. Start `greeneye ghidra` on the firmware image and load symbols
+ (`symbols export` from the Ghidra index) so hits are named.
+2. Put the cursor on a function in Ghidra and run `GreenEyeControl.java`.
+3. `runUntilHere()` sets a temporary breakpoint, services intervening stops
+ (including your hooks/watchpoints), and reports the symbol resolved at the
+ stop. If the function is never reached, the bridge returns a timeout error.
+
+### 4.2 Annotate the project with runtime evidence
+1. Run a session (any CLI/scripted path) and `exportHits("/tmp/hits.tsv")`.
+2. `MarkGreenEyeHits.java /tmp/hits.tsv` adds plate comments and bookmarks in
+ the Ghidra project.
+3. Optionally export a symbol DB from Ghidra (`ExportForGreenEye.java`) and
+ feed it back with `symbols export --index`.
+
+### 4.3 Remote-call driven experiments
+Use `callFunction()` to invoke firmware or injected-service functions from
+the decompiler (unit-test style), then inspect memory with
+`readMemoryHere()` and the recorded session.
+
+## 5. Address conventions
+
+Ghidra shows **program addresses** (e.g. libcuda file vaddr + image base).
+green-eye resolves **runtime addresses**. For firmware linked at its load
+address these coincide; otherwise export symbols with the right bias
+(docs/05-GHIDRA-BRIDGE.md) and prefer `resolve(name)` over hardcoded numbers.
+
+## 6. Limitations (v1)
+
+* Request/response only: the script pulls state; the bridge does not push
+ asynchronous hit events. Poll `hits`/`bp_list` between commands.
+* One command at a time per connection; multiple clients are accepted but are
+ not synchronised with each other.
+* `watch_set` exists on the bridge but has no dedicated Java wrapper yet —
+ call `send("watch_set", ...)` directly.
+* The bridge is unauthenticated **by design**: localhost only, never expose
+ it on a network interface.
+
+## 7. Troubleshooting
+
+| Symptom | Cause / fix |
+|---|---|
+| `Connection refused` | bridge not started, wrong port, or it exited (`close`) |
+| `green-eye bridge closed the connection` | the server was stopped or the target session closed |
+| timeout on `run_until` | address not executed; check symbol bias and that the target is not wedged |
+| empty `hits` | the session was started without recording; use the CLI (records by default) |
+| Java compile error on script load | Ghidra API drift; the script targets Ghidra 11/12 `GhidraScript` |
+
+## 8. Roadmap
+
+* Push notifications for breakpoint hits (server → Ghidra).
+* Decompiler annotations from live watchpoints (value changes per field).
+* A bridge client that lets multiple scripts share a session with a command
+ queue.
+* Direct MCP exposure of the same command set (roadmap R3).
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
new file mode 100644
index 0000000..d310f22
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md
@@ -0,0 +1,399 @@
+# 14 — Firmware adversarial interoperability research guide (green-eye + Ghidra)
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+## 1. Purpose and scope
+
+Open-IOV's hard problem is not writing the GSP emulation once; it is keeping
+`open-iov-vfio` accurate while the firmware changes underneath it. New GSP
+images, device pairings and architectures arrive stripped, signed and
+hardware-coupled, and some actively resist analysis. This guide is the
+practical companion to `GREEN-EYE-ROADMAP.md` H4: how to move from "the new
+image does something different" to a reviewed product change with evidence.
+
+Ghidra is the **static map**; green-eye is the **dynamic ground truth**;
+recorded sessions are the **shared evidence**. Recipes use only the v0.1
+surface, plus items explicitly labelled roadmap; see also docs/06-ACCURACY.md.
+
+| Capability used here | State |
+|---|---|
+| `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) |
+
+## 2. The adversary model (a moving target, not a malicious party)
+
+This guide assumes no threat actor. The "adversary" is **unplanned firmware
+change plus deliberate opacity**: new images on a hidden
+schedule; stripped symbols, signed images and non-recoverable boot states;
+anti-analysis behaviour (timing checks, self-consistency checks, hardware-
+dependent branches) and **decoys** (code or data not on the live path); and
+hardware coupling a simulator cannot yet exercise.
+
+1. Any single run is an anecdote. A conclusion needs at least an A/B pair.
+2. A static reading is a hypothesis. It becomes evidence only when execution
+ reaches it in a recorded session.
+3. Absence of a hit is a fact about *that* run at *that* fidelity (docs/06),
+ not proof of dead code.
+4. Every claim carries its fidelity level (L0–L3, docs/06) and provenance.
+
+## 3. The joint workflow: Ghidra, green-eye, sessions
+
+```
+ question > Ghidra (static): decompile, dispatcher tables, symbols
+ > green-eye (dynamic): same stimulus, hooks, watchpoints, trace
+ > sessions + hits TSV: greeneye compare -> triage -> evidence pack
+ > open-iov-vfio change (with evidence)
+```
+
+| Question | Ghidra answers | green-eye answers | Shared artifact |
+|---|---|---|---|
+| What is this code? | decompilation, call graph, xrefs | — | symbol DB (docs/05) |
+| Did it execute here? | hypothesis from xrefs | `bp_hit`/`watch_hit` events | `greeneye_hits.tsv` |
+| What changed between versions? | corpus/symbol diff | `compare` hit/watch/write deltas | symbol DB per version + report |
+| What can this service touch? | object references | watchpoint/TCG access map | capability table |
+
+References: docs/05-GHIDRA-BRIDGE.md, docs/13-GHIDRA-CONTROL.md, docs/07-PORTABILITY-PLAYBOOKS.md.
+
+## 4. Method catalogue
+
+Run all commands from `open-iov/debug/risc-v-gsp-rm-vm/`; angle-bracket
+placeholders are task-supplied (never hardcode private paths in docs).
+
+### 4.1 Semantic diff across firmware versions (H4.1)
+
+Goal: separate "the code changed" from "the behaviour moved" before the
+emulation is touched.
+
+1. Build a symbol DB per version. `ghidra/ExportForGreenEye.java` emits
+ `addr<TAB>name<TAB>size`, which `symbols export --index` accepts:
+
+ ```bash
+ ./greeneye.sh symbols export --index <new-export.tsv> --bias 0 -o symbols-new.json
+ ./greeneye.sh symbols export --index <old-export.tsv> --bias 0 -o symbols-old.json
+ ```
+
+2. Probe each image for first-touch structure changes (docs/07 P1.2):
+
+ ```bash
+ ./greeneye.sh probe <old.fwimage> --symbols symbols-old.json --record
+ ./greeneye.sh probe <new.fwimage> --symbols symbols-new.json --record
+ ```
+
+3. Run the **same stimulus** against the re-hosted build of each version. Put
+ it in a script so it is identical across runs:
+
+ ```python
+ # run_stimulus.py -- ./greeneye.sh run run_stimulus.py --kernel <rehosted.elf>
+ import greeneye as ge
+ s = ge.launch(kernel="<rehosted.elf>", record=True, name="stim")
+ ctrl = s.symbols.lookup("<control-symbol>") # demo: gsp_ctrl
+ s.watch(ctrl + 0x10, size=8, kind="w")
+ s.run(duration=0.5) # settle/self-test
+ s.memory.write_u64(ctrl + 0x20, 1) # mailbox PING
+ s.memory.write_u64(ctrl + 0x28, 0x1234)
+ s.run(duration=0.5)
+ s.note("stimulus: PING 0x1234")
+ s.close()
+ ```
+
+ Demo control-block offsets are demo-specific; use the schema
+ (`firmware/uproc_demo/gsp_ctrl.schema.json`) or SDK constants rather than
+ hardcoding them (docs/12 §4).
+
+4. Compare the sessions symbolically — breakpoint profile delta, watchpoint
+ delta and written-page delta:
+
+ ```bash
+ ./greeneye.sh compare dumps/<old-run> dumps/<new-run> --symbols
+ ```
+
+5. Triage with a decision table rather than intuition:
+
+ | Static code changed? | Hit/write profile changed? | Reading |
+ |---|---|---|
+ | no | no | unchanged on this stimulus |
+ | no | yes | behaviour moved: inputs, ordering, environment or state |
+ | yes | no | changed code off this stimulus path (or dead/decoy: §4.4) |
+ | yes | yes | new logic on a live path — prioritise |
+ | n/a | symbol never hit in either run | coverage gap: widen the stimulus first |
+
+6. Annotate the result back into Ghidra (`greeneye_hits.tsv` +
+ `MarkGreenEyeHits.java`). Behaviour that moved without a code change is
+ usually a state/ordering fix for the emulation, not new logic.
+
+### 4.2 Capability inference from watchpoints and TCG traces (H4.2)
+
+Goal: derive what a service/engine actually reads and writes, then compare it
+with the capability model the emulation assumes.
+
+1. Instrument the object ranges the service owns (kinds `w`, `r`, `a`):
+
+ ```python
+ # capability_probe.py -- ./greeneye.sh run capability_probe.py --kernel <elf>
+ import greeneye as ge
+ s = ge.launch(kernel="<rehosted.elf>", record=True, name="caps")
+ base = s.symbols.lookup("<object-symbol>")
+ for off in (0x0, 0x10, 0x20, 0x28, 0x38, 0x40):
+ for kind in ("r", "w"):
+ s.watch(base + off, size=8, kind=kind,
+ callback=lambda h, k=kind: s.note("cap-" + k, pc=h.pc))
+ s.run(duration=1.0)
+ s.close()
+ ```
+
+2. For access coverage **between stops**, enable the TCG plugin with a PC
+ filter for the service's load range:
+
+ ```bash
+ make -C qemu
+ qemu-system-riscv64 -machine virt -m 256M -display none \
+ -serial file:/tmp/uart.log -bios none -kernel <rehosted.elf> -S \
+ -gdb tcp:127.0.0.1:4444 \
+ -plugin qemu/greeneye-trace.so,path=/tmp/ge-trace.jsonl,start=0x80000000,end=0x81000000,exec=on
+ ./greeneye.sh attach --port 4444 --kernel <rehosted.elf> --plugin <capability-plugin.py>
+ ```
+
+ `start=`/`end=` bound the trace, `exec=on` adds per-instruction records and
+ `path=` selects the JSONL output (`qemu/greeneye_trace.c`; qemu/README.md).
+ Folding that JSONL into sessions is roadmap R4 — today it is a separate
+ evidence blob.
+
+3. Reduce to a capability table and compare with the emulation's model:
+
+ | Observed access | Address/range | Kind | PC/symbol | Emulated capability | Finding |
+ |---|---|---|---|---|---|
+ | write | `0x…` | w | `func+0x…` | not modelled | least-privilege candidate |
+ | read | `0x…` | r | `func+0x…` | denied in emulation | potential fail-closed gap |
+
+ Least-privilege readings matter for `open-iov-vfio`, but a simulator
+ observation is evidence about the firmware (docs/06 §6); confirm before it
+ changes the product.
+
+### 4.3 Mutation of construction/control inputs (H4.3)
+
+Goal: drive error paths and asserts deliberately, capture them with symbols,
+and build a corpus for the emulation's fail-closed rules. The full harness is
+roadmap R5; until then, bounded mutation over existing primitives is honest.
+
+1. Mutate single fields deterministically and record each attempt:
+
+ ```python
+ # mutate.py -- ./greeneye.sh run mutate.py --kernel <rehosted.elf>
+ import greeneye as ge
+ s = ge.launch(kernel="<rehosted.elf>", record=True, name="mutate")
+ ctrl = s.symbols.lookup("<control-symbol>")
+ for value in (0, 1, 0xFFFFFFFFFFFFFFFF, 0x8000000000000000):
+ s.memory.write_u64(ctrl + 0x20, value)
+ s.run(duration=0.2, max_stops=200)
+ s.note("mutation", field="mailbox", value=value)
+ s.close()
+ ```
+
+2. Mutate injected-service calls on both paths (direct and mailbox):
+
+ ```python
+ from greeneye.inject import load_service, invoke_remote, invoke_via_firmware
+ svc = load_service(s, "sdk/build/injected_demo.elf", slot=6)
+ invoke_remote(s, svc, cmd=0xFFFF, a0=0, a1=0) # malformed cmd
+ s.run(duration=0.3)
+ invoke_via_firmware(s, svc, cmd=0xFFFF, a0=0, a1=0) # malformed mailbox
+ ```
+
+3. Capture error paths with hooks/watchpoints on the dispatcher, fault
+ handlers and assert sites (symbol DB); `./greeneye.sh demo` records a trap.
+4. Compare clean vs mutated sessions, keeping the mutation value in a `note`
+ so the pack is self-describing:
+
+ ```bash
+ ./greeneye.sh compare dumps/<clean-run> dumps/<mutated-run> --symbols
+ ```
+
+ Every corpus entry needs the firmware hash; never run mutation against
+ hardware (docs/12 §9).
+
+### 4.4 Decoy and anti-analysis handling (H4.4)
+
+Goal: distinguish signal from noise when the image branches on environment,
+timing or integrity state, or contains bogus code paths.
+
+1. **Deterministic replay first.** Run the identical stimulus twice and
+ require the comparison to be empty:
+
+ ```bash
+ ./greeneye.sh compare dumps/<run1> dumps/<run2> --symbols
+ ```
+
+ Any delta is a non-determinism candidate (scheduler, uninitialised read, time); record it.
+
+2. **Two-run differential.** Flip one input bit; attribute every delta to it;
+ unrelated movement suggests a time/integrity check:
+
+ ```python
+ s.memory.write_u64(ctrl + 0x28, original ^ (1 << 0)) # one bit
+ ```
+
+3. **Coverage-based dead-code detection.** Use the TCG trace with `exec=on`
+ (or a bounded `s.step(n)` pass) to build a PC set, then annotate it into
+ Ghidra:
+
+ ```python
+ from greeneye.ghidra import export_hits
+ from greeneye.symbols import SymbolDB
+ db = SymbolDB(); db.load_json("<symbols.json>")
+ export_hits("dumps/<run>", "greeneye_hits.tsv", symbols=db)
+ ```
+ ```bash
+ $GHIDRA/support/analyzeHeadless <proj> <name> -process <program> -noanalysis \
+ -scriptPath debug/risc-v-gsp-rm-vm/ghidra \
+ -postScript MarkGreenEyeHits.java /abs/path/greeneye_hits.tsv
+ ```
+
+ Zero hits across every stimulus makes a function a **decoy candidate**, not proof of dead code (docs/06).
+
+4. **Trap-on-register-read experiments.** Arm a read watchpoint on a register
+ the firmware might be probing, and record which PC reads it:
+
+ ```python
+ s.watch("<reg-or-mmio-symbol>", size=8, kind="r",
+ callback=lambda hit: s.note("anti-analysis-read", pc=hit.pc))
+ ```
+
+ On today's `qemu virt` an unmodelled GPU register faults rather than
+ returning a value (`greeneye probe` reports that first read, docs/06 §3);
+ the value-return experiment waits on the H2 device model — log, don't claim.
+5. Maintain the catalogue of observed techniques in
+ docs/09-GSP-FIRMWARE-NOTES.md (roadmap H4.4), one session pair per technique.
+
+### 4.5 Differential execution: reference vs simulator vs injected service (H4.5)
+
+Goal: measure, not narrate. The contract is the M1–M6 table from docs/06:
+
+| # | Metric | How it is measured here |
+|---|---|---|
+| M1 | Inventory coverage | observed controls/RPCs vs emulated handled set |
+| M2 | Reply agreement | field-by-field reply diff (canonical size, consumed fields) |
+| M3 | Ordering agreement | sequence alignment of the control/event stream (LCS/edit distance) |
+| M4 | State agreement | firmware state transitions vs emulation state model |
+| M5 | Error-path agreement | injected faults take the same status/notifier path |
+| M6 | Memory side-effects | read/write map vs emulation shadow state; snapshot hashes |
+
+Procedure: (1) capture the reference on hardware/VFIO (docs/07 P4); (2) capture
+the simulator side with `record`/scripted `run`, hooks, watchpoints and
+snapshots; (3) `./greeneye.sh compare dumps/<reference-dir> dumps/<greeneye-dir>
+--symbols`, with a script or plugin for per-control M2/M3 payload events;
+(4) repeat with the injected-service path to test the service boundary;
+(5) escalate — divergence that would change emulation behaviour must be
+reproduced physically; simulator-only divergence becomes a fidelity gap.
+
+### 4.6 From observation to reimplementation
+
+The canonical accuracy loop (roadmap H4.5): (1) observe on hardware/VFIO;
+(2) observe in green-eye (replayable session); (3) `compare` + M1–M6;
+(4) implement in `open-iov-vfio` with the session directories attached as
+evidence; (5) re-run the same stimulus and require the metric table to improve
+or stay equal.
+
+Practical rules: one session pair per behaviour change; the PR links the
+`compare` report and session hashes plus the symbol DB diff if the static map
+changed; decoy or low-fidelity observations never justify a product change.
+
+## 5. Ghidra integration specifics
+
+Start the live bridge and drive it from the decompiler:
+
+```bash
+./greeneye.sh ghidra --kernel <rehosted.elf> --bridge-port 7000
+$GHIDRA/support/analyzeHeadless <proj> <name> -process <program> -noanalysis \
+ -scriptPath debug/risc-v-gsp-rm-vm/ghidra \
+ -postScript GreenEyeControl.java 7000 0x<decompiler-address>
+```
+
+`GreenEyeControl.java` operations (docs/13): `setBreakpointAt/Here`,
+`runUntil/Here`, `readMemoryHere`, `readRegister`/`writeRegister`,
+`callFunction`, `snapshotHere`, `listHits`, `exportHits`. Watchpoints exist on
+the bridge (`watch_set`) without a Java wrapper: `send("watch_set", {...})`.
+
+Address conventions and bias pitfalls (docs/05 §1):
+
+* Ghidra shows **program addresses**; green-eye resolves **runtime addresses**.
+ For firmware linked at its load address they coincide; otherwise supply
+ `--bias` when building the symbol DB (`ExportForGreenEye.java` emits program
+ addresses).
+* The classic mistake is passing a program address to `hook()` when the image
+ base is non-zero; prefer `s.hook("<name>")` and pass raw numbers through
+ `resolve`. `decompiled_index.tsv` carries both spaces; green-eye keeps the
+ program address in `symbol.extras["program_addr"]` for `bucket:line` mapping.
+* Wrapper-generated functions are re-laid-out between versions: transfer
+ names, never raw wrapper addresses (docs/05 §6).
+* Bridge `run_until` timeouts mean "not executed under this stimulus at this
+ fidelity"; they do not prove unreachability.
+
+## 6. Metrics, evidence pack and report template
+
+A finding is only a finding when the pack is complete. The evidence pack for
+one adversarial-navigation task contains:
+
+* [ ] firmware identity (version, hash, pairing) and symbol DB(s) with provenance and hash;
+* [ ] baseline/candidate session dirs (or sha256 inventory) and the exact stimulus;
+* [ ] `compare` report and relevant `analyze` reports;
+* [ ] M1–M6 table with the reference-capture method and explicit L2/L3 limits;
+* [ ] `greeneye_hits.tsv` plus any Ghidra annotations produced;
+* [ ] unexplained-delta list (empty is a claim, not a default).
+
+Short report template:
+
+```markdown
+# Firmware delta: <old> -> <new> (<device>)
+* firmware: <old sha256> -> <new sha256>; symbol DB: <old hash> -> <new hash>
+ (provenance: <export>, bias <n>)
+* sessions: <baseline dir>, <candidate dir> (greeneye-session/1); stimulus:
+ <script/spec + mutation values>
+
+## Observed delta
+* changed on live path: <symbols>; behaviour moved without code change:
+ <symbols, evidence>; coverage gaps/decoys: <symbols, why>
+
+## Metrics (docs/06)
+| M1 | M2 | M3 | M4 | M5 | M6 |
+|----|----|----|----|----|----|
+
+## Fidelity statement
+L0/L1 in simulator; L2 not claimed; L3 absent for <registers>.
+Physical reproduction required before product change: yes/no, why.
+
+## Product action
+open-iov-vfio change (or explicit "no change"): <link/notes>
+```
+
+## 7. Boundaries, safety and ethics
+
+* **Development-only tooling.** Never shipped, never a dependency of the emulation, never in a guest (docs/00).
+* **No guest modification.** Instrument the firmware under study in the
+ simulator; do not patch or observe a guest driver (project rule; V4).
+* **No confidentiality claims.** The simulator is a study environment, never a
+ security or confidentiality boundary (docs/06, V4/H6).
+* **Clean-room discipline.** Do not copy vendor code into product code; use
+ the study material to understand behaviour, then write the emulation from
+ observation. Tracked docs cite behaviour generically and never name source
+ locations.
+* **Fuzzing is gated.** Mutation runs only against the simulator or re-hosted
+ code, behind explicit harnesses, with session evidence (docs/12 §9, R5).
+ Never use injection to defeat a hardware security boundary (docs/12 §1).
+* **Escalate to physical.** Any conclusion that would change `open-iov-vfio`
+ must be confirmed against a physical capture (docs/06 §6, docs/07 P4.5).
+
+## 8. Roadmap hooks
+
+| Method | Roadmap item | What it adds | Gate |
+|---|---|---|---|
+| 4.1 semantic diff | H4.1 | versioned symbol corpora, automated delta report | H1+H2 |
+| 4.2 capability inference | H4.2 | access map vs capability model, least-privilege findings | watchpoints (done); TCG-session fold (R4) |
+| 4.3 mutation/fuzzing | H4.3, R5 | AFL++ class harness, corpus of error paths | R1/R2 |
+| 4.4 decoy handling | H4.4 | technique catalogue in docs/09, deterministic replay | H2 for register-value experiments |
+| 4.5 differential exec | H4.5, docs/06 | scripted consensus harness, M1–M6 automation | reference capture pipeline |
+| 4.6 reimplementation | H4.5, V1 | golden sessions, CI comparison | H1 stable |
+
+Program-level success criterion served here: a new GSP version can be triaged
+(inventory + behaviour diff + metric table) in **hours**, with evidence.
diff --git a/debug/risc-v-gsp-rm-vm/docs/15-AGENTIC-GSP-IDE.md b/debug/risc-v-gsp-rm-vm/docs/15-AGENTIC-GSP-IDE.md
new file mode 100644
index 0000000..79d17b8
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/15-AGENTIC-GSP-IDE.md
@@ -0,0 +1,387 @@
+# 15 — green-eye as an agentic integrated development, analysis and debugging environment for the GPU System Processor
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+## 1. Vision and what "agentic" means here
+
+green-eye already has the two properties that make an agent loop possible:
+a **deterministic, scriptable harness** (`ge.launch`/`ge.attach`, `Session`,
+`record`, `compare`) and a **durable evidence format** (the session directory).
+"Agentic" here means:
+
+* AI agents drive the deterministic harness — they choose instrumentation,
+ run stimuli, compare sessions and write findings;
+* sessions are the shared state between agents, humans and Ghidra, so every
+ claim is inspectable after the fact;
+* **humans keep product-change authority**: an agent may propose an
+ `open-iov-vfio` change with evidence, but only a human merges it, and the
+ physical-confirmation rules of docs/06 and docs/07 still apply.
+
+Non-goals: autonomous hardware cycles, unattended reboots, agents that "fix"
+the emulation directly, or replacing the engineer reading the decompiler.
+This document describes the environment that exists today plus the roadmap
+hooks that would make a full agent platform real.
+
+## 2. Architecture
+
+```
+ +-------------------- agents (triage / hypothesis / RE / portability) ----+
+ | |
+ | CLI: ./greeneye.sh info|demo|run|attach|launch|probe|record|analyze| |
+ | compare|symbols|plugins|inject|ghidra |
+ | Python: ge.launch/attach, Session.*, memory.*, symbols.*, structs, |
+ | probes, plugins, ghidra.export_hits, inject.* |
+ | REPL: bp/hook/watch/call/x/snap/events/note |
+ | Ghidra bridge: JSON lines over localhost (docs/13) |
+ | MCP: planned plugin only (roadmap R3) — does not exist in v0.1 |
+ +----------------------------------+---------------------------------------+
+ v
+ session directory (events.jsonl + session.json + snapshots + summary.md)
+```
+
+### 2.1 The session directory is the shared state
+
+| Artifact | Content | Consumers |
+|---|---|---|
+| `events.jsonl` | append-only event stream (`bp_hit`, `watch_hit`, `mem_write`, `call_enter/return`, `note`, `service_injected`, …) | `analyze`, `compare`, `export_hits`, plugins |
+| `session.json` | format, name, counts, snapshot inventory, meta | report tooling, provenance |
+| `snapshots/*.bin.z` | zlib-compressed region captures with sha256 | offline diff, field decode |
+| `blobs/*` | plugin/probe dumps (e.g. qemu.log) | forensics |
+| `summary.md` | rendered report | humans, PR attachments |
+
+`greeneye compare dumps/<a> dumps/<b> --symbols` is the primary agent
+primitive: it turns two sessions into hit/watch/write deltas. Format
+`greeneye-session/1` evolves additively (roadmap V2).
+
+### 2.2 Surfaces an agent may use
+
+* `./greeneye.sh …` — the stable CLI (all real subcommands listed above).
+* `./greeneye.sh run script.py` — the preloaded Python namespace (`ge`,
+ `session`/`s`, `memory`, `symbols`, `recorder`) makes a harness file the
+ unit of automation.
+* `./greeneye.sh ghidra` + `GreenEyeControl.java` — JSON-lines bridge for the
+ decompiler loop (`run_until`, `bp_set`, `read_mem`, `call`, `hits`,
+ `export_hits`; protocol in docs/13 §3).
+* Plugins — the extension point (docs/04). A plugin's `register(ge)` can add
+ instrumentation, CLI commands (`ge.add_command`) and recorded events; plugin
+ output lands in the same `events.jsonl`.
+* **MCP is planned, not present.** Roadmap R3/H7 reserves an MCP plugin that
+ would expose sessions as local stdio tools. Until that rung is scheduled, do
+ not add MCP dependencies to the core package (docs/04 §3d).
+
+### 2.3 Why the evidence model matters for agents
+
+A human can hold context across steps; an agent cannot. The session directory
+makes the context external: an agent can be restarted, handed a directory, and
+re-derive the same conclusion without replaying its own reasoning. This is the
+difference between an agent that reports opinions and one that produces
+reviewable evidence.
+
+## 3. Agent roles and workflows
+
+| Role | Input | Tools | Output | Stop condition |
+|---|---|---|---|---|
+| Triage | question + firmware version(s) | `symbols export`, `probe`, `analyze` | delta candidates, prioritised symbol list | every changed symbol classified |
+| Hypothesis | one behaviour question | `run` scripts, hooks, watchpoints, `compare` | session pair + verdict | delta attributed to one variable |
+| RE | decompiled function | `greeneye ghidra`, `run_until`, breakpoints, `export_hits` | hits TSV + annotation, reachability answer | function hit or timeout recorded |
+| Portability | version/device pair | docs/07 playbook, baseline + candidate sessions, M1–M6 | portability report | metric table complete |
+| Injection | service idea | `sdk/`, `inject`, `load_service`, `invoke_remote`, `invoke_via_firmware` | injected-service session pair | direct and firmware paths agree |
+| Verification | another agent's finding | golden sessions, `compare`, `analyze` | pass/fail + counterexample | finding reproduced or refuted |
+
+Role separation is deliberate: the agent that made a claim should not be the
+only one that checks it. Verification requires re-running the original
+stimulus, not re-reading the original report.
+
+## 4. Runbooks
+
+Each runbook is written so a human or an agent can execute it start to finish,
+and each ends with the evidence pack from docs/14 §6.
+
+### 4.1 Answer an emulation reply question
+
+Question template: "For control `<X>`, does the firmware set field `<Y>` of
+the reply/object, and in what order relative to `<Z>`?"
+
+1. Frame it as A/B: the emulation's reply (reference capture, docs/07 P4) and
+ the firmware's reply in green-eye. One session alone cannot answer it.
+2. Resolve the handler and object from the symbol DB; never hardcode:
+
+ ```bash
+ ./greeneye.sh symbols resolve --db symbols-<ver>.json <handler-or-addr>
+ ```
+
+3. Capture the firmware side with a hook + before/after snapshots:
+
+ ```python
+ # reply_q.py -- ./greeneye.sh run reply_q.py --kernel <rehosted.elf>
+ import greeneye as ge
+ from greeneye import Memory
+
+ s = ge.launch(kernel="<rehosted.elf>", record=True, name="reply-q")
+ try:
+ ctrl = s.symbols.lookup("<control-symbol>") # demo: gsp_ctrl
+ s.hook("<handler-symbol>",
+ on_enter=lambda h: s.note("enter", a0=h.arg(0)),
+ on_leave=lambda h: s.note("leave", retval=h.retval))
+ before = s.snapshot(ctrl, 0x100, "before")
+ s.memory.write_u64(ctrl + 0x20, 1) # stimulus
+ s.run(duration=0.5)
+ after = s.snapshot(ctrl, 0x100, "after")
+ print(Memory.render_diff(Memory.diff(before, after, base=ctrl), s.symbols))
+ finally:
+ s.close()
+ ```
+
+4. Decode the captured bytes with the structure schema so the answer is
+ field-by-field, not byte noise:
+
+ ```python
+ import json
+ from greeneye.structs import schema_from_json, decode, render
+ 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))
+ ```
+
+5. Compare against the reference capture with
+ `greeneye.analyze.compare_sessions(...)` / `./greeneye.sh compare …` for
+ ordering (M3) and state the field diff in words (M2). If the reply is in a
+ different object, repeat step 3 on that object's address.
+6. Report: question, session pair, `compare` output, M2/M3 rows, fidelity
+ statement, explicit "cannot answer because L3 absent" where applicable.
+
+### 4.2 Find why a handler disappeared
+
+1. Build/refresh the static inventory for both firmware versions:
+
+ ```bash
+ $GHIDRA/support/analyzeHeadless <proj> <name> -process <program> -noanalysis \
+ -scriptPath debug/risc-v-gsp-rm-vm/ghidra \
+ -postScript ExportForGreenEye.java /tmp/symbols-<ver>.tsv
+ ./greeneye.sh symbols export --index /tmp/symbols-<ver>.tsv --bias 0 -o symbols-<ver>.json
+ diff <(cut -f2 /tmp/symbols-old.tsv | sort) <(cut -f2 /tmp/symbols-new.tsv | sort)
+ ```
+
+2. Probe the new image for early structure changes:
+
+ ```bash
+ ./greeneye.sh probe <new.fwimage> --symbols symbols-<ver>.json --record
+ ```
+
+3. Record the same stimulus against each version. Hook the old handler name;
+ a missing symbol is itself evidence — catch `SymbolNotFound` and log it
+ rather than aborting the run. Also hook the dispatcher and the old
+ handler's neighbours from the inventory diff.
+4. Compare the sessions:
+
+ ```bash
+ ./greeneye.sh compare dumps/<old-run> dumps/<new-run> --symbols
+ ./greeneye.sh analyze dumps/<new-run> --symbols symbols-<ver>.json --out new.md
+ ```
+
+5. Classify the disappearance with the table below, then confirm in Ghidra:
+
+ | Observation | Likely cause | Next step |
+ |---|---|---|
+ | name absent from new symbol DB | removed or renamed before build | match by size/callgraph in Ghidra |
+ | old function exists, dispatcher hits moved to another symbol | merged or re-routed | `run_until` the new dispatcher entry, `bp_set` there |
+ | old function exists, zero hits under the standard stimulus | coverage gap or decoy | widen stimulus, check xrefs from the dispatch table |
+ | dispatch table entry changed but code identical | behaviour moved (state/input) | inspect the table with bridge `read_mem`/snapshot |
+
+6. With the bridge running (`./greeneye.sh ghidra --kernel <elf>`), execute
+ until the dispatch site from the decompiler and set breakpoints:
+
+ ```bash
+ $GHIDRA/support/analyzeHeadless <proj> <name> -process <program> -noanalysis \
+ -scriptPath debug/risc-v-gsp-rm-vm/ghidra \
+ -postScript GreenEyeControl.java 7000 0x<dispatch-address>
+ ```
+
+7. Export hits and annotate the old function's fate:
+
+ ```bash
+ ./greeneye.sh analyze dumps/<new-run> --symbols symbols-<ver>.json --out new.md
+ python3 - <<'PY'
+ from greeneye.ghidra import export_hits
+ from greeneye.symbols import SymbolDB
+ db = SymbolDB(); db.load_json("symbols-<ver>.json")
+ print(export_hits("dumps/<new-run>", "greeneye_hits.tsv", symbols=db))
+ PY
+ ```
+
+8. Report: static delta, dynamic delta, classification, and whether the
+ emulation must change (with the docs/14 evidence pack).
+
+### 4.3 Prototype a service
+
+1. Build the SDK reference service and prove position independence:
+
+ ```bash
+ make -C sdk
+ make -C sdk relocs # must print "no relocations in this file"
+ ```
+
+2. Inject and invoke through both paths (direct debugger call, then firmware
+ mailbox dispatch):
+
+ ```bash
+ ./greeneye.sh inject --service sdk/build/injected_demo.elf \
+ --cmd 1 --arg0 0x41 --arg1 0x42
+ ```
+
+3. Iterate programmatically when the CLI shape is not enough:
+
+ ```python
+ from greeneye.inject import load_service, invoke_remote, invoke_via_firmware
+ svc = load_service(s, "sdk/build/injected_demo.elf", slot=6)
+ print(hex(invoke_remote(s, svc, 2, 0x10, 0x20)))
+ s.run(duration=0.3)
+ print(hex(invoke_via_firmware(s, svc, 2, 0x10, 0x20)))
+ ```
+
+4. Instrument the service like firmware: hooks on the entry, watchpoints on
+ its data, snapshots before/after each command (docs/11 §7).
+5. Compare two versions of the service to see the behavioural delta:
+
+ ```bash
+ ./greeneye.sh compare dumps/<svc-v1> dumps/<svc-v2> --symbols
+ ```
+
+6. Stop condition: direct and firmware-dispatched results agree, the session
+ is recorded, and the prototype is documented as development-only. Product
+ code goes to `open-iov-vfio` only through the accuracy loop (docs/11 §8,
+ docs/12 §9); GSP-RM registration is roadmap H1.3.
+
+### 4.4 Prepare a portability report
+
+1. Follow the P1 loop in docs/07-PORTABILITY-PLAYBOOKS.md with real commands:
+
+ ```bash
+ ./greeneye.sh probe <old.fwimage> --symbols symbols-old.json --record
+ ./greeneye.sh probe <new.fwimage> --symbols symbols-new.json --record
+ ./greeneye.sh record --kernel <rehosted-old.elf> --duration 2.0 \
+ --watch gsp_ctrl+0x10:8 --snapshot gsp_ctrl:392
+ ./greeneye.sh record --kernel <rehosted-new.elf> --duration 2.0 \
+ --watch gsp_ctrl+0x10:8 --snapshot gsp_ctrl:392
+ ./greeneye.sh compare dumps/<old-run> dumps/<new-run> --symbols
+ ```
+
+2. Keep the stimulus identical across versions: use one script/probe spec and
+ record it in the pack (docs/14 §4.1).
+3. Fill the M1–M6 table from docs/06-ACCURACY.md. M7 (timing) is never
+ claimed; L2/L3 statements are mandatory.
+4. Produce the report with the docs/14 §6 template and attach session hashes,
+ symbol DB hashes and the Ghidra export provenance.
+5. Verification agent step: replay the candidate stimulus against the checked-in
+ golden session (roadmap V1) and require `compare` to match expectations.
+
+## 5. Guardrails
+
+Hard rules (no agent may override them):
+
+* **Never claim cycles, time or reboots from the simulator.** L2 is not
+ modelled; L3 is absent until H2 (docs/06).
+* **No guest modifications** and no instrumentation of a guest driver; the
+ tool is never shipped and never present in a guest (docs/00, V4).
+* **Sessions are mandatory evidence.** A finding without a session directory
+ and a `compare` report is not a finding (docs/11 §8).
+* **No unattended hardware.** The agent runs against the simulator by default;
+ physical GPU cycles are supervised and human-driven (H7).
+* **Human approval for product changes.** Agents produce proposals and
+ evidence packs; only humans merge `open-iov-vfio` changes.
+* **Budgets everywhere.** Every run uses `duration=`/`max_stops=`/`timeout=`
+ and a wall-clock cap; a wedged target leaves the session stopped and is
+ restarted clean rather than poked.
+
+Capability manifest for actions (proposed file for the harness; see §7):
+
+| Class | Examples | Who runs it | Preconditions |
+|---|---|---|---|
+| Read | `info`, `plugins`, `symbols resolve`, `analyze`, `compare` | agent, unattended | path/DB provenance recorded |
+| Observe | `probe`, `record`, `demo`, hooks/watch/step | agent, unattended on simulator | timeout, session recording on |
+| Write (target) | `memory.write*`, `call`, `run_until`, `inject`, mailbox stimulus | agent with manifest | declared address/range, fresh session, before/after snapshots, mutation values noted |
+| Product | `open-iov-vfio` patch | human only | M1–M6 + physical confirmation rules (docs/07 P4.5) |
+| Hardware | physical cycles/reboots | human only | supervised, never unattended |
+
+A write action's manifest entry names the session, the target range, the
+purpose and the budget; the session records the action, so the manifest and
+the evidence can be cross-checked.
+
+## 6. Evaluation
+
+An agent workflow is judged on the same axis as the human one: how long until
+a reviewable answer, and does the answer survive verification.
+
+| Metric | Definition | Instrument |
+|---|---|---|
+| TTFA | question → first evidence-backed answer | wall clock; session count |
+| TTEP | question → complete evidence pack (docs/14 §6) | pack checklist |
+| Evidence yield | sessions/`compare` reports per accepted finding | `dumps/` inventory |
+| Verification pass rate | findings reproduced by the verification role | replay runs |
+| Coverage | inventory observed vs emulated (M1) | docs/06 table |
+| Accuracy trend | M1–M6 per release | docs/06 tables |
+| Regression lead time | firmware drop → delta report | probe/compare timestamps |
+| Human minutes | manual minutes per accepted finding | benchmark log |
+
+Benchmark protocol: take five questions from recent history, run each twice —
+once with the agent (all actions recorded) and once manually — on the same
+machine and firmware set, then compare TTFA/TTEP/evidence yield. Re-run the
+manual side offline against the agent's sessions to separate speed from
+correctness. Publish the ratios with the raw session directories, not just
+the numbers (the project's "never a single run" rule applies to tooling too).
+
+Golden data is the regression bed: the demo session and re-hosted workloads
+are checked in CI (roadmap V1), and every new stimulus script should grow a
+golden expectation once it is stable.
+
+## 7. Roadmap hooks (H7) and what would make this real
+
+| H7 item | What exists today | What is missing |
+|---|---|---|
+| MCP plugin (R3) | JSON-lines Ghidra bridge; sessions as data | MCP server plugin (`plugins/mcp_server.py` when built) |
+| deterministic harnesses | `run` scripts, probes, plugins, `record`/`compare` | named harness entry points, budgets, manifests |
+| sessions as evidence | `greeneye-session/1`, `summary.md`, `compare` | session index, signed/hashed pack tooling |
+| autonomous loops | hooks/watchpoints/compare primitives | hypothesis/verification controllers, stop conditions |
+| safety | local-only bridge, no network, no hardware | enforced capability manifest, process isolation |
+
+What would make this real:
+
+1. **MCP tool schema** (sketch; planned only, no code in v0.1):
+
+ ```json
+ {
+ "transport": "local stdio; no network exposure by default",
+ "tools": [
+ {"name": "list_sessions", "args": {}},
+ {"name": "open_session", "args": {"dir": "string"}},
+ {"name": "symbols_resolve", "args": {"session": "string", "name": "string"}},
+ {"name": "memory_read", "args": {"session": "string", "addr": "string", "size": "int"}},
+ {"name": "memory_hexdump", "args": {"session": "string", "addr": "string", "size": "int"}},
+ {"name": "set_breakpoint", "args": {"session": "string", "target": "string", "kind": "sw|hw"}},
+ {"name": "watch", "args": {"session": "string", "target": "string", "size": "int", "kind": "w|r|a"}},
+ {"name": "run", "args": {"session": "string", "duration": "float", "max_stops": "int"}},
+ {"name": "run_until", "args": {"session": "string", "target": "string", "timeout": "float"}},
+ {"name": "snapshot", "args": {"session": "string", "addr": "string", "size": "int", "label": "string"}},
+ {"name": "compare_sessions", "args": {"a": "string", "b": "string", "symbols": "bool"}},
+ {"name": "export_hits", "args": {"session": "string", "out": "string"}}
+ ]
+ }
+ ```
+
+ Baseline names follow docs/08 R3; `watch` and `run_until` follow the
+ bridge command set and H7.
+
+2. **Harness scripts** with fixed names and budgets: `stimulus.py` (one
+ workload), `probe_matrix.py` (version × device), `golden_check.py`
+ (compare against expected profiles). Today these are written ad hoc.
+3. **Golden data**: checked-in expectation files for the demo and at least one
+ re-hosted workload, wired into `make test` (roadmap V1).
+4. **Capability manifest** file the harness reads before any write action
+ (§5 table), with the session recording as the audit trail.
+5. **Session index** for agents: a small JSONL listing session dirs, questions
+ and outcomes, so triage starts from history instead of from zero.
+6. **Physical-escalation gate**: a write-up checklist that must be complete
+ before an agent-proposed change reaches a supervised hardware cycle.
+
+Until R3 lands, the agent loop is: CLI/scripts + Ghidra bridge + sessions —
+nothing more, and the documentation should keep saying so.
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
new file mode 100644
index 0000000..2b77ca3
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md
@@ -0,0 +1,449 @@
+# 16 — Custom simulator: design and implementation plan
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status; this is a
+> far-later design, do not implement ahead of the H3 gate.
+
+## 1. Purpose and status
+
+This document is the design and delivery plan for a green-eye-native execution
+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.
+
+## 2. Why a custom simulator
+
+QEMU is an excellent execution engine and a poor *workflow* engine here. The
+limits below are consequences of QEMU being a whole-board emulator with a GDB
+stub as its scriptable surface; each maps to a §H3.1 requirement.
+
+| QEMU property | Consequence for green-eye | Requirement it forces |
+|---|---|---|
+| RSP serialises every access (hex frame, TCP round-trip, `PacketSize` chunking) | register/memory polling and watchpoint step-over are transport-bound; full-access tracing is impractical | in-process backend ABI with direct memory/register calls |
+| No cheap machine snapshots; `savevm` is board-level and not exposed over the stub | region snapshots are copied over RSP; no instant checkpoint exists | copy-on-write full-machine snapshot/restore |
+| No in-process plugin ABI: TCG plugins are C objects against QEMU internals; `qRcmd` is stringly | per-access callback *objects*, Python-side devices and deterministic fault injection have no home | native plugin/callback ABI for regions and devices |
+| Whole-board device model (`virt` models boards, not a TU104-class firmware environment) | device behaviour lives in QEMU code and cannot be versioned as data with a firmware profile | memory-map DSL + small device modules, versioned as profiles |
+| Host clock and iothread drive timers and wakeups | two runs of the same firmware interleave differently; replay is not reproducible | deterministic scheduler, virtual time, event-log replay |
+| GDB stub stop model | instructions between stops are invisible unless a trace plugin is built and loaded | event bus emitting `exec`/`access` natively |
+| Injection is debugger writes plus `Session.call` | RM-core call interception (stub/log/forward) is impossible at the ABI boundary | injection ABI with service registry and interceptors |
+
+The §H3.1 contract: RV64IMAC (+GC when needed) interpreter at ≥10–50 MIPS on
+one host thread (not realtime); memory map DSL; deterministic scheduler;
+snapshot/restore; the existing `events.jsonl` schema; a backend ABI identical
+to `Session`'s expectations; an injection ABI; Python-first embedding. §H3.4
+adds: differential tests against QEMU, gate behind H2, minimal `Backend`.
+
+## 3. Goals and non-goals
+
+**Goals**
+
+1. Byte-for-byte compatible `Session` behaviour for every script, probe,
+ plugin, recording and comparison that works today.
+2. Determinism: same image + profile + event log ⇒ identical event stream
+ (modulo host-only metadata) on every run and host.
+3. Cheap snapshot/restore and time-travel from snapshot + replay.
+4. Interceptable device and RM-core boundaries: accesses and calls can be
+ logged, stubbed, forwarded or (later, H4) mutated.
+5. Firmware/device knowledge as versioned data (profiles), not code edits.
+
+**Non-goals**
+
+* Realtime or wall-clock guarantees; virtual time only.
+* Confidentiality/isolation/security claims: a study environment only (§06 §6).
+* Replacing QEMU before M4; native is opt-in (`--backend native`, M6).
+* Graphics/board emulation, undeclared fidelity claims, or shipping: nothing
+ here becomes a dependency of `gsp-rm-vfio` or a guest.
+
+### 3.1 Fidelity claims
+
+Levels are exactly those of `docs/06-ACCURACY.md`; the native backend does
+not get to invent new ones.
+
+| Level | Property | QEMU backend today | Native target (gated) |
+|---|---|---|---|
+| L0 | instruction semantics | high (TCG, same ISA family) | parity with QEMU on differential tests (M1) |
+| L1 | memory behaviour | high for RAM-resident code | parity per profile; map is versioned data (M2) |
+| L2 | time/concurrency | not modelled | deterministic virtual time and event order, **not** cycle accuracy (M3); cycle claims only via H6 |
+| L3 | device environment | absent | parity with the H2.2 device model (M4) |
+
+No L2/L3 conclusion may be published before its gate; timing is out of scope
+entirely — the L2 row means reproducibly *ordered* events, not a timing model.
+
+## 4. Architecture
+
+The control plane is unchanged: `Session`, hooks, watchpoints, `Memory`,
+`SymbolDB`, `Recorder`, `analyze`/`compare`, probes, plugins and the Ghidra
+bridge sit above a backend interface. The §H3.2 sketch becomes:
+
+```
++-------------------- green-eye Python control plane --------------------+
+| Session API, hooks/watchpoints, recorder, plugins, Ghidra/agent bridge |
++---------------------------- Backend interface -------------------------+
+ | QEMU-RSP backend (today) | native backend (planned)
+ v v
+ qemu-system-riscv64 libgreenmachine
+ + greeneye/rsp.py + cpu/ decoder + execute
+ + greeneye/target.py + mem/ map DSL, COW snapshots
+ + dev/ falcon, csb, ptimer, dma, pmu
+ + sched/ cores, timers, event queue
+ + trace/ event bus, exec/access
+ + inject/ blob loader, interceptors
+```
+
+Design rules: (1) the backend implements transport primitives only —
+Frida-like `run_until`/`call` stay in `Session` (`greeneye/session.py:373`,
+`:403`) and work on every backend for free; (2) device knowledge is data
+(profile) plus a small module, never a core special case; (3) every backend
+is optional — the Python package stays stdlib-only, and the native artifact
+is loaded at session construction with a version check.
+
+## 5. Backend interface
+
+`Session` today assumes an RSP-like client. The planned interface is the same
+small set of operations; `run_until` and `call` are implemented above it.
+
+```python
+class Backend(Protocol):
+ def capabilities(self) -> set[str]: ... # machine_snapshot | injection | haystack
+ def close(self) -> None: ...
+ def read_registers(self) -> dict[str, int]: ...
+ def read_reg(self, name: str) -> int: ...
+ def write_reg(self, name: str, value: int) -> None: ...
+ @property
+ def pc(self) -> int: ...
+ def read_mem(self, addr: int, size: int) -> bytes: ...
+ def write_mem(self, addr: int, data: bytes) -> None: ...
+ def step(self) -> StopReply: ...
+ def continue_(self, budget: Budget | None = None) -> StopReply: ...
+ def interrupt(self) -> StopReply: ...
+ def set_sw_break(self, addr: int) -> None: ...
+ def clear_sw_break(self, addr: int) -> None: ...
+ def set_hw_break(self, addr: int) -> None: ...
+ def clear_hw_break(self, addr: int) -> None: ...
+ def set_watch(self, addr: int, size: int, kind: str) -> None: ...
+ def clear_watch(self, addr: int, size: int, kind: str) -> None: ...
+ def save_moment(self, label: str) -> str: ...
+ def restore_moment(self, moment_id: str) -> None: ...
+ def inject(self, blob_elf: str, slot: str) -> "InjectionHandle": ...
+```
+
+| Session need | Backend primitive | QEMU-RSP backend (today) | Native backend (planned) |
+|---|---|---|---|
+| registers | `read_registers`/`read_reg`/`write_reg` | `g`/`p`/`P` packets | register file + CSRs, direct |
+| memory | `read_mem`/`write_mem` | `m`/`M`/`X`, chunked to `PacketSize` | region lookup; RAM direct, MMIO callbacks |
+| software break | `set_sw_break`/`clear_sw_break` | `Z0`/`z0`; `Session._step_over` | breakpoint table in the dispatch loop |
+| hardware break | `set_hw_break`/`clear_hw_break` | `Z1`/`z1` | PC match |
+| watchpoint | `set_watch`/`clear_watch` | `Z2/Z3/Z4`; stops *at* the accessing instruction | access check; contract below |
+| step | `step` | `s` | one decoded instruction |
+| continue | `continue_(budget)` | `c` + wall timeout + interrupt | run to breakpoint/budget/event |
+| stop | `interrupt` | `0x03` | out-of-band flag checked per instruction |
+| region snapshot | `read_mem` via `Memory` | copy over RSP | direct map copy |
+| machine snapshot | `save_moment`/`restore_moment` | unsupported (raise, never lie) | COW pages + `moment.json` |
+| injection | `inject` | docs/12 path: writes + `Session.call` | loader + service registry + interceptors |
+| virtual time | `budget`, `StopReply.vt` | absent (host wall clock) | instruction count + timer ticks |
+
+Compatibility decisions:
+
+* `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.
+* **Watchpoint contract.** QEMU stops after the watched access and requires
+ the clear/step/re-arm dance (`greeneye/session.py:538`). Native advertises
+ `watch_after_access` so the Session skips the dance on that path only; a
+ `watch_before_access` mode exists for injection experiments but is not the
+ default, which must reproduce today's event profiles.
+* `Budget` is `{instructions, until_event}`; the wall-clock `duration=`
+ argument is translated via the profile's nominal clock, so native `run` is
+ deterministic. `StopReply` gains optional `vt` and `access`, defaulting for
+ old parsers.
+
+## 6. Core choice: interpreter first, JIT later
+
+| Option | Determinism | Speed | Debuggability | Cost / risk |
+|---|---|---|---|---|
+| Pure-Python interpreter | highest | very low (≪1 MIPS) | highest | prototype only; excellent oracle |
+| Decode/execute interpreter (recommended M1–M5) | highest | 10–50 MIPS achievable in Rust/C++ | high; per-instruction hooks and single-step are trivial | low-moderate |
+| JIT (copy-and-patch / TCG-like) | high if codegen is fixed | ≫100 MIPS | hard: self-modifying code, translation-cache invalidation | deferred to M6, only if profiling demands |
+| Existing core (e.g. a small open RISC-V interpreter/JIT) | depends | varies | depends | evaluation only; hooks/snapshots must be retrofit |
+
+**Decision.** Build a decode/execute interpreter with the ISA behind a
+`CpuCore` interface: a decoder mapping instruction words to typed operations,
+an execute loop dispatching into the memory map. `MISA` is a profile property
+(`rv64imac` default, `rv64gc` selectable, per 07 §P3); compressed
+instructions are canonicalised first so breakpoints and single-step work
+uniformly. No FPU/vector until a GC profile is needed. An embedded existing
+core (public options such as `libriscv`/`rv8`) is an **evaluation note only**:
+revisit at M4 as a `CpuCore` if its memory callbacks, watchpoint granularity,
+snapshot hooks and licence/provenance fit — it is not the M1 substrate,
+because those hooks *are* the product.
+
+## 7. Memory map DSL and device plugins
+
+The map is versioned data interpreted by `mem/`. Region kinds are `ram`,
+`mmio`, `dma` and `host-window`; each has attributes, logging policy and
+either a backing store or a device. Windows resolve to device instances.
+
+```yaml
+schema: greenmachine-profile/1
+profile: tu10x-gsp
+isa: rv64imac
+devices:
+ falcon0: { module: falcon, instance: gsp }
+ csb0: { module: csb, mailbox_depth: 8 }
+ ptimer0: { module: ptimer, hz: 1000000 }
+ pmu0: { module: pmu_stub, revision: r1 }
+ dma0: { module: dma, iommu: pass-through }
+regions:
+ - { name: tcm, base: 0x4000000, size: 0x40000, kind: ram, attrs: [r,w,x] }
+ - { name: gsp_regs, base: 0x2000000000000000, size: 0x100000, kind: mmio,
+ log: changes, # off | changes | access
+ windows: [{ offset: 0x000000, size: 0x1000, device: falcon0 },
+ { offset: 0x001000, size: 0x1000, device: csb0, writable: true },
+ { offset: 0x002000, size: 0x1000, device: ptimer0 },
+ { offset: 0x200a00, size: 0x0200, device: pmu0 }] }
+ - { name: host_dma, base: 0x100000000, size: 0x10000000, kind: dma,
+ device: dma0 }
+```
+
+Device modules are small in-process plugins; the same interface is what makes
+`stub`/`log`/`forward` interception possible later:
+
+```python
+class Device(Protocol):
+ def reset(self, ctx) -> None: ...
+ def read(self, ctx, offset: int, size: int) -> int: ...
+ def write(self, ctx, offset: int, size: int, value: int) -> None: ...
+ def tick(self, ctx, now: int) -> list[Event]: ...
+ def save_state(self) -> dict: ...
+ def load_state(self, state: dict) -> None: ...
+```
+
+Planned modules (all stubs until the H2.2 model defines semantics):
+
+| Module | Role | Notes |
+|---|---|---|
+| `falcon` | FALCON/SEC2 register windows, imem/dmem, bootvec | read-back semantics for boot polling |
+| `csb` | CSB/TRANSCOM mailbox windows | message queues, doorbells as events |
+| `ptimer` | programmable timer | feeds the scheduler; deterministic ticks |
+| `dma` | descriptor engine into host/FB windows | completions are scheduled events, not threads |
+| `pmu_stub` | identity/clock provisioning reads | canned but recorded; never a timing source |
+| `boot_shim` | FWSEC/booter/SEC2 hand-off state | documented shim; assumptions in session metadata |
+
+Profiles are keyed by `(device, firmware-family, profile-version)` and carry
+the firmware hash, provenance, shim assumptions and the validated map. A new
+pairing is a new profile, never a core change; profiles are JSON-Schema
+validated in CI.
+
+## 8. Deterministic scheduler
+
+Virtual time advances only by executed instructions and scheduled events; no
+host clock, thread or entropy source may influence guest-visible state.
+
+* **Cores.** One or more vCPUs with instruction budgets; the default is a
+ strict, seeded round-robin so interleavings are reproducible.
+* **Event queue.** Timers, IRQ lines, DMA completions and external stimuli
+ are ordered `(vt, kind, source, payload, seq)`; ties break by sequence
+ number, never host timing. `ptimer` ticks enqueue IRQ events; delivery
+ defaults to instruction boundaries, and injected interrupts are explicit
+ and recorded.
+* **Replay.** A recording captures a seed plus the ordered external-event
+ log; re-running the same image, profile and log must reproduce the event
+ stream and final memory hashes. The determinism test runs every suite twice
+ and compares a canonical hash.
+* **Hygiene (tested).** No `dict` iteration order in device dumps, no
+ unordered set serialisation, no host paths in semantic fields, fixed page
+ allocation order, fixed hash seeds where a container affects order.
+
+## 9. Snapshot/restore and time-travel
+
+A machine snapshot ("moment") is a new artifact type, not a change to region
+snapshots (`snapshots/*.bin.z`, `01-DESIGN.md §6`):
+
+```
+moments/<id>/
+ moment.json # greenmachine-moment/1, profile id+hash, vt, event-log offset,
+ # register file, CSR/MPU state, scheduler cursor, device state
+ pages/ # COW page list: base page id + XOR/delta or full page
+ events.tail # optional copy of events since the previous moment
+```
+
+* **Memory** is page-granular copy-on-write from a base moment; a 256 MiB
+ image that changes little is a page list, not a copy. **Device state**
+ comes from `Device.save_state()`; devices without it are marked "reset
+ required" and the moment is partial. **Event-log position** indexes
+ `events.jsonl`; restore replays external events from that index.
+* **Use cases (M2/M3):** checkpoint before a fault and explore both branches;
+ flip one reply byte and compare; restore and re-run a suffix to delta-debug
+ an injected service; seed a fuzzing harness from a moment. **Limits:**
+ in-flight DMA and out-of-band input must be in the event log, host/FB
+ windows backed by real files are excluded, and the profile hash must match.
+
+## 10. Event bus
+
+The recorder schema (`greeneye-session/1`, one JSON object per line with `t`
+and `type`, `greeneye/record.py`) is preserved exactly. The native bus is the
+single producer for stop-based and full-access events:
+
+| Event type | Status | Fields |
+|---|---|---|
+| `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` |
+| `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
+host paths as non-semantic, so cross-backend comparison is first-class.
+
+## 11. Injection ABI
+
+The docs/12 blob contract is reused unchanged: ELF64 `EM_RISCV`, RV64IMAC,
+soft-float, PIC, one dispatch entry, optional `ge_service_t` metadata. The
+native backend changes *where the blob is registered* and *what can be
+intercepted* (H1.3/H3.1):
+
+* **Loader.** `Backend.inject()` verifies PIC/no-relocations, maps PT_LOAD
+ segments through the memory map, and records `service_injected`.
+* **Service registry.** The demo path registers into the firmware handler
+ slot as today; the RM path (M5) registers into a service table presented to
+ the simulated RM core, so the firmware dispatches to the service itself.
+* **Interception points** are declared in the profile and matched at runtime:
+
+| Mode | Behaviour | Intended use |
+|---|---|---|
+| `log` | real call executes; target, args and return recorded | observation, M5 evidence |
+| `stub` | simulator returns a canned reply; real code skipped | isolate the RM front-end from an engine |
+| `forward` | call routed to a nominated implementation or injected service | prototype a replacement engine |
+| `mutate` (gated H4) | argument or reply deterministically corrupted; mutation id recorded | fuzzing harness |
+
+**ABI v2 sketch (M5).** `ge_service_fn(cmd, a0, a1)` becomes
+`ge_service_v2(ctx, cmd, a0, a1)` where `ctx` carries a call table; blobs
+declare their ABI version so loaders can refuse mismatches. Interception
+applies to call-table calls, MMIO accesses and the RM dispatch boundary.
+
+## 12. Language and embedding options
+
+| Option | Speed | Embedding | Safety/ergonomics | Build & CI cost | Assessment |
+|---|---|---|---|---|---|
+| C/C++ core | high | CPython C API or pybind11 | manual memory; best if a core is embedded later | compiler matrix; sanitizers essential | viable fallback; no build advantage |
+| Rust core + PyO3 | high | first-class via PyO3/maturin | memory-safe; enums/decoders fit the ISA well | cargo + maturin; easy wheel caching | **recommended** |
+| Pure Python | lowest | native | fastest iteration; great reference model | none | M0/M1 oracle, not the product core |
+
+**Recommendation.** Build a pure-Python reference interpreter first as an
+executable specification and differential oracle (small, CI-friendly, forces
+the memory-map semantics to be testable), then the production core in Rust
+with PyO3 behind an identical C-visible ABI. The C ABI lets a C/C++ core or
+an existing core slot into `CpuCore` without touching the control plane.
+
+## 13. Implementation plan (expands H3.3)
+
+Entry gate for all milestones: **H2.2 exit** and a stable H2.2 device
+semantics baseline. Effort is rough engineering-weeks for one developer
+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 |
+| 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 |
+
+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.
+
+## 14. Differential testing and performance targets
+
+**Differential testing (the fidelity gate for every milestone).** Fixture:
+the bundled demo firmware plus a re-hosted uproc build once M2 exists. The
+same script runs on both backends; recordings are canonicalised (drop `t`,
+host paths, backend identity, QEMU-log-only fields) and compared: identical
+event-type sequence/counts; identical hook/watch hits and `pc` per stop;
+byte-identical region snapshots at matched points; `exec`/`access` streams
+equal in order, multiplicity and virtual timestamps at equal log levels.
+
+Divergence blocks the milestone and is filed with both session directories as
+evidence (`06 §5`/`§7`); a declared fidelity gap is recorded, not waived.
+
+**Performance targets** (one host thread; a checked `greeneye-sim bench`
+harness, trended in CI):
+
+| Metric | Target | Gate |
+|---|---|---|
+| RAM-bound tight loop (no logging) | ≥10 MIPS | M1 |
+| Interpreter optimized | ≥50 MIPS | M6 |
+| JIT (only if built) | ≥200 MIPS | post-M6, gated |
+| Moment save, 256 MiB RAM, sparse changes | ≤100 ms | M3 |
+| Moment restore | ≤50 ms | M3 |
+| Watchpoint-enabled slowdown | ≤2× | M4 |
+| MMIO access vs RAM access | ≤5× | M2 |
+| Event bus throughput | ≥1 M events/s buffered, lossless under budget | M3 |
+
+## 15. CI strategy
+
+* The Python package and QEMU path remain the default, always-on suite; no
+ native artifact is required for it.
+* Native tests carry a `native` marker and `pytest.importorskip`-style
+ detection: without the built backend they skip, never fail. A dedicated job
+ builds the core (cargo/maturin), runs native + differential tests, the
+ determinism double-run and profile-schema validation; nightly while M1 is
+ in flight, required at M1 exit.
+* Real-image fixtures are opt-in (`GREENEYE_FW`); the default job uses the
+ bundled demo. Golden sessions are per backend and profile-hash checked.
+
+## 16. Annex — Verilog soft processor (roadmap H6, far later)
+
+**Scope (H6.1).** An RTL soft implementation of the GSP core environment:
+RV64 core with TCM, MPU/PMP, vectored interrupts and timers; a device fabric
+with falcon windows, CSB/TRANSCOM, DMA to host and WPR2/FWSEC state. RTL is
+separate from `libgreenmachine`; it only shares profile data.
+
+**Run modes and integration (H6.2).** Verilator for determinism and
+coverage, FPGA for speed, optionally both behind one co-simulation bridge. A
+`Backend` talks to the simulation's clock/step and memory API: `step` becomes
+clock edges, breakpoints become traps, watchpoints become signal/page
+monitors, snapshots use saved simulation state. A new `cycle` event class
+makes L2 timing *measurable* — only under docs/06 caveats, and physical
+measurement is still required for any silicon claim.
+
+**Gates (H6.3).** Requires H2/H3 proven, a validated device map, and a funded
+reason to care about cycle accuracy; not on the critical path and not before
+the baseline project-roadmap goals. **Security posture:** a study
+environment, never a confidentiality boundary, never shipped, never in a
+guest; no isolation or side-channel claim may be based on it (`06 §6`).
+
+## 17. Risks and mitigations
+
+| Risk | Impact | Mitigation |
+|---|---|---|
+| Fidelity divergence from QEMU/hardware | wrong conclusions | differential suite from M1; milestones gated on canonical event agreement; declared gaps recorded per docs/06 |
+| Two backends to maintain | effort/tool decay | minimal `Backend` interface (H3.4); shared `Session` primitives; QEMU stays the reference backend in CI |
+| Effort creep before the gate | roadmap damage | explicit H2.2 entry gate; this document is design-only until then |
+| Determinism leaks (clock, hashing, dict order, host input) | replay broken, trust lost | double-run determinism test; sequence-number tie-breaks; no host time in semantic fields |
+| DSL insufficient for device semantics | profile churn, core edits | `Device` modules own whole windows; escape hatch is a module, never a core special case |
+| Snapshot size/I/O blowup | snapshots unusable on real images | page-level COW + sparse encoding; region-scoped logging; moment retention policy |
+| Injection misuse (real hardware/guest) | safety/security incident | local-only, dev-only tooling; no guest presence; interception only in the simulator; docs/12 policy checks |
+
+## 18. Open questions
+
+1. Which GC extensions must the first `rv64gc` profile cover, and can FPU
+ instructions be trapped and recorded rather than executed until then?
+2. Are before-access watchpoints needed for injection, and how do their
+ recordings stay distinct from the QEMU-compatible profile?
+3. What page size/encoding do moments need, what restore budget, and how much
+ RTOS semantics must be modelled for M3 (docs/09) without a timing claim?
+4. Is Rust + Python sufficient, or does the profile DSL need a native loader,
+ and does an embedded public core's licence fit this repository (M4)?
+5. How are multi-core interleavings represented so "same seed + same log"
+ stays the only replay contract, and which events must be byte-identical
+ across backends for the evidence rules?