# 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 | | `_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.