diff options
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.md | 297 |
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. |
