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
|
# 29 — Packaging and trialing: sim + injected code as a deliverable
> Status: scaffolding implemented and tested; this surface remains an unadmitted pilot (audit: docs/27-TOOL-AUDIT.md; value demos: docs/25-VALUE-VALIDATION.md).
> This is a development-only convenience layer over docs/12 (injection SDK),
> docs/16 (native simulator) and docs/24 (device map). It makes no hardware,
> timing or security claim; the real `.fwimage` path is honestly blocked on
> H2.2 and says so in every result.
## 1. Purpose
risc-v-gsp-rm-debugger can already load an injected service into the native simulator and
invoke it (docs/12, docs/16 §1.1, `harness/value_demos/s06_inject_service.py`).
That evidence lives in the tool tree: a reviewer needs the checkout, the built
demo and SDK, and the exact command sequence. `harness/package_sim.py` turns
"the s06 scenario" into a self-contained, hash-addressed package that a third
party can run, inspect and compare:
* one directory (or deterministic `.tar.gz`) containing the target, the service
blob, the scenario and a manifest of hashes;
* one entry point (`run.sh`) that boots the target **natively**, registers the
injected service, invokes it directly and through the firmware dispatcher,
runs a hook/watch pass, and writes `results.json` + `results.md`;
* a machine-readable manifest so a recipient can verify exactly which inputs
produced a result before trusting it.
It exists to make a code addition trialable and reviewable, not to ship
anything: the eventual target is a modified, **unsigned** GSP image for the
simulator with tested injected code (§7).
## 2. Exact usage
All commands run from `open-iov/debug/risc-v-gsp-rm-vm`. Prerequisites:
`make demo && make sdk` (or pass `--service`), Python 3.10+; no QEMU needed.
```bash
# 1) Demo trial (defaults: demo uproc + sdk/build/injected_demo.elf, slot 6)
python3 harness/package_sim.py --out /tmp/greeneye-trial-demo
# 2) Build, archive and immediately run the trial
python3 harness/package_sim.py --out /tmp/greeneye-trial-demo --tar --run
/tmp/greeneye-trial-demo/run.sh # or run it yourself
# 3) Combined artifact: the service is pre-injected at a fixed offset
python3 harness/package_sim.py --pack-image --out /tmp/greeneye-trial-packed
python3 harness/package_sim.py --pack-image --tar --run --out /tmp/greeneye-trial-packed
# 4) Real 570.144 fwimage: image + service + lenient device map (blocked, see §5)
python3 harness/package_sim.py --target image \
--image /path/to/gsp-570.144.fwimage.bin \
--device-map dumps/570.144-device-map.json \
--out /tmp/greeneye-trial-image
# 5) Custom scenario and slot
python3 harness/package_sim.py --script my_scenario.py --slot 6 --out /tmp/my-trial
# 6) Tests
python3 -m unittest tests.test_package_sim -v # 6 tests, ~4 s
```
Measured on the reference host (2026-10-02, CPython 3.12): a demo build plus
`run.sh` reports `trial: ok` in ~1 s wall; the test file (6 tests, including
the real-image bounded probe) runs in ~4.1 s.
Flags: `--target demo|image` (default demo), `--image`, `--service`,
`--device-map`, `--script`, `--slot 0..7` (default 6), `--pack-image`,
`--tar`, `--run`, `--run-timeout`. `--out` must be empty or absent; builds do
not overwrite.
## 3. Package layout and manifest schema
```
<out>/
README.md what this is, how to run, what to expect, safety
run.sh single entry; finds the tool root (GREENEYE_HOME) and runs scenario.py
manifest.json format/version + hashes + component list + run contract
scenario.py the pre-scripted scenario as run (builtin or --script)
payload/
<target>.elf|fwimage the target as supplied
injected_demo.elf the service blob
device_map.json only for image packages when no --device-map was given
combined.elf only with --pack-image
preinject.json only with --pack-image (placement config)
results/ produced by run.sh: results.json, results.md, session/
```
`manifest.json` (`greeneye-trial/1`):
| Key | Contents |
|---|---|
| `format`, `greeneye_version`, `created`, `results_schema` | identity; `created` is the only timestamp |
| `target` | `kind` (demo/image), `file`, `input` (repo-relative when possible), `sha256`, `entry`, `min_vaddr`, `max_vaddr`, `bias`, `runnable`, `limitations` |
| `service` | `file`, `input`, `sha256`, `entry`, `entry_symbol` (`ge_demo_service_dispatch`), `slot` |
| `device_map` | `file`, `input`, `sha256`, `lenient`, `learn` (null for a plain demo) |
| `script` | `kind` (builtin/custom), `name`, `input`, `sha256` |
| `preinject` | `file`, `runnable`, `reason`, `target_load_addr`, `target_entry`, `service_load_addr`, `service_offset`, `service_entry`, `entry_addr`, `slot`, `size`, `segments` (null without `--pack-image`) |
| `scenario` | demo boot/settle seconds, image instruction budget, `memory_mb`, expected call values |
| `run` | `command`, expected result paths, `exit_nonzero_on: "assertion failure"` |
| `safety`, `determinism` | policy text carried into the package |
| `components` | sorted `{path, role, bytes, sha256}` for every packaged file except the manifest itself |
`results.json` (`greeneye-trial-results/1`): `status` (`ok`/`failed`/
`blocked`), `success`, `backend`, `target`, `service` (load/entry addresses,
`preinjected`), `calls` (remote vs firmware with expected/agree), `hooks`
(`rpc_dispatch` hits and first entries), `watches` (`gsp_ctrl.result` hits),
`control`, `handlers_slot`, `uart_excerpt`, `session`, `assertions`
(`checks`, `failed`), `limitations`, plus `error` and the blocked fields
(§5) where applicable. `results.md` renders the same.
## 4. How a user trials it and reads results
1. Receive `<package>.tar.gz` plus its sha256 out of band; verify
`sha256sum <package>.tar.gz`.
2. Extract and run: `./run.sh` (add `--budget N` for image packages). The
script finds the risc-v-gsp-rm-debugger tree by walking up, or via
`GREENEYE_HOME=/path/to/risc-v-gsp-rm-vm`. Exit codes: `0` pass (or the
documented blocked image status), `1` assertion failure, `3` tree not found.
3. Read `results/results.md` first, then `results/results.json` for machines.
Anything other than `status: ok` on a demo package is a finding, not a
flake: `assertions.failed` names the check (`handlers_slot_registered`,
`remote_ping`, `routes_agree`, `dispatch_hook_hits`, `uart_banner`, ...).
4. For deeper review, point the tools at `results/session/`:
`./risc-v-gsp-rm-debugger.sh analyze <pkg>/results/session` and
`python3 -m greeneye compare baseline/results/session candidate/results/session`.
5. Verify provenance: each `components[].sha256` must match the packaged file;
`target.sha256`/`service.sha256` identify the inputs.
The demo package asserts: the service is registered in
`gsp_ctrl.handlers[slot]`; direct and mailbox invocations agree with the SDK
ABI (`cmd 1: a0 ^ 0xbeef`, `cmd 2: a0 + a1`); at least one `rpc_dispatch`
hook hit and one `gsp_ctrl.result` watch hit; and the UART banner/self-test
lines are present. A bisected or broken service fails these and exits 1.
## 5. The `--pack-image` path and its honest limits
For the demo, `--pack-image` writes `payload/combined.elf`: the demo's PT_LOAD
segments plus the service's segments relocated to `target_base + 0x01000000`
(the same address `greeneye.inject.load_service` uses by default), merged into
one ELF64. `payload/preinject.json` records the placement, and the scenario
boots the combined ELF, writes the entry into the handler slot and skips the
separate blob load. Round-trip verified in
`tests/test_package_sim.py::test_pack_image_round_trip`: call results are
identical to the load/register path and `service.preinjected` is `true` in
`results.json`. This is the trial starting with the injected code already part
of the simulated system.
For the real `.fwimage`, the package contains the image, the service blob and
a lenient `greeneye-device-map/1` map (given or emitted empty with
`learn: true`). **The native simulator cannot boot it yet**: the H2.2 device
model is absent. The scenario therefore runs a bounded, deterministic lenient
first-touch probe and records `status: blocked`,
`blocked_on_device_model: true`, `instructions_executed`, the stop PC, the
unmodelled MMIO list and (when learning) `results/learned_device_map.json`.
Reference measurement: a 200 000-instruction budget (199 998 executed),
stopped at `pc=0x84009070` (firmware `0x4009070`), exactly one unmodelled
access at `0x84009014` reading `0x2000000000000a00`, matching docs/24 §5. No
service is loaded, because no RM service registration point exists yet (M5) and the default load
address would overwrite the image. Exit is 0 for the documented blocked
status; a thrown error is recorded and exits 1.
## 6. Sharing the results of a code addition
1. Author builds the package with the service under review
(`service.sha256` is the code addition's identity) and runs it.
2. Attach `manifest.json`, `results/results.json`, `results/results.md` and
the `results/session/` directory; pre-injection evidence is in the session
as `service_injected`, `call_enter`/`call_return`, `watch_hit` and `bp_hit`
events, so the claim is replayable.
3. A baseline package with the previous service is diffed with
`./risc-v-gsp-rm-debugger.sh compare`; the delta is the behavior change.
4. A reviewer re-runs the received package on the tool tree, verifies hashes
and expects the same `status`/call results. Divergence is a finding against
the package or the tree, not a nuance.
5. Nothing here is product evidence by itself: any conclusion that would
change `open-iov-vfio` still escalates to a physical capture (docs/06 §6,
docs/25 §2.1). The package makes that physical confirmation cheap, it does
not replace it.
## 7. Eventual path: a modified GSP image for the simulator
Once H2.2 can boot the real image (docs/16 M4) and the RM service registry
exists (M5), the same pipeline packages a **modified, unsigned** image:
1. take the original `.fwimage` (hash in `target.sha256`); never a signed
production image, never a copy that can be flashed;
2. record the patch/injection recipe (`preinject`) and the verified device map
as versioned profile data keyed by `(device, firmware-family, profile)` per
docs/16 §7;
3. run the package's scenario, which boots the image under the H2.2 device
model, registers the tested service with the simulated RM core, and asserts
its behavior end to end;
4. attach the session as evidence and escalate any product-relevant finding to
hardware.
Until M4/M5, image packages are deliberately non-runnable and labelled so; the
`preinject.runnable: false` + `reason` fields and the blocked result keep the
gap visible instead of implying a boot.
## 8. Security and safety
* The package executes only inside the in-process simulator: no hardware
access, no mdev, no guest, no kernel module, no network. `run.sh` invokes
Python in the tool tree found locally.
* Injected code runs with the simulator's full privileges and may corrupt
simulated firmware state (docs/12 §9). Treat every package as untrusted
code: run only packages whose sha256 and `manifest.json` you have verified,
in a scratch copy of the tree, and restart clean after a crash.
* Never use a package to inject into a guest, a shipped environment or real
hardware; never sign, flash or install a modified `.fwimage`.
* `/tmp` outputs are scratch; packages and sessions contain firmware bytes and
are development artifacts, not for redistribution outside the project.
## 9. Review/evaluation criteria (keep vs prune)
Keep the feature only while it earns its maintenance. This is an **unadmitted
pilot** under `eng-ops.md` §2: the evaluation recorded in
[`future-eng-ops.md`](../../../docs/future-eng-ops.md) F4 is the admission
decision. It is worth keeping if:
* `python3 -m unittest tests.test_package_sim` stays green in CI in well under
a minute, including the bounded real-image probe;
* two builds under a fixed `SOURCE_DATE_EPOCH` are byte-identical
(manifest and `.tar.gz`) and an intentionally broken service fails the
assertions and exits 1 (add that negative test if it is missing);
* at least one code addition was actually reviewed from a package by someone
who did not author it, with hashes verified and results reproduced;
* the tar carries enough provenance (`manifest.components`, target/service/
device-map/script hashes, `preinject` placement) for a reviewer to trust a
result without touching the author's checkout;
* the image path converts into a real H2.2 boot when M4/M5 land, and its
`blocked` status is removed rather than quietly left in place.
Prune or rework it if: packages are never shared or reviewed (the s06 demo is
enough); the M5 native service registry subsumes `--pack-image`; the manifest
format churns without consumers; the real-image path stays permanently blocked
and adds only maintenance; or package results are being quoted as product or
hardware evidence.
Evidence for this document: `harness/package_sim.py`,
`tests/test_package_sim.py` (6 tests), docs/12, docs/16 §1.1, docs/24 §4–5,
docs/25 §5–6.
<!-- change (2026-10-02): use the tree's stdlib unittest runner instead of an
undeclared pytest dependency, and state the unadmitted-pilot status with the
future-eng-ops F4 evaluation; scope: §2 command, §9 only; benefit: the test
command runs with the same interpreter as CI and the keep/prune decision is
linked. -->
|