summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
commit002e3d7d13c414ea874abbee8d82803dd3a72fe5 (patch)
tree6627c12fa07f6a1cf73e5aaeb7390e698dd3a896 /debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md
parent2f249817df327f5dcccf0464ad517fa60d6071d3 (diff)
downloadopen-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.tar.gz
open-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.zip
debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md445
1 files changed, 445 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md b/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md
new file mode 100644
index 0000000..0f9afbe
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/19-CONTROL-DISCOVERY.md
@@ -0,0 +1,445 @@
+# 19 — Control discovery and hot-path reachability
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status and
+> [`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) H4 for the program.
+
+Two harness tools close the loop between "a new firmware image does something
+we do not model" and "here is the code path, its trigger and its effects":
+
+| Tool | What it answers |
+|---|---|
+| `harness/control_discovery.py` | Which command IDs exist, which code path each one takes, and what it changes. New/undiscovered controls fall out of the sweep instead of waiting for documentation. |
+| `harness/reach.py` | Given one decompiled symbol/address: is it reachable, what triggers it, what arguments does it need, what happens. |
+
+Both are simulator-only, bounded, evidence-producing, and depend only on the
+public `Session`/hook/watch primitives (stdlib-only Python). They are the
+practical start of the "Sandsifter for RM" idea in
+[`GREEN-EYE-ROADMAP.md`](../GREEN-EYE-ROADMAP.md) H4 and reuse the contracts
+in [06-ACCURACY](06-ACCURACY.md) and [14-ADVERSARIAL-INTEROP-GUIDE](14-ADVERSARIAL-INTEROP-GUIDE.md).
+
+---
+
+## 1. The problem
+
+The GSP's RM is a message/method system: services own objects and answer
+controls dispatched by ID. NVIDIA ships new images on a hidden schedule,
+stripped and signed, and new services, messages and controls appear between
+versions. Static diffing (docs/18 portability matrix, docs/14 section 4.1)
+tells us the code changed; it does not tell us which input activates the new
+path, what the path needs, or what it changes. A sweep of the input surface
+plus dynamic hooks answers exactly those questions, and it is safe to run in a
+simulator where the firmware cannot be damaged.
+
+## 2. The discovery model
+
+### 2.1 Sweep spaces
+
+A "call surface" is a finite input space with an observable effect:
+
+| Surface (today / future) | Space | Probe action |
+|---|---|---|
+| demo mailbox (`--surface demo`) | command IDs `1..--max-id`, with argument patterns | write `gsp_ctrl.arg0..2`, then `gsp_ctrl.mailbox` |
+| RM control/method entry (`--surface rm`, H1.3) | control IDs, classes and method opcodes | call the RM method entry with `(control, a0, a1, a2)` |
+| future CSB/TRANSCOM message functions | message function IDs | write the message buffer and ring the doorbell |
+
+ID 0 is the demo's idle mailbox sentinel (the polling loop ignores it), so the
+sweep starts at 1; surface classes with a meaningful 0 can start there instead.
+
+The same machinery applies to any of them because the sweep is described by a
+:class:`Surface` subclass: a probe function, an effect observer, and an
+entry-point table.
+
+### 2.2 Deterministic argument patterns
+
+Every ID is probed with `--patterns` (default all three):
+
+* `zero` — `(0, 0, 0)` (the "no arguments" baseline);
+* `sentinel` — three distinct pseudo-random 64-bit values from a stream keyed
+ by `(seed, id)`, so an ID that misuses an argument is caught;
+* `boundary` — `(2^64-1, 1, 2^63-1)`, the usual off-by-one/size boundaries.
+
+Patterns are deterministic: the same `--seed`/`--max-id` replays the same
+findings byte-for-byte (verified by `tests/test_discovery.py`).
+
+### 2.3 Entry-point modelling
+
+Before probing, the tool reads the surface's declared entry points:
+
+* the dispatcher symbol (`rpc_dispatch` on the demo) is hooked for every
+ probe, capturing the command and the raw `arg0..arg2` at dispatch entry;
+* every non-zero handler-table slot (`gsp_ctrl.handlers[0..7]`) is hooked so a
+ hit can be attributed to a function even when the command is not one of the
+ documented switch cases.
+
+On real RM the same role is played by the RM-core dispatch table / object
+class table: hook the dispatcher, then resolve the handler the simulator
+routes to.
+
+### 2.4 Effect capture
+
+Each probe observes:
+
+* control-block fields before/after (`state`, `rpc_count`, `last_rpc`,
+ `result`, `buf` hash) plus write watchpoints on the scalar fields, which
+ also record the *store PC* (`effects.watch`);
+* the new QEMU exception window (`-d guest_errors,unimp,int`), parsed with
+ `greeneye.analyze.parse_qemu_faults`, so traps (`cause 3`) and faults are
+ distinguished from "no progress";
+* stop signals as a backend-independent fallback (a native backend has no
+ QEMU log: SIGTRAP maps to trap, SIGSEGV/SIGBUS/SIGILL to fault; budget
+ SIGINTs are deliberately excluded from findings).
+
+### 2.5 Classification
+
+One probe yields one outcome, in priority order:
+
+| Outcome | Meaning |
+|---|---|
+| `trap` | breakpoint exception (`ebreak`) or trap loop below `0x100` |
+| `fault` | load/store/fetch/illegal exception |
+| `method-return` | an RM-style call entry returned a value |
+| `no-progress` | the mailbox was never consumed in the budget |
+| `injected-slot` | command routed through the injected-service handler table |
+| `known-handler` | a declared handler ran |
+| `default-path` | the dispatcher catch-all ran (no dedicated handler) |
+| `settle-failed` / `error` | the pre-probe state or the probe itself failed |
+
+`default-path`, `injected-slot` and `trap/fault` are the interesting classes:
+they are where new behaviour, injection wiring and input bugs live.
+
+---
+
+## 3. `control_discovery.py` — unknown-call discovery
+
+### 3.1 Usage
+
+Full demo mailbox sweep (189 probes: IDs 1..63 times three patterns):
+
+```bash
+python3 harness/control_discovery.py --out dumps/discovery --seed 7
+```
+
+A quick sweep for CI or bring-up (IDs 1..32, one pattern):
+
+```bash
+python3 harness/control_discovery.py --surface demo --out dumps/discovery-min \
+ --max-id 32 --patterns zero --seed 5 --timeout 0.25
+```
+
+Other surfaces / budgets:
+
+```bash
+# RM surface (documented future interface; see section 5)
+python3 harness/control_discovery.py --surface rm --out dumps/discovery-rm
+
+# cap the probe count and keep every recorded session
+python3 harness/control_discovery.py --out dumps/discovery-cap --iterations 40 --record-all
+```
+
+Flags: `--max-id` (1..4096), `--patterns zero,sentinel,boundary`, `--seed`,
+`--iterations` (0 = all probes), `--timeout` (per-probe observation window),
+`--settle` (self-test budget), `--backend qemu|native` (QEMU is the supported
+default; native drops the QEMU fault log and uses signals), `--kernel`
+(bundled demo by default; a foreign ELF needs `--allow-foreign-elf`),
+`--record-classes` and `--record-all`, `--max-log-bytes`.
+
+Each probe uses a **fresh session**, settles through the self-test, then
+applies one command. Fresh sessions are the safe strategy: a trap or fault can
+leave the demo in a state that would corrupt the next probe.
+
+### 3.2 Output
+
+```text
+<out>/
+ findings.jsonl one compact finding per probe (deterministic order)
+ discovery-report.json format + options + per-ID aggregate + findings
+ control-discovery.md rendered report grouped by class
+ sessions/<idx>-id<NNN>-<pattern>/ kept evidence for interesting classes
+```
+
+Finding shape (stable keys, no wall-clock data):
+
+```json
+{"probe": 15, "cmd": 16, "pattern": "zero", "args": [0, 0, 0],
+ "outcome": "injected-slot",
+ "handler": "rpc_ping", "handler_entry": 2147483720, "documented_handler": null,
+ "dispatch_seen": true, "dispatch_pc": 2147483912, "dispatch_args": [16, 0, 0, 0],
+ "rpc_count_delta": 1,
+ "effects": {"changed": [{"field": "result", "before": 48, "after": 48879}],
+ "watch": [{"field": "result", "pc": 2147484066, "symbol": "rpc_dispatch"}]},
+ "fault": null, "signals": [], "settle_ok": true,
+ "session": "sessions/0015-id016-zero", "reason": null}
+```
+
+### 3.3 Reading the report
+
+The markdown report opens with the outcome counts and an "Ids by class" line,
+then sections that matter:
+
+* **Known handler ids** — the documented dispatch table (by the firmware's own
+ control block, not the docs);
+* **Injected-slot dispatch ids** — commands whose path is the injected-service
+ table, with the handler each aliases;
+* **Unknown / undiscovered ids** — the explicit list that answers "what is
+ not handled here yet";
+* **Traps and faults** — IDs that reach error paths under some input;
+* **All findings by id** — the join key for `findings.jsonl`.
+
+### 3.4 What the demo sweep already shows
+
+Measured on 2026-10-02, demo ELF sha256 `c038a04a...`, 189 probes in ~36 s:
+
+* IDs 1..6 hit `rpc_ping`/`rpc_echo`/`rpc_fill`/`rpc_sum`/`rpc_state`/
+ `rpc_fault`; the ID 6 path ends in the `ebreak` trap and never writes
+ `result` (the dispatcher's post-handler stores do not run);
+* **discovered class:** IDs 16..23 fall through the switch into the
+ injected-service handler table. IDs 16..21 alias the *built-in* handlers
+ (`16 -> rpc_ping`, ..., `21 -> rpc_fault`); IDs 22..23 are NULL slots and
+ return `~id`. This is exactly the kind of wiring a new firmware can add
+ silently;
+* IDs 7..15 and 24..63 take the catch-all and return `~id` with
+ `rpc_count + 1` — the unknown-ID space;
+* argument patterns change the class where the handler uses the argument:
+ `rpc_echo` and `rpc_sum` fault on sentinel/boundary pointers, while
+ `rpc_ping`/`rpc_state`/`rpc_fill` stay on the handler path.
+
+---
+
+## 4. `reach.py` — hot-path reachability
+
+### 4.1 Usage
+
+```bash
+# Directly callable target, with an argument
+python3 harness/reach.py --target rpc_ping --method call --args 0x41 \
+ --out dumps/reach-ping
+
+# Natural boot/self-test path
+python3 harness/reach.py --target rpc_sum --method run --out dumps/reach-sum
+
+# Only reachable through a mailbox command: discover the trigger from the
+# handler table and apply it
+python3 harness/reach.py --target rpc_fault --method trigger --out dumps/reach-fault
+
+# Replay a saved probe spec; emit one for CI
+python3 harness/reach.py --probe dumps/reach-fault/probe.json --emit-probe /tmp/fault-probe.json
+```
+
+Flags: `--method auto|stimulus|trigger|run|call`, `--all-methods`, `--args`,
+`--arg-count` with `--arg-fill zero|sentinel`, `--timeout`, `--settle`,
+`--stimulus script.py`, `--probe spec.json`, `--emit-probe`, `--record [DIR]`,
+`--max-prior`, `--kernel`, `--backend`, `--allow-foreign-elf`.
+
+### 4.2 Methods
+
+`auto` tries methods in this order and stops at the first hit (use
+`--all-methods` to compare routes):
+
+1. `stimulus` — run a green-eye script (or a probe spec's stimulus) with the
+ target hook and watchpoints armed. The script drives `session.run` itself.
+2. `trigger` — read the surface's handler table, find the command ID(s) that
+ resolve to the target, settle the firmware, apply that command and run
+ bounded. This is the trigger discovery for IDs 1..6 and 16..23 on the demo.
+3. `run` — `Session.run_until(target, timeout)` from launch, which catches the
+ natural boot/self-test path.
+4. `call` — `Session.call(target, args)`, the Frida-like direct call, for
+ unit-testing a function in isolation.
+
+A method reports a hit when the target-entry hook fires, even if execution
+immediately traps (`rpc_fault`): reachability and success are different
+questions, and the report distinguishes them.
+
+### 4.3 Output
+
+```text
+<out>/
+ reach-report.json machine-readable report (stable keys)
+ reach-report.md rendered report
+ probe.json suggested repeatable probe spec (on a hit)
+ sessions/<method>/ optional recorded session (--record)
+```
+
+The report records:
+
+* `reachable`, `method`, and every `methods_tried` with its miss reason;
+* `entry`: the register state at the target (args `a0..a3`, `ra`, `sp`) and
+ `dispatch_args` — the authoritative command arguments when the target was
+ reached through the dispatcher;
+* `prior_events`: the ordered hook events that led to the hit (bounded by
+ `--max-prior`);
+* `required_args`: discovered arguments plus their provenance (`call-args` or
+ `firmware-dispatch`) and the caller `ra`;
+* `effects`: field deltas, watchpoint stores, the return value, and whether a
+ trap/fault followed (`rpc_fault`: trap `ebreak` at `pc 0`);
+* `trace_window`: hook counts and, when recorded, session event counts;
+* `probe_spec`: the exact spec to replay the route.
+
+### 4.4 Demo results
+
+Measured on 2026-10-02 (each run ~0.5 s, QEMU):
+
+| Target | Method | Result |
+|---|---|---|
+| `rpc_ping` | `call` with `--args 0x41` | reachable; returns `0x41 ^ 0xbeef = 0xbeae` |
+| `rpc_sum` | `run` | reachable in the self-test; entry args `(buf, 16, 32, ...)`; path passes through `rpc_ping` -> `rpc_echo` -> `rpc_fill` first |
+| `rpc_fault` | `trigger` | reachable via mailbox command 6 (also aliased at 21); dispatch args `cmd=6, a0=0, a1=0`; immediate `ebreak` trap |
+
+The self-test path is a useful reminder that compile-time inlining can bypass
+the dispatcher: the `run` route reaches `rpc_sum` from an inlined call site,
+so there are no `rpc_dispatch` hits before it. The `trigger` route does pass
+through the dispatcher and exposes the control arguments.
+
+---
+
+## 5. Sandsifter for RM (the future surface)
+
+A Sandsifter-style tool for RM needs three more capabilities than the demo
+has today, in this order.
+
+### 5.1 The `Surface` abstraction (already in place)
+
+`harness/control_discovery.Surface` separates:
+
+* `probe(session, id, args)` — how one call is applied (mailbox write today;
+ RM method call / message write later);
+* `observer()` — the effect model (control-block fields today; RM reply
+ buffer and object state later);
+* `handler_map()` / `command_for_addr()` — how IDs map to code, which drives
+ both discovery and reach's `trigger` method.
+
+Adding a real RM surface is a new subclass, not a rewrite of the sweep,
+classification or reporting.
+
+### 5.2 RM sweep design
+
+1. **Control/method inventory.** Start from `rmtool`/`rmq` (docs/17) for the
+ host-visible control list and class IDs; import the Ghidra RM-core dispatch
+ table into the symbol DB. The sweep ranges are then control IDs by class
+ (`--rm-class`) and method opcodes within a control, mirroring the
+ `nv2080 method call` surface.
+2. **Entry-point modelling.** Hook the RM dispatch routine, the object
+ constructor/method tables and the status/notifier writes. For each swept
+ ID, capture the handler entry, the object it selects and the status it
+ returns.
+3. **Effect observation.** Point `--rm-reply` at the reply buffer for the
+ control and hash/snapshot it; add watchpoints on the object handle table
+ and status queue. Compare the reply field-by-field against the host oracle
+ (`rmtool` JSON) for M2 (docs/06).
+4. **Branch exploration.** The native simulator's snapshots (H3) make it cheap
+ to fork at the dispatcher and sweep IDs without re-booting; the QEMU
+ surface uses fresh sessions exactly like today.
+5. **Corpus and gates.** Findings become an inventory corpus (`findings.jsonl`)
+ that CI diffs between firmware versions; new IDs, new handlers, or changed
+ default-path results are the regression signal (docs/21).
+
+### 5.3 Rollout gates
+
+* Entry gate: H2.2/H1.3 — a simulator session reaches the RM message loop and
+ exposes the dispatch table (otherwise `--surface rm` exits cleanly with the
+ message below).
+* Exit gate: sweep a real control space, reach one previously unknown control,
+ and replay its probe spec through `reach.py` with a recorded session.
+
+When unavailable the tool is explicit, not mysterious:
+
+```text
+$ python3 harness/control_discovery.py --surface rm --out dumps/discovery-rm
+[discovery] skipped: surface 'rm' unavailable: no RM control/method entry point is exposed
+by the simulator or a live session yet (roadmap H1.3). Pass --rm-entry SYMBOL once the
+RM method dispatcher exists; the demo surface works today with --surface demo.
+```
+
+---
+
+## 6. Feeding the simulator/device model and CI
+
+* **Device model.** Each discovered class is a candidate row in the
+ simulator's dispatch/control model: the command ID, the entry symbol, the
+ argument pattern that reaches it, and the effect snapshot are exactly the
+ data the H2/H3 device map needs. Unknown IDs (`default-path`) document the
+ fail-closed/default behaviour the emulation must reproduce.
+* **Accuracy metrics.** The `dispatch_pc`/`handler_entry`/effects fields feed
+ M1 (inventory coverage) and M4/M6 (state and memory side effects,
+ docs/06); compare two firmware versions by diffing `discovery-report.json`
+ (new handler IDs, moved handler entries, changed default results).
+* **CI (docs/21).** The intended gates are:
+ 1. `control_discovery.py --max-id 32 --patterns zero` as a fast inventory
+ smoke test: known handlers must resolve, `rpc_fault` must trap, and the
+ default-path result must stay `~id`;
+ 2. `reach.py` on a pinned list of targets, asserting `reachable` and the
+ method (`call` for `rpc_ping`, `run` for `rpc_sum`, `trigger` for
+ `rpc_fault`) so a compiler/inlining or dispatch change is caught;
+ 3. diff `findings.jsonl`/`discovery-report.json` against a golden run.
+ Findings contain no wall-clock data, so the diff is stable for a fixed
+ seed/params (the same property the mutation harness documents in docs/18).
+* **Probe specs are the interface.** `reach.py --emit-probe` and
+ `--probe` turn a discovered route into an artifact that any agent or CI job
+ can replay without re-deriving it.
+
+---
+
+## 7. Tests
+
+```bash
+python3 -m unittest tests.test_discovery -v
+make test # includes the discovery tests with the rest of the suite
+```
+
+`tests/test_discovery.py` covers the pure functions (pattern determinism,
+probe ordering/caps, classification priority), a 32-probe demo sweep that
+must attribute IDs 1..6, find an unknown ID and an injected-slot alias and
+write the report, byte-identical replay for a fixed seed, the clean RM skip,
+and the three reach demonstrations (`rpc_ping` call, `rpc_sum` run,
+`rpc_fault` trigger). It skips cleanly when QEMU or the demo ELF is absent.
+The suite completes in well under a minute.
+
+---
+
+## 8. Safety and ethics
+
+* **Simulator only.** The tools launch their own QEMU (or the native
+ simulator with `--backend native`) against the bundled demo or an
+ explicitly allowed re-hosted ELF. They never attach to hardware, a guest, a
+ kernel module or an existing gdbstub. Ctrl-C leaves no target running: each
+ session is closed in a `finally`.
+* **Bounded probes.** Every probe has a settle budget and an observation
+ window; `--max-id`/`--iterations` cap the sweep; QEMU log reads are capped.
+* **No hardware writes.** All memory writes go through the debug stub into
+ emulated RAM; the only host writes are under `--out`.
+* **Evidence, not truth.** A `default-path` result is evidence about this
+ firmware at this fidelity (L0/L1, docs/06), not a statement about silicon.
+ Never use a simulator observation to bypass a hardware protection; escalate
+ anything that would change the product to a physical capture (docs/14
+ sections 6-7).
+* **No injected payloads here.** Discovery only *observes* injection slots; it
+ never loads a blob. Injection itself stays behind `greeneye inject` and the
+ docs/12 contract.
+
+---
+
+## 9. Limits and fidelity
+
+* QEMU wall-clock slicing bounds when a handler is interrupted, so a very long
+ handler may be classified from its entry hit without its final result; the
+ finding says `known-handler` and leaves `effects` empty rather than guessing.
+* The demo is not RM: there is no object model, no status queue and no
+ per-control reply. The RM design in section 5 is deliberately unbuilt until
+ the simulator exposes an RM dispatch table (H1.3/H2.2).
+* `trap` vs `fault` uses the guest exception cause; QEMU and the native
+ simulator agree on the demo, but x86/RISC-V mapping differences are a
+ documented fidelity gap (docs/06 section 8).
+* The sweep is single-threaded and deterministic; concurrent/multi-core
+ dispatch interleavings (H2.4) are future work.
+
+---
+
+## 10. Roadmap mapping
+
+| Item | Contribution |
+|---|---|
+| **H4.adversarial navigation** | unknown-call discovery and trigger discovery over control surfaces |
+| **H4.1 semantic diffing** | `discovery-report.json` is a version-diffable inventory (new/changed IDs, handlers, default paths) |
+| **H4.2 capability inference** | `effects.watch` store PCs are access evidence for the capability table |
+| **H4.5 reimplementation loop** | `reach.py` output plus probe spec is the observe -> reproduce artifact for a new behaviour |
+| **H3 native simulator** | `--backend native` reuses the same sweep once parity lands; snapshots will remove the fresh-session cost |
+| **V1 golden data / docs/21 CI** | stable findings and the fast 32-probe gate are the golden corpus |