summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-06 19:28:39 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-06 19:28:39 -0400
commit6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf (patch)
tree3b3680902cd2223cbe60aaec0a0799cfc7b26be6 /debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md
parenta5473c91b8869b017385582ea969783b33553ded (diff)
downloadopen-iov-6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf.tar.gz
open-iov-6c3fd5df61ff2f5cb8833ead6c05eaff9fdc92bf.zip
risc-v-gsp-rm-debugger: land docs 45-53 + harness/sim tool set with tests
rm_capture (chunked decode), launch_audit, state_seed/state_decode, valprobe, rehost, contracts+run_contracts, host_traffic, gmmu_walk, msgq_attach, libos_port, log_decode, server_snapshot; doc index updates in the tool overview, operations pages and the repo README.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md297
1 files changed, 297 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md b/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md
new file mode 100644
index 0000000..1dc6203
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md
@@ -0,0 +1,297 @@
+# 52 — RM-object state seeding (`state_seed`) — in development
+
+> **Status: in development (2026-10-06, state-seeding wave).** Documents
+> `harness/state_seed.py`: a declarative **seed** of RM object state
+> (`greeneye-state-seed/1`) that is placed into the same native machine
+> `harness/rehost.py` uses, then run at a chosen entry under a bounded budget.
+> The tool records which seeded state the firmware reads, in order, and the
+> **first gate** it stops at: the address and the state the returning/faulting
+> path checked. This is the state-seeding fixture the campaign asked for:
+> rehost early-outs on missing objects; a seed supplies the
+> objects so the next blocker is named instead of guessed.
+>
+> Development-only: executes firmware on the simulator. Never touches a GPU,
+> kernel module, mdev, guest or network. No timing/cycle claim (L0/L1).
+>
+> **See also:** `46-SURGICAL-REHOST.md` (the runner this reuses),
+> `47-VALIDATION-PREDICATE-PROBE.md` (the predicate probe the mechanism proof
+> replays), `45-RM-RPC-CAPTURE-AND-REPLAY.md` (the capture whose objects a
+> seed can mirror), `06-ACCURACY.md` (fidelity levels), `39-RE-WORKFLOW-REVIEW.md`
+> (time-to-value rankings).
+
+## 1. Why it exists
+
+A cold rehost of a construct/promote entry on the 570.144 image bottoms out in
+the error family the live campaign observes (`0x40` invalid state, or an
+illegal instruction at `pc 0x0` after a NULL indirect call), because the RM
+object state the handler walks -- the client/device object, the channel or
+context object, the graphics instance and its memDesc table -- does not exist.
+The first rehost probes could only record the stop (`docs/46` §3); they could
+not say *which object* was missing, so the next step was still a guess.
+
+`state_seed.py` makes the missing state explicit:
+
+* a **seed** declares symbolic objects (clients, channels, graphics contexts,
+ memdescs/context buffers, generic RM objects), their fields, an allocator
+ that places them at chosen addresses, and pointer fields that reference each
+ other by name;
+* a **loader** compiles the seed into byte writes and register values and
+ applies them to the native machine through the same wiring `rehost.py`
+ uses;
+* a **runner** executes the entry under a bounded budget with watchpoints on
+ every seeded field and records the ordered first-touch state reads, then
+ reports the first gate: the last seeded state read before the stop plus the
+ stop reason.
+
+A field whose value is not known is left at the literal `"unknown"` marker:
+the seeder does not write it and the report lists it. A gate that reads an
+`unknown` field is a finding (the minimal object set is incomplete), not a
+seed bug.
+
+## 2. Schema `greeneye-state-seed/1`
+
+```json
+{
+ "format": "greeneye-state-seed/1",
+ "name": "construct-gate-minimal",
+ "entry": "0x52a70ec",
+ "allocator": {"base": "0x10010000", "align": "0x1000"},
+ "stack": "0x1003e000",
+ "registers": {"a0": "@client", "a1": "@channel", "a2": "0x1003f000"},
+ "stubs": [{"name": "engine_stub", "return": 0}],
+ "objects": [
+ {"name": "client", "type": "client", "size": "0x2000",
+ "fields": {"0x1de8": "@engine_node", "0x1dd8": {"value": 0, "size": 8}}},
+ {"name": "engine_node", "type": "rm_object", "size": "0x1000",
+ "fields": {"0x600": "unknown"}}
+ ],
+ "memory": [{"pa": "0x200000", "u32": 7}],
+ "outcomes": {"0x5290ebc": "refuse-fecs-slot-exists-0x1f"},
+ "watch": [{"pa": "0x200000", "size": 4, "label": "raw-state"}],
+ "reads": [{"pa": "0x200000", "size": 4, "label": "raw-after"}],
+ "provenance": {"source": "construct-path disassembly", "date": "2026-10-06"},
+ "notes": ["minimal object set; +0x600 callback unknown"]
+}
+```
+
+Object `type` is one of `client`, `channel`, `graphics_context` (an alias of
+`channel`), `graphics_instance`, `memdesc`, `ctx_buffer` or the generic
+`rm_object` (no field map; every field is explicit).
+
+Field value forms (object `fields`):
+| Form | Meaning |
+|---|---|
+| number / numeric string | raw value; the width is the field-map width, else 8 bytes |
+| `{"value": v, "size": n}` | raw value with an explicit width (1/2/4/8) |
+| `"@name"` / `"@name+0xNN"` | pointer to another object's or stub's base (+offset) |
+| `"unknown"` / `{"unknown": true}` | explicit unknown: not written, reported in `unknown_fields` |
+| `{"ptr": "@name+0xNN"}` | pointer form in a dict (same as the string form) |
+
+Object placement (the allocator/pointer story):
+
+* objects without `pa` (or its alias `va`) are placed in declaration order
+ from `allocator.base`, each aligned to `align` (object-level `align`
+ overrides) and advanced by `size`;
+* every object name becomes a symbol, so `"@client+0x10"` resolves to the
+ object's address plus `0x10` when the writes are compiled;
+* stubs are the same: a named stub without `addr` is allocated after the
+ objects and can be used as a pointer target (`"@engine_stub"`);
+* in this machine memory is flat and the MPU is inactive, so PA and VA are
+ identical; `va` is accepted as an alias of `pa` and recorded as such.
+
+Other top-level keys:
+
+| Key | Meaning |
+|---|---|
+| `entry` | code entry (required unless `registers.pc` is given) |
+| `registers` | any `x0..x31`/ABI register name; `pc` is an entry alias, not a register |
+| `stack` | initial `sp` when the seed does not set `sp` explicitly |
+| `memory` | raw page entries: `pa` (or `addr`) plus one of `hex/u64/u32/u16/u8` |
+| `stubs` | `{addr?, name?, return}` -> a `lui/addi/jalr ra` returning `return` |
+| `outcomes` | address -> label; reaching one stops the run as `outcome` |
+| `watch` | extra watch ranges `{pa/addr, size, label}` for the state-read log |
+| `reads` | post-run reads included in the report |
+| `provenance`, `notes` | free-form evidence/provenance carried into the report |
+
+## 3. Runner and report `greeneye-state-seed-report/1`
+
+```bash
+python3 harness/state_seed.py demo
+python3 harness/state_seed.py emit --kind fecs --out /tmp/fecs.json
+python3 harness/state_seed.py emit --kind target-minimal --out /tmp/target.json
+python3 harness/state_seed.py emit --kind target-advanced --out /tmp/target-adv.json
+python3 harness/state_seed.py validate --seed /tmp/target.json
+python3 harness/state_seed.py run --seed /tmp/target.json --out dumps/state-seed-target
+```
+
+`run` loads the seed, boots the native machine exactly like `rehost` (device
+registry from the profile, boot shim, libos syscall model, lenient MMIO),
+applies the plan, installs a return trampoline, watchpoints every seeded field
+and runs under `--budget`/`--timeout`. It stops on `outcome` (a labelled
+address), `return` (the trampoline), `fault`, unhandled `ecall`, `timeout` or
+`budget`, and records a `greeneye-session/1` directory plus
+`state-seed-report.json`.
+
+Report highlights:
+
+* `objects[]`: the compiled layout (name, type, PA, size);
+* `unknown_fields[]`: every field the seed marked `unknown`, with its mapped
+ role where one exists;
+* `state_reads[]`: the ordered first touch of each watched field
+ (`pc`, `addr`, `label`, `value`, `width`) -- the state the path checked;
+* `gate`: for a stop, the last read before it and the verdict:
+
+| gate kind | meaning |
+|---|---|
+| `null_indirect_call` | the stop is `illegal @ pc 0x0` and the last read was a 0 function-pointer field; `pc` is the load site, `addr`/`label` the pointer read |
+| `fault` | a fault with a different last read |
+| `<status>_before_gate` | timeout/budget before any early-out; last read shown |
+
+## 4. Known object field map (derived so far)
+
+Status `known` = read by a documented path; `unknown` = the offset is real
+(from the same disassembly) but its semantics/value are not established.
+Every entry is numeric evidence, no source paths.
+
+| Type | Offset | Width | Status | Role | Evidence |
+|---|---|---|---|---|---|
+| client | `0x1be0` | 8 | known | graphics-instance pointer array base (index `0x37c + idx`) | promote predicate (`docs/47`) |
+| client | `0x1dd8` | 8 | known | engine-context pointer; `0` selects the no-engine join | construct-path disassembly (this doc) |
+| client | `0x1de8` | 8 | known | sub-device object; `+0x600` callback is invoked | construct-path disassembly |
+| client | `0x1aa8` | 8 | unknown | sub-object consumed later (`+0x24c` callback) | construct-path disassembly |
+| client | `0x1cc8` | 8 | unknown | sub-object consumed later (`+0x150`, `+0x1d0`) | construct-path disassembly |
+| client | `0x1e48` | 8 | unknown | sub-object on the no-engine path (`+0x110` callback) | construct-path disassembly |
+| channel / context | `0x0d28` | 4 | known | graphics-instance index (u32) | promote predicate (`docs/47`) |
+| channel / context | `0x0898` | 8 | unknown | callback invoked inside `0x5285aa8` | construct-path disassembly |
+| channel / context | `0x0d01` | 1 | unknown | u8 flag: `0` takes the panic path on a failed lookup | construct-path disassembly |
+| channel / context | `0x0cd1` | 1 | unknown | u8 flag before the promote calls | construct-path disassembly |
+| channel / context | `0x0648` | 8 | unknown | callback on the invalid-state path | construct-path disassembly |
+| channel / context | `0x03b8` | 8 | unknown | identity marker compared across objects | construct-path disassembly |
+| channel / context | `0x00ee` | 8 | unknown | sub-object (`+0xe8` read as an index/gfid) | construct-path disassembly |
+| graphics_instance | `0x0190` | 8 | known | pointer to the per-buffer memDesc slot table | promote predicate (`docs/47`) |
+| graphics_instance | `0x0161` | 1 | unknown | u8 flag gating the `0x529bd44` call | construct-path disassembly |
+| graphics_instance | `0x03b8` | 8 | unknown | identity marker compared across objects | construct-path disassembly |
+| memdesc | `0x0000`, `0x0008` | 8 | unknown | slot value semantics outside the promote predicate | promote predicate only checks non-NULL |
+| ctx_buffer | `0x0000` | 8 | unknown | context-buffer (memdesc) object; field semantics unknown | promote predicate only checks non-NULL |
+
+## 5. What the demonstration proves (2026-10-06)
+
+`python3 harness/state_seed.py demo` runs three things, all offline,
+deterministic and bounded:
+
+1. **Mechanism proof on a path that needs no RM objects.** The
+ `fecs-promote` seed replays the `docs/47` predicate through this loader:
+ `FECS init, slot non-NULL` -> `refuse-fecs-slot-exists-0x1f` (stop
+ `0x5290ebc`, 75 instructions) and `slot NULL` -> `accept-commit-path`
+ (`0x5290cd0`). The only difference between the two seeds is the seeded slot
+ value, so the seed data is driving the firmware's own predicate. Two runs
+ of the same seed are identical (status, outcome, instructions, reads).
+2. **First-gate attempt on `FUN_ram_052a70ec` (entry `0x52a70ec`).** The
+ minimal seed places a client, a channel/context, a graphics instance and a
+ memdesc table, sets `a0`/`a1`/`a2`, and walks the real prologue. The path
+ reads, in order: `client+0x1dd8`, `client+0x1cc8`, `client+0x1aa8`,
+ `client+0x1de8`, `client+0x1e48`, then (no-engine join) `channel+0xd28`,
+ then the gate: at `0x52a724c` it loads the `client+0x1de8` sub-object's
+ `+0x600` callback (=0, marked `unknown`) and calls it at `0x52a7268`; the
+ call faults `illegal @ pc 0x0` after 106 instructions. **First gate:
+ `engine_node+0x600` (the indirect call target), read at `0x52a724c`.** The
+ report's `unknown_fields` names it, and the ordered read list shows the
+ client fields the seed satisfied.
+3. **Gate advance.** The same seed with `+0x600` pointed at a recorded
+ return-0 stub walks past the first gate and stops at the next one: inside
+ `0x5285aa8`, `channel+0x898` is loaded at `0x5285af4` and called at
+ `0x5285b0c`; the target is 0 (marked `unknown`), fault after 143
+ instructions. This is the loop the fixture is for: seed the named state,
+ re-run, name the next gate.
+
+Timing on this machine (same environment for all rows): FECS single run
+0.08 s, construct single run 0.07 s, whole `demo` 0.8 s. Zero hardware
+cycles.
+
+## 6. Admission record
+
+Per the project admission rule (`open-iov/docs/eng-ops.md` §2), all five
+fields:
+
+| Field | Value |
+|---|---|
+| Scenario | dynamic-RE loop: run state-gated RM firmware paths (construct/promote family) off-hardware with crafted client/channel/context objects so the first missing state is named without a hardware cycle; trigger = a wall where rehost stops with no state attribution. |
+| Measurable outcome | per seed: one `greeneye-state-seed-report/1` with layout, `unknown_fields[]`, ordered `state_reads[]` and a `gate` (address + state checked); sessions per attempt; wall time per run. |
+| Baseline | cold rehost of the same entry with no objects (docs/46 §3): stop with `0x40`/illegal-at-0 and no attribution, so the next object is a guess; the alternative for an attributed answer is a supervised construct cycle ([live campaign log PART 96 Add. D, 2026-10-06]: `-110` + NOCAT, FLR recovery). |
+| Evaluation threshold | ≥1 wall-class question answered with the gate identified in ≤1 session per attempt, two-run deterministic, zero hardware cycles; seed loop ≤2 min per object. Measured: 0.07–0.08 s per run, 0.8 s demo. |
+| Failure signal | a seeded run cannot reach any gate (faults in the loader itself), or the same gate reproduces with two different seeds and the read state cannot be attributed -> record the gap here and fall back to static + supervised capture. |
+
+## 7. Proven vs hypothesised
+
+**Proven by this wave (sessions/reports are the evidence):**
+
+* the loader places objects, pointers, raw memory, stubs and registers
+ byte-exact and deterministically (unit tests and reports);
+* the FECS predicate verdicts are reproduced through the seed with the slot
+ value as the only variable;
+* the construct prologue's ordered state reads are real (watchpoint
+ first-touch on the seeded fields), and the first gate for the minimal seed
+ is the `client+0x1de8` -> `+0x600` indirect call;
+* stubbing that callback advances execution to the `channel+0x898` gate,
+ which proves the fixture can iterate the object set.
+
+**Hypothesised / not claimed:**
+
+* the object **type names** `client`, `channel`, `graphics_context` are
+ inferred from the promote predicate and the construct call shape, not from a
+ live RM object dump;
+* fields marked `unknown` in §4 have no established semantics, and their
+ values in the demo seeds are 0 (or a stub), not real object state;
+* the real function behind `engine_node+0x600` and `channel+0x898` is not
+ known; the return-0 stubs are synthetic probes, never product code;
+* no timing, scheduling or engine-side behaviour is claimed (L0/L1 only).
+
+## 8. Minimal object set still missing
+
+To walk further than the second gate, the fixture still needs (in order):
+
+1. the **`engine_node` object** (`client+0x1de8`) with a real or recorded
+ `+0x600` callback (first gate);
+2. the **`channel+0x898` callback** reached inside `0x5285aa8` (second gate);
+3. the **`obj_1cc8`** object with `+0x150` (a valid object pointer) and
+ `+0x1d0` flags, and **`obj_1aa8`** with its `+0x24c` callback (next gates
+ after the lookup);
+4. **`obj_1e48`** with its `+0x110` callback on the no-engine path;
+5. **memdesc field semantics** (`+0x0`, `+0x8`, ...) for paths that actually
+ read a memdesc rather than only checking non-NULL;
+6. the **`channel+0x0d01` / `+0x0cd1` / `+0x0648` / `+0x03b8` / `+0x00ee`**
+ fields once a path reaches the lookup-result and invalid-state branches.
+
+The function-pointer fields are the load-bearing gap: they must come from a
+live/B4 object dump or a capture, because a seeder must not invent code
+addresses. Recorded return-0 stubs are the bounded way to cross a gate and
+name the next one, and are always labelled as stubs in the report.
+
+## 9. How to author a new seed
+
+1. Start from the target's entry and the known field map (§4); write the
+ smallest object set the entry's prologue reads, marking everything else
+ `unknown`.
+2. Give each object a name; wire pointer fields with `"@name"`; set the entry
+ registers (`a0`/`a1`/...) and `stack`.
+3. `validate` the seed to see the layout and the `unknown_fields` list; check
+ nothing overlaps and every pointer resolved.
+4. `run` it (add `--out` to keep the session + report). Read `state_reads[]`
+ to confirm each object field was actually consumed, then read `gate` to get
+ the blocker.
+5. If the gate is a callback, re-run with that field pointed at a named stub
+ to reveal the next gate; repeat until the required object cannot be
+ authored without a capture. The remaining `unknown_fields` are the
+ minimal missing object set for the next session.
+
+## 10. Limits
+
+* Not a boot and not an RM model: only the seeded state exists; the report
+ lists it and every limit inline.
+* A fault is the first missing state observed, not a product defect; a
+ timeout/budget is not a verdict.
+* Watchpoints report the first touch of each watched field (repeated accesses
+ are not replayed); the ordered first-touch list is the gate evidence.
+* PA = VA here (flat memory, inactive MPU); a seed that needs a translated
+ view must say so explicitly.
+* No timing/cycle/security claim; L0/L1 fidelity only.