diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md | 7 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/08-ROADMAP.md | 5 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/11-GSP-RM-SERVICE-DEVELOPER-GUIDE.md | 181 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/12-INJECTION-SDK.md | 149 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/13-GHIDRA-CONTROL.md | 154 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md | 399 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/15-AGENTIC-GSP-IDE.md | 387 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/docs/16-CUSTOM-SIMULATOR.md | 449 |
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? |
