# 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 ``` / 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/ .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 `.tar.gz` plus its sha256 out of band; verify `sha256sum .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 /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 `vgsp` 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.