summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/52-RM-OBJECT-STATE-SEEDING.md
blob: 1dc6203a6287c4847555dfa67dc9e2188dff7f14 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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.