summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/harness
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/harness')
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.json289
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.md102
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/re-demo-report.md116
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/re_demo.py832
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/re_demo/result.json454
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py1247
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/triage.py875
7 files changed, 3915 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.json b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.json
new file mode 100644
index 0000000..4df3593
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.json
@@ -0,0 +1,289 @@
+{
+ "above_qemu_line": true,
+ "features": {
+ "features": {
+ "analyze_compare": {
+ "detail": {
+ "analyze_chars": 444,
+ "analyze_session": "harness/qemu_line/sessions/native-1",
+ "compare_chars": 526,
+ "compare_headers": [
+ "Breakpoint profile delta (A vs B)",
+ "Watchpoint delta",
+ "Written pages delta"
+ ],
+ "compare_pair": [
+ "harness/qemu_line/sessions/native-1",
+ "harness/qemu_line/sessions/native-2"
+ ]
+ },
+ "status": "pass"
+ },
+ "breakpoints": {
+ "detail": {
+ "attached_then_removed": true,
+ "kind": "sw",
+ "run_until_hit": 2147483912
+ },
+ "status": "pass"
+ },
+ "hooks": {
+ "detail": {
+ "cmds": [
+ 1,
+ 2
+ ],
+ "rpc_dispatch_hits": 1
+ },
+ "status": "pass"
+ },
+ "injection": {
+ "detail": {
+ "blob": "sdk/build/injected_demo.elf",
+ "dir": "harness/qemu_line/sessions/features-inject",
+ "expected": 48814,
+ "hashes": {
+ "events.jsonl": "08ab0874cfad71e0e4565b569d9b0ea5251bf158eaab3cdf17798182e4c66a95",
+ "session.json": "f34b677be0b0b453cdade4487118e059c3dfb5a1010d4e6c2fc8b37a867fb25e",
+ "summary.md": "59a1aa4f2b616d5266d565c4df4b0257d11aab95b09cec80f380e9aca8bfc22f"
+ },
+ "ping_result": 48814,
+ "slot": 6
+ },
+ "status": "pass"
+ },
+ "mcp": {
+ "reason": "native MCP surface is S3's deliverable (plugins/mcp_server.py); S4 must not race that work. Re-run after S3 lands (docs/37).",
+ "status": "not_run"
+ },
+ "memory": {
+ "detail": {
+ "ctrl": 2147485760,
+ "magic": 1196642353
+ },
+ "status": "pass"
+ },
+ "sessions": {
+ "detail": {
+ "dir": "harness/qemu_line/sessions/features",
+ "event_counts": {
+ "bp_hit": 2,
+ "hook": 1,
+ "image_loaded": 1,
+ "mem_read": 2,
+ "mem_write": 4,
+ "session_end": 1,
+ "session_start": 1,
+ "stop": 2,
+ "watch": 1,
+ "watch_hit": 1
+ },
+ "hashes": {
+ "events.jsonl": "a6328cb00287bdf678d9b62d496929fc59190a08af3910b059c840c6bc98462e",
+ "session.json": "2372eea627f31d11f114019759d8d6dc665683bbc88e834f158f56b16fa5c0d4",
+ "summary.md": "6dfca9e61a4f23374b90dbef995d86771130fcf4c2b61dc40aac6d379d4b8706"
+ }
+ },
+ "status": "pass"
+ },
+ "struct": {
+ "detail": {
+ "fields": 14,
+ "schema": "gsp_ctrl.schema.json"
+ },
+ "status": "pass"
+ },
+ "trace": {
+ "detail": {
+ "blob": "harness/qemu_line/sessions/features-trace/blobs/trace.jsonl",
+ "dir": "harness/qemu_line/sessions/features-trace",
+ "events": 4001,
+ "hashes": {
+ "events.jsonl": "36b80ed88205daa8e1bd58536eb274ed4849c3d4b1a05daa0f2c44382c7b764a",
+ "session.json": "a09ec38202c291a5b7a3719434ea785725e291cba2dc877d94cd0caf7fcd1a4e",
+ "summary.md": "10686e9f247862f4b0f2c68a2353821f4039befdd76846f8daefa72ae79f4b44"
+ },
+ "max_events": 4000
+ },
+ "status": "pass"
+ },
+ "watch": {
+ "detail": {
+ "pcs": [
+ 2147484066
+ ],
+ "result_field_stores": 1
+ },
+ "status": "pass"
+ }
+ },
+ "ran": true,
+ "reason": "ok"
+ },
+ "firmware": {
+ "name": "gsp-570.144.fwimage.bin",
+ "sha256": "2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea"
+ },
+ "harness": "harness/sim_qemu_line.py",
+ "line_definition": "docs/35-GREENEYE-SIMULATOR-DESIGN.md 1.1 + 3.3",
+ "native": {
+ "backend": "native",
+ "compare": {
+ "chars": 526,
+ "headers": [
+ "Breakpoint profile delta (A vs B)",
+ "Watchpoint delta",
+ "Written pages delta"
+ ],
+ "ran": true
+ },
+ "device_model_used": true,
+ "identical": true,
+ "instructions_budget": 200000,
+ "pair_wall_s": 0.3093,
+ "profile": "sim/profiles/tu104-570.144.json",
+ "ran": true,
+ "reason": "ok",
+ "repeat": 2,
+ "rung": "B2",
+ "rung_basis": "observed S1 boot-ladder rung from sim.gsp_boot (milestones=['bootargs_consumer', 'relocation_copy'], mmio_unknown=0, device_unhandled=0)",
+ "runs": [
+ {
+ "cycle_model": "rv64imac-modeled/1",
+ "cycles": 27294,
+ "device_accesses": 1,
+ "device_state_sha256": "3fa264b9f81081e8455a9cd26c0c2a721e3ac272e1079105a0eb191b1497f83d",
+ "device_unhandled": 0,
+ "first_unknown_mmio": null,
+ "instructions": 14682,
+ "milestones": [
+ "bootargs_consumer",
+ "relocation_copy"
+ ],
+ "mmio_unknown": 0,
+ "rung": "B2",
+ "rung_basis": "observed S1 boot-ladder rung from sim.gsp_boot (milestones=['bootargs_consumer', 'relocation_copy'], mmio_unknown=0, device_unhandled=0)",
+ "session": "harness/qemu_line/sessions/native-1",
+ "session_hashes": {
+ "events.jsonl": "f1f336af84b750b0c8bf8f8e9a7a3952161eb884418f5580f08cb3e26a070713",
+ "session.json": "c756f8078812aa1ed03628e097ee517320d7de3664089f1133861c96130d5b5b",
+ "summary.md": "c0aa5d5b2cb306fda703a7144af455c36fe72b9d1e2c73ddcdab905d36cdc8a7"
+ },
+ "stop": {
+ "pc": 67168780,
+ "pc_fw": 67168780,
+ "reason": "fault"
+ },
+ "wall_s": 0.1561
+ },
+ {
+ "cycle_model": "rv64imac-modeled/1",
+ "cycles": 27294,
+ "device_accesses": 1,
+ "device_state_sha256": "3fa264b9f81081e8455a9cd26c0c2a721e3ac272e1079105a0eb191b1497f83d",
+ "device_unhandled": 0,
+ "first_unknown_mmio": null,
+ "instructions": 14682,
+ "milestones": [
+ "bootargs_consumer",
+ "relocation_copy"
+ ],
+ "mmio_unknown": 0,
+ "rung": "B2",
+ "rung_basis": "observed S1 boot-ladder rung from sim.gsp_boot (milestones=['bootargs_consumer', 'relocation_copy'], mmio_unknown=0, device_unhandled=0)",
+ "session": "harness/qemu_line/sessions/native-2",
+ "session_hashes": {
+ "events.jsonl": "d0d15816dbde8c85c30ba2576f97c9d5e3f470e6c38d9b4762d266a612cafe12",
+ "session.json": "8c7f0ea958a930edb814690bc0b233be01fcab3f747332abff39989e66642c2e",
+ "summary.md": "d2d6154ec22f4ee0d942543a32c29699ccb8031188df955c415d836462d04fd3"
+ },
+ "stop": {
+ "pc": 67168780,
+ "pc_fw": 67168780,
+ "reason": "fault"
+ },
+ "wall_s": 0.1524
+ }
+ ]
+ },
+ "not_verified": [
+ "lenient MMIO (B0) is observation, not device semantics",
+ "B3/B4 are not reached: the S1 device model stops at the B2 DMEM-runspace unknown",
+ "the boot shim values (bootargs/WPR meta/DMEM mirror) are declared assumptions",
+ "MCP on native is S3-owned and not exercised here",
+ "L2 timing: cycles are modeled (docs/32), never silicon claims",
+ "no hardware/GPU/guest/module involvement by design"
+ ],
+ "qemu": {
+ "backend": "qemu",
+ "device_map_entries": 2,
+ "fault_count_window": 5269,
+ "first_fault": {
+ "access_addr": 2305843009213696512,
+ "cause": 5,
+ "desc": "fault_load",
+ "epc": 2214629396,
+ "epc_fw": 67145748,
+ "reason": "rejected",
+ "rw": "read",
+ "size": 4
+ },
+ "first_touch_match": true,
+ "log_window_capped": true,
+ "ran": true,
+ "reason": "ok",
+ "rung": "pre-B0",
+ "session": "harness/qemu_line/sessions/qemu-probe",
+ "session_hashes": {
+ "events.jsonl": "cdba69c63e306f4c3de220b171e2ea7c1de4bd4abf18cdba1fb51bc681d5d172",
+ "session.json": "7e3f0f8b24c0403d114e59f68ed1b923e44abbfbf19f4865bd566a1b631c64ac",
+ "summary.md": "d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b"
+ },
+ "stopped_pc": 0,
+ "wall_s": 0.9519
+ },
+ "rerun_when_s1_lands": [
+ "python3 harness/sim_qemu_line.py",
+ "S1 landed (2026-10-03): sim/profiles/*.json is discovered and the native side runs sim.gsp_boot",
+ "sim/devices/*.json with greeneye-device-map/1 (or a device_map key) is still picked up as an observational map"
+ ],
+ "rung_order": [
+ "pre-B0",
+ "B0",
+ "B1",
+ "B2",
+ "B3",
+ "B4",
+ "B5"
+ ],
+ "rung_progress": true,
+ "s1_integration": {
+ "declared_rung": null,
+ "notes": [
+ "using sim/profiles/tu104-570.144.json (greeneye-sim-profile/1, firmware_sha256 matched)"
+ ],
+ "profile": "sim/profiles/tu104-570.144.json",
+ "source": "sim/profiles/tu104-570.144.json"
+ },
+ "schema": "greeneye-sim-qemu-line/1",
+ "threshold_checks": {
+ "all": true,
+ "device_model_documented": true,
+ "identical_runs": true,
+ "native_above_qemu": true,
+ "native_pair_wall_s": true,
+ "qemu_first_fault_reproduced": true
+ },
+ "thresholds": {
+ "device_model_required": true,
+ "frozen_on": "2026-10-03",
+ "identical_runs": 2,
+ "native_pair_wall_s": 20.0,
+ "native_rungs_above_qemu": 1,
+ "qemu_first_fault_fw": 67145748,
+ "qemu_first_touch": 2305843009213696512
+ },
+ "verdict": "above",
+ "verdict_explanation": "all frozen checks passed: QEMU fault reproduced, native >= QEMU + 1 rung, identical runs, wall bound, documented device model",
+ "x86_vms_running": 0
+}
diff --git a/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.md b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.md
new file mode 100644
index 0000000..4e546b3
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/qemu_line/sim-qemu-line.md
@@ -0,0 +1,102 @@
+# Simulator vs the QEMU line - measured report
+
+* schema: `greeneye-sim-qemu-line/1`
+* harness: `harness/sim_qemu_line.py`
+* line: docs/35-GREENEYE-SIMULATOR-DESIGN.md 1.1 + 3.3
+* firmware: `gsp-570.144.fwimage.bin` sha256 `2d1bb5c60b208cfd...`
+* verdict: **above** (above_qemu_line=True)
+* verdict meaning: all frozen checks passed: QEMU fault reproduced, native >= QEMU + 1 rung, identical runs, wall bound, documented device model
+
+## Rung table
+
+| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence |
+|---|---|---|---:|---:|---|---:|---|
+| QEMU probe | True | pre-B0 | - | - | fault | 0.9519 | `harness/qemu_line/sessions/qemu-probe` |
+| native run 1 | True | B2 | 14682 | 27294 | fault@0x400ea0c | 0.1561 | `harness/qemu_line/sessions/native-1` |
+| native run 2 | True | B2 | 14682 | 27294 | fault@0x400ea0c | 0.1524 | `harness/qemu_line/sessions/native-2` |
+
+### QEMU first fault
+
+* fault_load at fw vaddr `0x4009014` accessing `0x2000000000000a00` (4 bytes, read)
+* first-touch matches the documented reference: True
+* fault records in the capped log window: 5269
+
+## Frozen thresholds
+
+| check | pass |
+|---|---|
+| qemu_first_fault_reproduced | True |
+| native_above_qemu | True |
+| identical_runs | True |
+| native_pair_wall_s | True |
+| device_model_documented | True |
+| all | True |
+
+Thresholds (frozen 2026-10-03): native >= QEMU + 1 rung(s); 2 identical runs; pair wall <= 20.0 s.
+
+## Determinism
+
+* identical across 2 native runs: True
+* session compare ran: True (['Breakpoint profile delta (A vs B)', 'Watchpoint delta', 'Written pages delta'])
+
+## Session hashes (sha256)
+
+| session | file | sha256 |
+|---|---|---|
+| qemu-probe | events.jsonl | `cdba69c63e306f4c3de220b171e2ea7c1de4bd4abf18cdba1fb51bc681d5d172` |
+| qemu-probe | session.json | `7e3f0f8b24c0403d114e59f68ed1b923e44abbfbf19f4865bd566a1b631c64ac` |
+| qemu-probe | summary.md | `d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b` |
+| native-1 | events.jsonl | `f1f336af84b750b0c8bf8f8e9a7a3952161eb884418f5580f08cb3e26a070713` |
+| native-1 | session.json | `c756f8078812aa1ed03628e097ee517320d7de3664089f1133861c96130d5b5b` |
+| native-1 | summary.md | `c0aa5d5b2cb306fda703a7144af455c36fe72b9d1e2c73ddcdab905d36cdc8a7` |
+| native-2 | events.jsonl | `d0d15816dbde8c85c30ba2576f97c9d5e3f470e6c38d9b4762d266a612cafe12` |
+| native-2 | session.json | `8c7f0ea958a930edb814690bc0b233be01fcab3f747332abff39989e66642c2e` |
+| native-2 | summary.md | `d2d6154ec22f4ee0d942543a32c29699ccb8031188df955c415d836462d04fd3` |
+| sessions | events.jsonl | `a6328cb00287bdf678d9b62d496929fc59190a08af3910b059c840c6bc98462e` |
+| sessions | session.json | `2372eea627f31d11f114019759d8d6dc665683bbc88e834f158f56b16fa5c0d4` |
+| sessions | summary.md | `6dfca9e61a4f23374b90dbef995d86771130fcf4c2b61dc40aac6d379d4b8706` |
+| trace | events.jsonl | `36b80ed88205daa8e1bd58536eb274ed4849c3d4b1a05daa0f2c44382c7b764a` |
+| trace | session.json | `a09ec38202c291a5b7a3719434ea785725e291cba2dc877d94cd0caf7fcd1a4e` |
+| trace | summary.md | `10686e9f247862f4b0f2c68a2353821f4039befdd76846f8daefa72ae79f4b44` |
+| injection | events.jsonl | `08ab0874cfad71e0e4565b569d9b0ea5251bf158eaab3cdf17798182e4c66a95` |
+| injection | session.json | `f34b677be0b0b453cdade4487118e059c3dfb5a1010d4e6c2fc8b37a867fb25e` |
+| injection | summary.md | `59a1aa4f2b616d5266d565c4df4b0257d11aab95b09cec80f380e9aca8bfc22f` |
+
+## Native feature matrix (demo firmware)
+
+| feature | status | detail |
+|---|---|---|
+| analyze_compare | pass | {'analyze_session': 'harness/qemu_line/sessions/native-1', 'analyze_chars': 444, 'compare_pair': ['harness/qemu_line/sessions/native-1', 'harness/qemu_line/sessions/native-2'], 'compare_chars': 526, 'compare_headers': ['Breakpoint profile delta (A vs B)', 'Watchpoint delta', 'Written pages delta']} |
+| breakpoints | pass | {'run_until_hit': 2147483912, 'attached_then_removed': True, 'kind': 'sw'} |
+| hooks | pass | {'rpc_dispatch_hits': 1, 'cmds': [1, 2]} |
+| injection | pass | {'dir': 'harness/qemu_line/sessions/features-inject', 'blob': 'sdk/build/injected_demo.elf', 'slot': 6, 'ping_result': 48814, 'expected': 48814, 'hashes': {'session.json': 'f34b677be0b0b453cdade4487118e059c3dfb5a1010d4e6c2fc8b37a867fb25e', 'events.jsonl': '08ab0874cfad71e0e4565b569d9b0ea5251bf158eaab3cdf17798182e4c66a95', 'summary.md': '59a1aa4f2b616d5266d565c4df4b0257d11aab95b09cec80f380e9aca8bfc22f'}} |
+| mcp | not_run | native MCP surface is S3's deliverable (plugins/mcp_server.py); S4 must not race that work. Re-run after S3 lands (docs/37). |
+| memory | pass | {'ctrl': 2147485760, 'magic': 1196642353} |
+| sessions | pass | {'dir': 'harness/qemu_line/sessions/features', 'event_counts': {'bp_hit': 2, 'hook': 1, 'image_loaded': 1, 'mem_read': 2, 'mem_write': 4, 'session_end': 1, 'session_start': 1, 'stop': 2, 'watch': 1, 'watch_hit': 1}, 'hashes': {'session.json': '2372eea627f31d11f114019759d8d6dc665683bbc88e834f158f56b16fa5c0d4', 'events.jsonl': 'a6328cb00287bdf678d9b62d496929fc59190a08af3910b059c840c6bc98462e', 'summary.md': '6dfca9e61a4f23374b90dbef995d86771130fcf4c2b61dc40aac6d379d4b8706'}} |
+| struct | pass | {'schema': 'gsp_ctrl.schema.json', 'fields': 14} |
+| trace | pass | {'dir': 'harness/qemu_line/sessions/features-trace', 'blob': 'harness/qemu_line/sessions/features-trace/blobs/trace.jsonl', 'events': 4001, 'max_events': 4000, 'hashes': {'session.json': 'a09ec38202c291a5b7a3719434ea785725e291cba2dc877d94cd0caf7fcd1a4e', 'events.jsonl': '36b80ed88205daa8e1bd58536eb274ed4849c3d4b1a05daa0f2c44382c7b764a', 'summary.md': '10686e9f247862f4b0f2c68a2353821f4039befdd76846f8daefa72ae79f4b44'}} |
+| watch | pass | {'result_field_stores': 1, 'pcs': [2147484066]} |
+
+## S1 integration
+
+* device source: sim/profiles/tu104-570.144.json
+* profile: sim/profiles/tu104-570.144.json
+* declared rung: None
+* note: using sim/profiles/tu104-570.144.json (greeneye-sim-profile/1, firmware_sha256 matched)
+
+## Not verified
+
+* lenient MMIO (B0) is observation, not device semantics
+* B3/B4 are not reached: the S1 device model stops at the B2 DMEM-runspace unknown
+* the boot shim values (bootargs/WPR meta/DMEM mirror) are declared assumptions
+* MCP on native is S3-owned and not exercised here
+* L2 timing: cycles are modeled (docs/32), never silicon claims
+* no hardware/GPU/guest/module involvement by design
+
+## S1 integration notes
+
+* `python3 harness/sim_qemu_line.py`
+* S1 landed (2026-10-03): sim/profiles/*.json is discovered and the native side runs sim.gsp_boot
+* sim/devices/*.json with greeneye-device-map/1 (or a device_map key) is still picked up as an observational map
+
+* other agents' x86 QEMU VMs observed (never touched): 0
diff --git a/debug/risc-v-gsp-rm-vm/harness/re-demo-report.md b/debug/risc-v-gsp-rm-vm/harness/re-demo-report.md
new file mode 100644
index 0000000..aa7a116
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/re-demo-report.md
@@ -0,0 +1,116 @@
+# Real RE demo - what the GSP does between entry and the first fault
+
+* schema: `greeneye-re-demo/1`
+* script: `harness/re_demo.py` (stdlib-only, no hardware/guest/module)
+* firmware: `gsp-570.144.fwimage.bin` sha256 `2d1bb5c60b208cfd...`
+* verdict: **PASS**
+
+## Scenario
+
+> What happens on the real GSP image between entry and the first fault/milestone, and what does the state tell us about boot?
+
+## Answer
+
+**q1_first_touch: the first MMIO is a 4-byte read of PA 0x2000000000000a00 at fw PC 0x4009014; it is the QEMU-documented first fault**
+
+**q2_path: entry state is all-zero (no bootloader handoff modelled); the bootstrap builds the device PA with 5 arithmetic instructions (li/slli/addi/slli/addi), reads it, then runs the chip-ID comparison chain: 25 instructions recorded from entry until the halt at fw 0x4009070**
+
+**q3_id_field: the read value's bits [28:20] are extracted (srli 20, andi 0x1ff) and compared against 354..360 (Turing) and 368 (Ampere); the image itself defines this as a chip-ID field**
+
+**q4_dispatch: ID 354..360 dispatches to .tu10x_text at 0x4007000; ID 368 dispatches to .ga100_text at 0x4005000; any other value (including the lenient zero) falls to the fail-closed `j .` loop at 0x4009070**
+
+**q5_next_blocker: with a Turing ID served, .tu10x_text programs CSRs 0x7c8-0x7cc (region descriptors) and enables mstatus FP/SIMD state, then builds the absolute link-time address 0x40070f8 and jumps to it; because the loader relocates segments but not absolute immediates, that jump lands outside RAM and faults - a relocation artifact, so the next rung needs a chip-ID device model plus load-at-link-address/alias translation**
+
+## Baseline vs emulator (measured)
+
+| side | method | wall s | what it answers |
+|---|---|---:|---|
+| baseline | `python3 -m greeneye.cli probe <image> --duration 1.0 (here run via harness.run_qemu_baseline, 0.5 s bound)` | 0.9495 | first fault only |
+| baseline | manual static reading (estimate, floor estimate: ~90 instructions across .bootstrap + .tu10x_text + .ga100_text read by hand, dispatch table identified and correlated with the QEMU first-fault report; not measured in this run) | 600.0 | entry->fault path, by hand |
+| baseline | total (conservative) | 600.9495 | full question |
+| emulator | three recorded native sessions | 0.1251 | full question + evidence |
+
+Per-session `wall_s` in the milestone table is the recorded run only; the emulator total includes session launch, repeat and report writing.
+
+## Rung / milestone table
+
+| run | first touch | stop/milestone | instructions | wall s | evidence |
+|---|---|---|---:|---:|---|
+| QEMU probe | 0x2000000000000a00 (fault) | fault, then mtvec=0 loop | - | 0.9495 | `harness/re_demo/sessions/qemu-probe` |
+| native lenient (B0) | 0x2000000000000a00 | halt loop fw 0x4009070 | 25 | 0.0006 | `harness/re_demo/sessions/native-lenient` |
+| native modeled chip-ID (B1 candidate) | 0x2000000000000a00 | instruction fault at fw 0x40070f8 (outside RAM) | 74 | 0.0004 | `harness/re_demo/sessions/native-modeled-id` |
+
+## Frozen thresholds
+
+| check | pass |
+|---|---|
+| answers_complete | True |
+| deterministic_lenient_runs | True |
+| emulator_wall_s | True |
+| milestone_past_qemu | True |
+| baseline_beat | True |
+| pass | True |
+
+Thresholds frozen 2026-10-03 before the run: emulator wall <= 120.0 s; 2 identical lenient runs; all 5 answers present; milestone past the QEMU point. Failure signal: any check false -> the run is a failed experiment and the feature is not claimed.
+
+## Sessions (evidence)
+
+| session | file | sha256 |
+|---|---|---|
+| qemu-probe | events.jsonl | `8c4dba8006a496d853c81ce9e2b16944cd27b8d6171c01777daa6e5738be02bc` |
+| qemu-probe | session.json | `c7bf5e4d29f534160d660dadb81f81942faa17cbfb1941587c60dcf78180f56c` |
+| qemu-probe | summary.md | `d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b` |
+| native-lenient | events.jsonl | `ccb568588738c0609071ee542c9b04f7bb2868ee8fae12c28b2bfc5d9343721b` |
+| native-lenient | session.json | `ec5944adce4b823a3d6fd7d5aa6385fb5b61c2f9542c7221f46f328601c12c66` |
+| native-lenient | summary.md | `b50820afa60c35da0d4701e54f9907d4b7090c52721e1ecf656942a41bcf44db` |
+| native-lenient-repeat | events.jsonl | `2cd28b4da009447d70505cfbba59e9c1bf8bd277e3a7ea696e2690f54f66f756` |
+| native-lenient-repeat | session.json | `fe21ed52c05436d0f7886dfeb0886ec1a11b25678e4d827db3b9e4009d2187e4` |
+| native-lenient-repeat | summary.md | `357d95a664e33128a2a48da7ecfc058bdbbea56fae130557593fd59ff2efa597` |
+| native-modeled-id | events.jsonl | `fdf0756c3543741b74c9267dc9e4d22156f43a4b54463ffbfb1671c327444515` |
+| native-modeled-id | session.json | `5ae7eeff56a19ed7eea093a164a3a3738965b56c39f35a563db3ee93a2c90d39` |
+| native-modeled-id | summary.md | `bd670d207d23079173b494a9be9f9a0e7d31a10a11f9dc48fb8a134888442d0b` |
+
+## Disassembly of the entry prologue (from the lenient session memory read)
+
+```
+0x84009000: 0010029b addiw t0,zero,1
+0x84009004: 03129293 slli t0,t0,49
+0x84009008: 00128293 addi t0,t0,1
+0x8400900c: 00c29293 slli t0,t0,12
+0x84009010: a0028293 addi t0,t0,-1536
+0x84009014: 0002a283 lw t0,0(t0)
+0x84009018: 0142d293 srli t0,t0,20
+0x8400901c: 1ff2f293 andi t0,t0,511
+```
+
+### Turing dispatch tail (from the modeled session memory read)
+
+```
+0x840070e4: 1e0002b7 lui t0,0x1e000
+0x840070e8: 3002a073 csrs mstatus,t0
+0x840070ec: 040072b7 lui t0,0x4007
+0x840070f0: 0f828293 addi t0,t0,248
+0x840070f4: 00028067 jalr zero,0(t0)
+```
+
+## Not verified (honest limits)
+
+* the modeled chip-ID value is an assumption (the QEMU baseline faults before reading it); only the field position and the compared IDs are firmware evidence
+* no timing/cycle claim: cycles are modeled (L2 not modelled, docs/06) and none are used in this answer
+* L3 device environment is absent: the register's on-silicon semantics are inferred from firmware comparisons, not from a device model
+* load-at-link-address and absolute-immediate relocation behaviour are not implemented; the B1 candidate fault is a relocation artifact of this harness
+* watchpoints are not exercised on this path: the bootstrap performs no RAM data access before the fault (RAM watchpoints are exercised by harness/sim_qemu_line.py on the demo firmware)
+* no hardware, GPU, mdev, module, guest or network involvement by design
+
+## Reproduce
+
+```bash
+cd open-iov/debug/risc-v-gsp-rm-vm
+python3 harness/re_demo.py
+```
+
+## Re-run contract
+
+* thresholds live in harness/re_demo.py (FROZEN_THRESHOLDS) and are not moved after the run
+* the QEMU baseline needs qemu-system-riscv64; without it the emulator columns still reproduce and the baseline is marked unverified
+* sessions are under harness/re_demo/sessions/ (hash-stamped); the report regenerates with the same command
diff --git a/debug/risc-v-gsp-rm-vm/harness/re_demo.py b/debug/risc-v-gsp-rm-vm/harness/re_demo.py
new file mode 100644
index 0000000..3f9c317
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/re_demo.py
@@ -0,0 +1,832 @@
+#!/usr/bin/env python3
+"""Real reverse-engineering demo: GSP entry -> first fault/milestone (U51).
+
+Answers one concrete question on the real TU104 570.144 image with the
+simulator's own primitives (``launch_native`` session, hooks, memory reads,
+snapshots, exec trace, recorded sessions):
+
+ What happens on the real GSP image between entry and the first
+ fault/milestone, and what does the state tell us about boot?
+
+Method (both sides measured, thresholds frozen in this file before any run):
+
+* **baseline** - the QEMU ``probe`` path (bounded 0.5 s, measured) plus a
+ conservative manual static-reading estimate (``MANUAL_ESTIMATE_S``; the
+ method note says how the estimate is derived). QEMU reports the first
+ fault; the "what happens between entry and the fault" part is hand work
+ in a disassembler.
+* **emulator** - three recorded native sessions: (A) lenient MMIO (B0), which
+ reproduces the QEMU first touch and follows the firmware to its fail-closed
+ halt; (A') the same run repeated for determinism; (B) the same run with the
+ chip-ID register served a Turing value from a one-entry device map, which
+ advances past the QEMU point to the next (relocation) blocker.
+
+Everything is stdlib-only, bounded (<= 2M instructions, <= 10 s QEMU), and
+touches no hardware, GPU, guest, module or network. This is simulator
+evidence at L0/L1 (docs/06): no timing or silicon claim.
+
+CLI::
+
+ python3 harness/re_demo.py
+ python3 harness/re_demo.py --json
+ python3 harness/re_demo.py --out harness/re_demo --report harness/re-demo-report.md
+"""
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import shutil
+import sys
+import time
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+TOOL_ROOT = os.path.dirname(HERE)
+if TOOL_ROOT not in sys.path:
+ sys.path.insert(0, TOOL_ROOT)
+
+from greeneye.device_map import DeviceEntry, DeviceMap, QEMU_REFERENCE # noqa: E402
+from greeneye.sim_backend import launch_native # noqa: E402
+from harness.sim_qemu_line import run_qemu_baseline, session_hashes # noqa: E402
+from sim.decode import ( # noqa: E402
+ K_AUIPC,
+ K_BRANCH,
+ K_CSR,
+ K_JAL,
+ K_JALR,
+ K_LOAD,
+ K_LUI,
+ K_OP,
+ K_OP32,
+ K_OPI,
+ K_OPI32,
+ decode16,
+ decode32,
+)
+
+DEFAULT_IMAGE = os.path.normpath(
+ os.path.join(
+ TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", "gsp-570.144.fwimage.bin"
+ )
+)
+DEFAULT_OUT = os.path.join(HERE, "re_demo")
+DEFAULT_REPORT = os.path.join(HERE, "re-demo-report.md")
+
+#: GPU physical addresses are relocated into simulator RAM at this bias
+#: (same convention as the probe/device-map/gsp-profile paths).
+BIAS = 0x80000000
+
+ENTRY = 0x4009000
+FIRST_TOUCH_PC = 0x4009014
+HALT_PC = 0x4009070
+TU10X_ENTRY = 0x4007000
+GA100_ENTRY = 0x4005000
+TU10X_ABS_TARGET = 0x40070F8
+
+#: The chip-ID field is bits [28:20] of the register read at the first touch;
+#: these are the comparison values the image itself uses (session A trace).
+TU10X_IDS = (354, 355, 356, 357, 358, 359, 360)
+GA100_ID = 368
+#: Exploratory modeled register value: Turing TU10x ID in the field position.
+#: This is an *assumption* chosen to exercise the dispatch path, not an
+#: observed hardware value (the QEMU baseline faults before reading it).
+MODELED_ID = TU10X_IDS[0] << 20
+
+#: Frozen thresholds - recorded 2026-10-03, before the first measurement.
+#: Never move silently; if a threshold changes, date the change in docs/37.
+FROZEN_THRESHOLDS = {
+ "frozen_on": "2026-10-03",
+ "emulator_wall_s_max": 120.0,
+ "manual_static_estimate_s": 600.0,
+ "answers_required": 5,
+ "identical_lenient_runs": 2,
+ "milestone_past_qemu": True,
+}
+
+#: How the manual estimate is derived (a floor, not a ceiling): read the
+#: 6-instruction .bootstrap prologue + the two per-architecture text sections
+#: (~90 instructions), identify the pointer construction, the chip-ID bit
+#: field, the two dispatch targets and the halt path, then correlate against
+#: the QEMU fault report. No human fast path is measured; 10 minutes is the
+#: conservative floor for one engineer with the artifacts already open.
+MANUAL_METHOD_NOTE = (
+ "floor estimate: ~90 instructions across .bootstrap + .tu10x_text + "
+ ".ga100_text read by hand, dispatch table identified and correlated "
+ "with the QEMU first-fault report; not measured in this run"
+)
+
+
+def _relpath(path: str | None) -> str | None:
+ if path is None:
+ return None
+ return os.path.relpath(os.path.abspath(path), TOOL_ROOT)
+
+
+def _sha256_file(path: str) -> str:
+ digest = hashlib.sha256()
+ with open(path, "rb") as fh:
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
+ digest.update(chunk)
+ return digest.hexdigest()
+
+
+def _read_json_lines(path: str) -> list[dict]:
+ rows: list[dict] = []
+ try:
+ with open(path, "r", encoding="utf-8") as fh:
+ for line in fh:
+ line = line.strip()
+ if line:
+ rows.append(json.loads(line))
+ except (OSError, ValueError):
+ pass
+ return rows
+
+
+def _trace_exec_pcs(session_dir: str) -> list[int]:
+ rows = _read_json_lines(os.path.join(session_dir, "blobs", "trace.jsonl"))
+ return [int(row["pc"], 16) for row in rows if row.get("t") == "exec"]
+
+
+def _fw(pc: int) -> int:
+ return pc - BIAS if pc >= BIAS else pc
+
+
+# -- ELF section names (stdlib) ----------------------------------------------
+
+
+def elf_progbits_sections(path: str) -> dict:
+ """{name: (addr, size)} for PROGBITS sections, parsed directly from ELF."""
+ with open(path, "rb") as fh:
+ data = fh.read()
+ e_shoff = int.from_bytes(data[0x28:0x30], "little")
+ e_shentsize = int.from_bytes(data[0x3A:0x3C], "little")
+ e_shnum = int.from_bytes(data[0x3C:0x3E], "little")
+ e_shstrndx = int.from_bytes(data[0x3E:0x40], "little")
+
+ def header(index: int):
+ off = e_shoff + index * e_shentsize
+ return (
+ int.from_bytes(data[off : off + 4], "little"), # sh_name
+ int.from_bytes(data[off + 4 : off + 8], "little"), # sh_type
+ int.from_bytes(data[off + 16 : off + 24], "little"), # sh_addr
+ int.from_bytes(data[off + 24 : off + 32], "little"), # sh_offset
+ int.from_bytes(data[off + 32 : off + 40], "little"), # sh_size
+ )
+
+ _, _, _, strtab_off, strtab_size = header(e_shstrndx)
+ strtab = data[strtab_off : strtab_off + strtab_size]
+ sections: dict = {}
+ for index in range(e_shnum):
+ name_off, sh_type, addr, _offset, size = header(index)
+ end = strtab.find(b"\0", name_off)
+ name = strtab[name_off:end].decode("utf-8", "replace") if end >= 0 else ""
+ if sh_type == 1 and size: # SHT_PROGBITS
+ sections[name] = (addr, size)
+ return sections
+
+
+def section_for(sections: dict, fw_addr: int) -> str | None:
+ for name, (base, size) in sections.items():
+ if base <= fw_addr < base + size:
+ return name
+ return None
+
+
+# -- minimal disassembly (decode tuples -> mnemonics) ------------------------
+
+_LOAD_NAMES = ("lb", "lh", "lw", "ld", "lbu", "lhu", "lwu")
+_BRANCH_NAMES = ("beq", "bne", "blt", "bge", "bltu", "bgeu")
+_OPI_NAMES = {0: "addi", 1: "slli", 2: "slti", 3: "sltiu", 4: "xori", 5: "srli", 6: "ori", 7: "andi", 0x15: "srai"}
+_OPI32_NAMES = {0: "addiw", 1: "slliw", 5: "srliw", 0x15: "sraiw"}
+_OP_NAMES = {
+ 0: "add", 1: "sll", 2: "slt", 3: "sltu", 4: "xor", 5: "srl", 6: "or", 7: "and",
+ 8: "sub", 0x15: "sra",
+}
+_OP32_NAMES = {0: "addw", 1: "sllw", 5: "srlw", 8: "subw", 0x15: "sraw"}
+_CSR_NAMES = {0x300: "mstatus"}
+_CSR_OPS = {1: "csrrw", 2: "csrs", 3: "csrc", 5: "csrrwi", 6: "csrsi", 7: "csrci"}
+
+
+def _abi(index: int) -> str:
+ return (
+ "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1",
+ "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7",
+ "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11",
+ "t3", "t4", "t5", "t6",
+ )[index]
+
+
+def disasm(pc: int, raw: int, decoded: tuple) -> str:
+ kind, sub, rd, rs1, rs2, imm, _next = decoded
+ if kind == K_OPI:
+ name = _OPI_NAMES.get(sub, f"opi:{sub}")
+ text = f"{name} {_abi(rd)},{_abi(rs1)},{imm}"
+ elif kind == K_OP:
+ name = _OP_NAMES.get(sub, f"op:{sub}")
+ text = f"{name} {_abi(rd)},{_abi(rs1)},{_abi(rs2)}"
+ elif kind == K_OPI32:
+ name = _OPI32_NAMES.get(sub, f"opi32:{sub}")
+ text = f"{name} {_abi(rd)},{_abi(rs1)},{imm}"
+ elif kind == K_OP32:
+ name = _OP32_NAMES.get(sub, f"op32:{sub}")
+ text = f"{name} {_abi(rd)},{_abi(rs1)},{_abi(rs2)}"
+ elif kind == K_LOAD:
+ name = _LOAD_NAMES[sub] if sub < len(_LOAD_NAMES) else f"load:{sub}"
+ text = f"{name} {_abi(rd)},{imm}({_abi(rs1)})"
+ elif kind == K_BRANCH:
+ name = _BRANCH_NAMES[sub] if sub < len(_BRANCH_NAMES) else f"branch:{sub}"
+ text = f"{name} {_abi(rs1)},{_abi(rs2)},{_next:#x}"
+ elif kind == K_LUI:
+ text = f"lui {_abi(rd)},{imm >> 12:#x}"
+ elif kind == K_AUIPC:
+ text = f"auipc {_abi(rd)},{imm >> 12:#x}"
+ elif kind == K_JALR:
+ text = f"jalr {_abi(rd)},{imm}({_abi(rs1)})"
+ elif kind == K_JAL:
+ text = f"jal {_abi(rd)},{_next:#x}"
+ elif kind == K_CSR:
+ csr = _CSR_NAMES.get(imm, f"0x{imm:x}")
+ name = _CSR_OPS.get(sub, f"csr:{imm:#x}")
+ text = f"{name} {csr},{_abi(rs1)}"
+ else:
+ text = f".raw 0x{raw:08x}"
+ return f"{pc:#x}: {raw:08x} {text}"
+
+
+def read_disasm(session, addr: int, count: int) -> list[str]:
+ lines: list[str] = []
+ for _ in range(count):
+ raw16 = int.from_bytes(session.memory.read(addr, 2), "little")
+ if raw16 & 3 == 3:
+ raw = int.from_bytes(session.memory.read(addr, 4), "little")
+ decoded = decode32(addr, raw)
+ step = 4
+ else:
+ raw = raw16
+ decoded = decode16(addr, raw16)
+ step = 2
+ lines.append(disasm(addr, raw, decoded))
+ addr += step
+ return lines
+
+
+# -- sessions ----------------------------------------------------------------
+
+
+def _launch(image: str, out_dir: str, name: str, device_map, trace_spec):
+ session_dir = os.path.join(out_dir, "sessions", name)
+ shutil.rmtree(session_dir, ignore_errors=True)
+ session = launch_native(
+ kernel=image,
+ record=session_dir,
+ name=name,
+ memory_mb=512,
+ bias=BIAS,
+ device_map=device_map,
+ unknown_mmio="lenient",
+ learn_mmio=device_map is not None,
+ trace=trace_spec,
+ )
+ return session, session_dir
+
+
+def run_lenient_session(image: str, out_dir: str, tag: str = "native-lenient") -> dict:
+ """Session A: lenient MMIO; reproduce the QEMU first touch, follow to halt."""
+ dm = DeviceMap(meta={"source": "re-demo-lenient", "bias": BIAS})
+ session, session_dir = _launch(
+ image,
+ out_dir,
+ tag,
+ dm,
+ {"exec": True, "start": ENTRY + BIAS, "end": HALT_PC + BIAS, "max_events": 256},
+ )
+ start = time.monotonic()
+ entry_regs = session.read_regs()
+ snapshot = session.snapshot(ENTRY + BIAS, 0x78, name="bootstrap")
+ stops: list[dict] = []
+
+ def record(label):
+ def callback(hit):
+ regs = hit.regs
+ stops.append(
+ {
+ "label": label,
+ "pc": hit.pc,
+ "pc_fw": _fw(hit.pc),
+ "t0": regs.get("t0", 0),
+ "t1": regs.get("t1", 0),
+ "t2": regs.get("t2", 0),
+ }
+ )
+ return callback
+
+ session.hook(FIRST_TOUCH_PC + BIAS, on_enter=record("first_touch_pc"))
+ session.hook(HALT_PC + BIAS, on_enter=record("halt"))
+ session.run(duration=0.05, max_stops=2)
+ wall = time.monotonic() - start
+ machine = session.target.machine
+ halt_pc = session.pc
+ disassembly = read_disasm(session, ENTRY + BIAS, 8)
+ session.note(
+ "re_demo_lenient",
+ first_unknown_mmio=machine.mmio_log[0] if machine.mmio_log else None,
+ stop_pc_fw=_fw(halt_pc),
+ instructions=machine.insns,
+ hook_stops=stops,
+ )
+ session.close()
+ trace_pcs = _trace_exec_pcs(session_dir)
+ first_touch = machine.mmio_log[0] if machine.mmio_log else None
+ return {
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "wall_s": round(wall, 4),
+ "entry_regs_zero": all(value == 0 for value in entry_regs.values()),
+ "snapshot_bytes": len(snapshot),
+ "first_unknown_mmio": first_touch,
+ "first_touch_fw_pc": _fw(first_touch["pc"]) if first_touch else None,
+ "first_touch_matches_qemu": bool(
+ first_touch and first_touch["addr"] == QEMU_REFERENCE["access_addr"]
+ ),
+ "hook_stops": stops,
+ "instructions": machine.insns,
+ "stop_pc": halt_pc,
+ "stop_pc_fw": _fw(halt_pc),
+ "trace_exec_count": len(trace_pcs),
+ "trace_path_fw": [_fw(pc) for pc in trace_pcs],
+ "disassembly": disassembly,
+ }
+
+
+def run_modeled_session(image: str, out_dir: str) -> dict:
+ """Session B: serve the chip-ID register and follow the Turing dispatch."""
+ dm = DeviceMap(
+ meta={
+ "source": "re-demo-modeled-chip-id",
+ "bias": BIAS,
+ "note": (
+ "exploratory assumption: register value carries the chip-ID field "
+ f"{TU10X_IDS[0]} in bits[28:20]; not observed (QEMU faults before reading)"
+ ),
+ }
+ )
+ dm.add(
+ DeviceEntry(
+ address=QEMU_REFERENCE["access_addr"],
+ width=4,
+ kind="r",
+ values=(MODELED_ID,),
+ hits=1,
+ first_epc=FIRST_TOUCH_PC + BIAS,
+ first_epc_fw=FIRST_TOUCH_PC,
+ source="manual",
+ notes="assumed Turing TU10x chip ID",
+ )
+ )
+ session, session_dir = _launch(
+ image,
+ out_dir,
+ "native-modeled-id",
+ dm,
+ {"exec": True, "start": 0x84000000, "end": 0x84010000, "max_events": 1024},
+ )
+ start = time.monotonic()
+ jumps: list[dict] = []
+
+ def on_jr(hit):
+ jumps.append(
+ {
+ "pc_fw": _fw(hit.pc),
+ "t0_absolute_target": hit.regs.get("t0", 0),
+ "target_fw": _fw(hit.regs.get("t0", 0)),
+ }
+ )
+
+ stops: list[dict] = []
+ session.on_event(
+ "stop",
+ lambda hit: stops.append(
+ {
+ "pc_fw": _fw(hit.pc),
+ "signal": hit.stop.signal if hit.stop is not None else None,
+ }
+ ),
+ )
+ session.hook(TU10X_ABS_TARGET - 4 + BIAS, on_enter=on_jr)
+ session.run(duration=0.05, max_stops=1)
+ after_jump_pc = session.pc
+ session.run(duration=0.05, max_stops=1)
+ stop_pc = session.pc
+ fault_stop = stops[-1] if stops else None
+ wall = time.monotonic() - start
+ machine = session.target.machine
+ tail_disasm = read_disasm(session, TU10X_ABS_TARGET - 0x14 + BIAS, 5)
+ session.note(
+ "re_demo_modeled",
+ jumps=jumps,
+ after_jump_pc_fw=_fw(after_jump_pc),
+ stop_pc_fw=_fw(stop_pc),
+ instructions=machine.insns,
+ fault_stops=stops,
+ )
+ session.close()
+ trace_pcs = _trace_exec_pcs(session_dir)
+ return {
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "wall_s": round(wall, 4),
+ "modeled_id": MODELED_ID,
+ "assumed_chip_id": TU10X_IDS[0],
+ "jumps": jumps,
+ "instructions": machine.insns,
+ "stop_pc": stop_pc,
+ "stop_pc_fw": _fw(stop_pc),
+ "stop_outside_ram": stop_pc < BIAS,
+ "fault_signal": fault_stop.get("signal") if fault_stop else None,
+ "fault_stops": stops,
+ "disassembly_tail": tail_disasm,
+ "trace_exec_count": len(trace_pcs),
+ "trace_path_fw": [_fw(pc) for pc in trace_pcs],
+ }
+
+
+# -- answers / evaluation / rendering ----------------------------------------
+
+
+def build_answers(lenient: dict, modeled: dict) -> dict:
+ first = lenient.get("first_unknown_mmio") or {}
+ path = lenient.get("trace_path_fw") or []
+ halt_seen = any(
+ stop.get("label") == "halt" for stop in (lenient.get("hook_stops") or [])
+ )
+ answers = {
+ "q1_first_touch": {
+ "answer": (
+ f"the first MMIO is a 4-byte read of PA {first.get('addr'):#x} at fw "
+ f"PC {lenient.get('first_touch_fw_pc'):#x}; it is the QEMU-documented "
+ "first fault"
+ ),
+ "addr": first.get("addr"),
+ "size": first.get("size"),
+ "rw": first.get("rw"),
+ "pc_fw": lenient.get("first_touch_fw_pc"),
+ "matches_qemu_reference": lenient.get("first_touch_matches_qemu"),
+ },
+ "q2_path": {
+ "answer": (
+ f"entry state is all-zero (no bootloader handoff modelled); the "
+ f"bootstrap builds the device PA with 5 arithmetic instructions "
+ f"(li/slli/addi/slli/addi), reads it, then runs the chip-ID "
+ f"comparison chain: {lenient.get('trace_exec_count')} instructions "
+ f"recorded from entry until the halt at fw "
+ f"{lenient.get('stop_pc_fw'):#x}"
+ ),
+ "entry_regs_zero": lenient.get("entry_regs_zero"),
+ "instructions_to_halt": lenient.get("instructions"),
+ "halt_pc_fw": lenient.get("stop_pc_fw"),
+ "halt_seen": halt_seen,
+ "trace_path_fw": path,
+ },
+ "q3_id_field": {
+ "answer": (
+ "the read value's bits [28:20] are extracted (srli 20, andi 0x1ff) "
+ f"and compared against {TU10X_IDS[0]}..{TU10X_IDS[-1]} (Turing) and "
+ f"{GA100_ID} (Ampere); the image itself defines this as a chip-ID "
+ "field"
+ ),
+ "shift": 20,
+ "mask": 0x1FF,
+ "turing_ids": list(TU10X_IDS),
+ "ampere_id": GA100_ID,
+ "disassembly": lenient.get("disassembly"),
+ },
+ "q4_dispatch": {
+ "answer": (
+ f"ID {TU10X_IDS[0]}..{TU10X_IDS[-1]} dispatches to .tu10x_text at "
+ f"{TU10X_ENTRY:#x}; ID {GA100_ID} dispatches to .ga100_text at "
+ f"{GA100_ENTRY:#x}; any other value (including the lenient zero) "
+ f"falls to the fail-closed `j .` loop at {HALT_PC:#x}"
+ ),
+ "tu10x_target": TU10X_ENTRY,
+ "ga100_target": GA100_ENTRY,
+ "halt_pc": HALT_PC,
+ "sections": None, # filled by the caller (needs the image path)
+ },
+ "q5_next_blocker": {
+ "answer": (
+ "with a Turing ID served, .tu10x_text programs CSRs 0x7c8-0x7cc "
+ "(region descriptors) and enables mstatus FP/SIMD state, then "
+ f"builds the absolute link-time address {TU10X_ABS_TARGET:#x} and "
+ "jumps to it; because the loader relocates segments but not "
+ "absolute immediates, that jump lands outside RAM and faults - a "
+ "relocation artifact, so the next rung needs a chip-ID device "
+ "model plus load-at-link-address/alias translation"
+ ),
+ "modeled_run": {
+ "jumps": modeled.get("jumps"),
+ "instructions": modeled.get("instructions"),
+ "stop_pc_fw": modeled.get("stop_pc_fw"),
+ "stop_outside_ram": modeled.get("stop_outside_ram"),
+ "fault_signal": modeled.get("fault_signal"),
+ },
+ "disassembly_tail": modeled.get("disassembly_tail"),
+ },
+ }
+ return answers
+
+
+def evaluate(
+ *,
+ answers: dict,
+ lenient: dict,
+ repeat: dict,
+ emulator_wall_s: float,
+ baseline_total_s: float,
+ thresholds: dict,
+) -> dict:
+ keys = ("instructions", "stop_pc_fw", "first_unknown_mmio", "trace_exec_count")
+ identical = all(lenient.get(key) == repeat.get(key) for key in keys)
+ checks = {
+ "answers_complete": all(
+ isinstance(answer, dict) and bool(answer.get("answer"))
+ for answer in answers.values()
+ )
+ and len(answers) >= int(thresholds["answers_required"]),
+ "deterministic_lenient_runs": identical,
+ "emulator_wall_s": emulator_wall_s <= float(thresholds["emulator_wall_s_max"]),
+ "milestone_past_qemu": bool(
+ lenient.get("first_touch_matches_qemu")
+ and lenient.get("stop_pc_fw") == HALT_PC
+ and (repeat.get("stop_pc_fw") == HALT_PC)
+ ),
+ "baseline_beat": emulator_wall_s < baseline_total_s,
+ }
+ checks["pass"] = all(checks.values())
+ return checks
+
+
+def render_markdown(report: dict) -> str:
+ answers = report["answers"]
+ baseline = report["baseline"]
+ emulator = report["emulator"]
+ lenient = emulator["lenient"]
+ modeled = emulator["modeled"]
+ checks = report["threshold_checks"]
+ first = lenient.get("first_unknown_mmio") or {}
+ lines = [
+ "# Real RE demo - what the GSP does between entry and the first fault",
+ "",
+ f"* schema: `{report['schema']}`",
+ f"* script: `{report['script']}` (stdlib-only, no hardware/guest/module)",
+ f"* firmware: `{report['firmware']['name']}` sha256 "
+ f"`{report['firmware']['sha256'][:16]}...`",
+ f"* verdict: **{'PASS' if checks.get('pass') else 'FAIL'}**",
+ "",
+ "## Scenario",
+ "",
+ f"> {report['scenario']}",
+ "",
+ "## Answer",
+ "",
+ ]
+ for index in sorted(answers):
+ lines.append(f"**{index}: {answers[index]['answer']}**")
+ lines.append("")
+ lines += [
+ "## Baseline vs emulator (measured)",
+ "",
+ "| side | method | wall s | what it answers |",
+ "|---|---|---:|---|",
+ f"| baseline | `{baseline['qemu_method']}` | {baseline['qemu_probe_wall_s']} | first fault only |",
+ f"| baseline | manual static reading (estimate, {MANUAL_METHOD_NOTE}) | "
+ f"{baseline['manual_estimate_s']} | entry->fault path, by hand |",
+ f"| baseline | total (conservative) | {baseline['total_s']} | full question |",
+ f"| emulator | three recorded native sessions | {emulator['wall_s']} | full question + evidence |",
+ "",
+ "Per-session `wall_s` in the milestone table is the recorded run only; the "
+ "emulator total includes session launch, repeat and report writing.",
+ "",
+ "## Rung / milestone table",
+ "",
+ "| run | first touch | stop/milestone | instructions | wall s | evidence |",
+ "|---|---|---|---:|---:|---|",
+ f"| QEMU probe | {QEMU_REFERENCE['access_addr']:#x} (fault) | fault, then mtvec=0 loop | - | "
+ f"{baseline['qemu_probe_wall_s']} | `{baseline['qemu_session']}` |",
+ f"| native lenient (B0) | {first.get('addr'):#x} | halt loop fw "
+ f"{lenient['stop_pc_fw']:#x} | {lenient['instructions']} | {lenient['wall_s']} | "
+ f"`{lenient['session']}` |",
+ f"| native modeled chip-ID (B1 candidate) | {first.get('addr'):#x} | instruction "
+ f"fault at fw {modeled['stop_pc_fw']:#x} (outside RAM) | {modeled['instructions']} | "
+ f"{modeled['wall_s']} | `{modeled['session']}` |",
+ "",
+ "## Frozen thresholds",
+ "",
+ "| check | pass |",
+ "|---|---|",
+ ]
+ for name, value in checks.items():
+ lines.append(f"| {name} | {value} |")
+ lines += [
+ "",
+ f"Thresholds frozen {report['thresholds']['frozen_on']} before the run: "
+ f"emulator wall <= {report['thresholds']['emulator_wall_s_max']} s; "
+ f"{report['thresholds']['identical_lenient_runs']} identical lenient runs; "
+ f"all {report['thresholds']['answers_required']} answers present; milestone past "
+ "the QEMU point. Failure signal: any check false -> the run is a failed "
+ "experiment and the feature is not claimed.",
+ "",
+ "## Sessions (evidence)",
+ "",
+ "| session | file | sha256 |",
+ "|---|---|---|",
+ ]
+ for label, session in (
+ ("qemu-probe", baseline.get("qemu_session_hashes")),
+ ("native-lenient", lenient.get("session_hashes")),
+ ("native-lenient-repeat", report.get("lenient_repeat_hashes")),
+ ("native-modeled-id", modeled.get("session_hashes")),
+ ):
+ if not session:
+ continue
+ for name, digest in sorted(session.items()):
+ lines.append(f"| {label} | {name} | `{digest}` |")
+ lines += [
+ "",
+ "## Disassembly of the entry prologue (from the lenient session memory read)",
+ "",
+ "```",
+ *lenient.get("disassembly", []),
+ "```",
+ "",
+ "### Turing dispatch tail (from the modeled session memory read)",
+ "",
+ "```",
+ *(modeled.get("disassembly_tail") or []),
+ "```",
+ "",
+ "## Not verified (honest limits)",
+ "",
+ ]
+ for item in report.get("not_verified") or []:
+ lines.append(f"* {item}")
+ lines += [
+ "",
+ "## Reproduce",
+ "",
+ "```bash",
+ f"cd {report['rerun']['cwd']}",
+ report["rerun"]["command"],
+ "```",
+ "",
+ "## Re-run contract",
+ "",
+ ]
+ for item in report.get("rerun_notes") or []:
+ lines.append(f"* {item}")
+ lines.append("")
+ return "\n".join(lines)
+
+
+# -- main --------------------------------------------------------------------
+
+
+def run_demo(image: str = DEFAULT_IMAGE, out_dir: str = DEFAULT_OUT,
+ report_path: str = DEFAULT_REPORT, run_qemu: bool = True) -> dict:
+ os.makedirs(out_dir, exist_ok=True)
+ if not os.path.isfile(image):
+ return {
+ "schema": "greeneye-re-demo/1",
+ "ran": False,
+ "reason": f"firmware absent: {image}",
+ }
+
+ baseline_start = time.monotonic()
+ qemu = (
+ run_qemu_baseline(image=image, out_dir=out_dir, duration=0.5)
+ if run_qemu
+ else {"ran": False, "reason": "qemu disabled", "wall_s": 0.0}
+ )
+ baseline_wall = time.monotonic() - baseline_start
+ baseline_total = (
+ (qemu.get("wall_s") or 0.0 if qemu.get("ran") else baseline_wall)
+ + FROZEN_THRESHOLDS["manual_static_estimate_s"]
+ )
+
+ emulator_start = time.monotonic()
+ lenient = run_lenient_session(image, out_dir, tag="native-lenient")
+ repeat = run_lenient_session(image, out_dir, tag="native-lenient-repeat")
+ modeled = run_modeled_session(image, out_dir)
+ emulator_wall = time.monotonic() - emulator_start
+
+ sections = elf_progbits_sections(image)
+ answers = build_answers(lenient, modeled)
+ answers["q4_dispatch"]["sections"] = {
+ "tu10x_target": section_for(sections, TU10X_ENTRY),
+ "ga100_target": section_for(sections, GA100_ENTRY),
+ "bootstrap": section_for(sections, ENTRY),
+ }
+ checks = evaluate(
+ answers=answers,
+ lenient=lenient,
+ repeat=repeat,
+ emulator_wall_s=emulator_wall,
+ baseline_total_s=baseline_total,
+ thresholds=FROZEN_THRESHOLDS,
+ )
+ report = {
+ "schema": "greeneye-re-demo/1",
+ "ran": True,
+ "script": "harness/re_demo.py",
+ "scenario": (
+ "What happens on the real GSP image between entry and the first "
+ "fault/milestone, and what does the state tell us about boot?"
+ ),
+ "firmware": {"name": os.path.basename(image), "sha256": _sha256_file(image)},
+ "qemu_reference_first_fault": QEMU_REFERENCE,
+ "thresholds": FROZEN_THRESHOLDS,
+ "threshold_checks": checks,
+ "baseline": {
+ "qemu_method": "python3 -m greeneye.cli probe <image> --duration 1.0 (here run via harness.run_qemu_baseline, 0.5 s bound)",
+ "qemu_probe_wall_s": qemu.get("wall_s"),
+ "qemu_session": qemu.get("session"),
+ "qemu_session_hashes": qemu.get("session_hashes"),
+ "qemu_first_fault": qemu.get("first_fault"),
+ "manual_estimate_s": FROZEN_THRESHOLDS["manual_static_estimate_s"],
+ "manual_method": MANUAL_METHOD_NOTE,
+ "total_s": round(baseline_total, 4),
+ },
+ "emulator": {
+ "wall_s": round(emulator_wall, 4),
+ "lenient": lenient,
+ "modeled": modeled,
+ },
+ "lenient_repeat": repeat,
+ "lenient_repeat_hashes": repeat.get("session_hashes"),
+ "answers": answers,
+ "not_verified": [
+ "the modeled chip-ID value is an assumption (the QEMU baseline faults before reading it); only the field position and the compared IDs are firmware evidence",
+ "no timing/cycle claim: cycles are modeled (L2 not modelled, docs/06) and none are used in this answer",
+ "L3 device environment is absent: the register's on-silicon semantics are inferred from firmware comparisons, not from a device model",
+ "load-at-link-address and absolute-immediate relocation behaviour are not implemented; the B1 candidate fault is a relocation artifact of this harness",
+ "watchpoints are not exercised on this path: the bootstrap performs no RAM data access before the fault (RAM watchpoints are exercised by harness/sim_qemu_line.py on the demo firmware)",
+ "no hardware, GPU, mdev, module, guest or network involvement by design",
+ ],
+ "rerun": {
+ "cwd": "open-iov/debug/risc-v-gsp-rm-vm",
+ "command": "python3 harness/re_demo.py",
+ },
+ "rerun_notes": [
+ "thresholds live in harness/re_demo.py (FROZEN_THRESHOLDS) and are not moved after the run",
+ "the QEMU baseline needs qemu-system-riscv64; without it the emulator columns still reproduce and the baseline is marked unverified",
+ "sessions are under harness/re_demo/sessions/ (hash-stamped); the report regenerates with the same command",
+ ],
+ }
+ json_path = os.path.join(out_dir, "result.json")
+ with open(json_path, "w", encoding="utf-8") as fh:
+ json.dump(report, fh, indent=2, sort_keys=True)
+ fh.write("\n")
+ markdown = render_markdown(report)
+ os.makedirs(os.path.dirname(report_path) or ".", exist_ok=True)
+ with open(report_path, "w", encoding="utf-8") as fh:
+ fh.write(markdown)
+ report["result_json"] = _relpath(json_path)
+ report["report_md"] = _relpath(report_path)
+ return report
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(
+ prog="python3 harness/re_demo.py",
+ description=(
+ "U51 real RE demo: entry->first fault on the real GSP image, with "
+ "frozen thresholds and recorded sessions. No hardware."
+ ),
+ )
+ parser.add_argument("--image", default=DEFAULT_IMAGE)
+ parser.add_argument("--out", default=DEFAULT_OUT)
+ parser.add_argument("--report", default=DEFAULT_REPORT)
+ parser.add_argument("--no-qemu", action="store_true")
+ parser.add_argument("--json", action="store_true")
+ args = parser.parse_args(argv)
+
+ report = run_demo(
+ image=args.image, out_dir=args.out, report_path=args.report, run_qemu=not args.no_qemu
+ )
+ if not report.get("ran"):
+ print(f"[re-demo] skipped: {report.get('reason')}", file=sys.stderr)
+ return 2
+ checks = report["threshold_checks"]
+ if args.json:
+ print(json.dumps(report, indent=1, sort_keys=True, default=str))
+ print(f"[re-demo] answers: {len(report['answers'])}")
+ print(f"[re-demo] emulator wall: {report['emulator']['wall_s']} s")
+ print(f"[re-demo] baseline (qemu+manual): {report['baseline']['total_s']} s")
+ print(f"[re-demo] verdict: {'PASS' if checks.get('pass') else 'FAIL'} ({checks})")
+ print(f"[re-demo] report: {report.get('report_md')} + {report.get('result_json')}")
+ return 0 if checks.get("pass") else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/debug/risc-v-gsp-rm-vm/harness/re_demo/result.json b/debug/risc-v-gsp-rm-vm/harness/re_demo/result.json
new file mode 100644
index 0000000..4de85de
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/re_demo/result.json
@@ -0,0 +1,454 @@
+{
+ "answers": {
+ "q1_first_touch": {
+ "addr": 2305843009213696512,
+ "answer": "the first MMIO is a 4-byte read of PA 0x2000000000000a00 at fw PC 0x4009014; it is the QEMU-documented first fault",
+ "matches_qemu_reference": true,
+ "pc_fw": 67145748,
+ "rw": "r",
+ "size": 4
+ },
+ "q2_path": {
+ "answer": "entry state is all-zero (no bootloader handoff modelled); the bootstrap builds the device PA with 5 arithmetic instructions (li/slli/addi/slli/addi), reads it, then runs the chip-ID comparison chain: 25 instructions recorded from entry until the halt at fw 0x4009070",
+ "entry_regs_zero": false,
+ "halt_pc_fw": 67145840,
+ "halt_seen": true,
+ "instructions_to_halt": 25,
+ "trace_path_fw": [
+ 67145728,
+ 67145732,
+ 67145736,
+ 67145740,
+ 67145744,
+ 67145748,
+ 67145752,
+ 67145756,
+ 67145760,
+ 67145764,
+ 67145768,
+ 67145772,
+ 67145776,
+ 67145780,
+ 67145784,
+ 67145788,
+ 67145792,
+ 67145796,
+ 67145800,
+ 67145804,
+ 67145808,
+ 67145812,
+ 67145824,
+ 67145828,
+ 67145840
+ ]
+ },
+ "q3_id_field": {
+ "ampere_id": 368,
+ "answer": "the read value's bits [28:20] are extracted (srli 20, andi 0x1ff) and compared against 354..360 (Turing) and 368 (Ampere); the image itself defines this as a chip-ID field",
+ "disassembly": [
+ "0x84009000: 0010029b addiw t0,zero,1",
+ "0x84009004: 03129293 slli t0,t0,49",
+ "0x84009008: 00128293 addi t0,t0,1",
+ "0x8400900c: 00c29293 slli t0,t0,12",
+ "0x84009010: a0028293 addi t0,t0,-1536",
+ "0x84009014: 0002a283 lw t0,0(t0)",
+ "0x84009018: 0142d293 srli t0,t0,20",
+ "0x8400901c: 1ff2f293 andi t0,t0,511"
+ ],
+ "mask": 511,
+ "shift": 20,
+ "turing_ids": [
+ 354,
+ 355,
+ 356,
+ 357,
+ 358,
+ 359,
+ 360
+ ]
+ },
+ "q4_dispatch": {
+ "answer": "ID 354..360 dispatches to .tu10x_text at 0x4007000; ID 368 dispatches to .ga100_text at 0x4005000; any other value (including the lenient zero) falls to the fail-closed `j .` loop at 0x4009070",
+ "ga100_target": 67129344,
+ "halt_pc": 67145840,
+ "sections": {
+ "bootstrap": ".bootstrap",
+ "ga100_target": ".ga100_text",
+ "tu10x_target": ".tu10x_text"
+ },
+ "tu10x_target": 67137536
+ },
+ "q5_next_blocker": {
+ "answer": "with a Turing ID served, .tu10x_text programs CSRs 0x7c8-0x7cc (region descriptors) and enables mstatus FP/SIMD state, then builds the absolute link-time address 0x40070f8 and jumps to it; because the loader relocates segments but not absolute immediates, that jump lands outside RAM and faults - a relocation artifact, so the next rung needs a chip-ID device model plus load-at-link-address/alias translation",
+ "disassembly_tail": [
+ "0x840070e4: 1e0002b7 lui t0,0x1e000",
+ "0x840070e8: 3002a073 csrs mstatus,t0",
+ "0x840070ec: 040072b7 lui t0,0x4007",
+ "0x840070f0: 0f828293 addi t0,t0,248",
+ "0x840070f4: 00028067 jalr zero,0(t0)"
+ ],
+ "modeled_run": {
+ "fault_signal": 11,
+ "instructions": 74,
+ "jumps": [
+ {
+ "pc_fw": 67137780,
+ "t0_absolute_target": 67137784,
+ "target_fw": 67137784
+ }
+ ],
+ "stop_outside_ram": true,
+ "stop_pc_fw": 67137784
+ }
+ }
+ },
+ "baseline": {
+ "manual_estimate_s": 600.0,
+ "manual_method": "floor estimate: ~90 instructions across .bootstrap + .tu10x_text + .ga100_text read by hand, dispatch table identified and correlated with the QEMU first-fault report; not measured in this run",
+ "qemu_first_fault": {
+ "access_addr": 2305843009213696512,
+ "cause": 5,
+ "desc": "fault_load",
+ "epc": 2214629396,
+ "epc_fw": 67145748,
+ "reason": "rejected",
+ "rw": "read",
+ "size": 4
+ },
+ "qemu_method": "python3 -m greeneye.cli probe <image> --duration 1.0 (here run via harness.run_qemu_baseline, 0.5 s bound)",
+ "qemu_probe_wall_s": 0.9495,
+ "qemu_session": "harness/re_demo/sessions/qemu-probe",
+ "qemu_session_hashes": {
+ "events.jsonl": "8c4dba8006a496d853c81ce9e2b16944cd27b8d6171c01777daa6e5738be02bc",
+ "session.json": "c7bf5e4d29f534160d660dadb81f81942faa17cbfb1941587c60dcf78180f56c",
+ "summary.md": "d141fa06d3e2d0603f3b99ad7603758e1fc14627aed74c6fe8a6ad22c56eb14b"
+ },
+ "total_s": 600.9495
+ },
+ "emulator": {
+ "lenient": {
+ "disassembly": [
+ "0x84009000: 0010029b addiw t0,zero,1",
+ "0x84009004: 03129293 slli t0,t0,49",
+ "0x84009008: 00128293 addi t0,t0,1",
+ "0x8400900c: 00c29293 slli t0,t0,12",
+ "0x84009010: a0028293 addi t0,t0,-1536",
+ "0x84009014: 0002a283 lw t0,0(t0)",
+ "0x84009018: 0142d293 srli t0,t0,20",
+ "0x8400901c: 1ff2f293 andi t0,t0,511"
+ ],
+ "entry_regs_zero": false,
+ "first_touch_fw_pc": 67145748,
+ "first_touch_matches_qemu": true,
+ "first_unknown_mmio": {
+ "addr": 2305843009213696512,
+ "pc": 2214629396,
+ "rw": "r",
+ "size": 4
+ },
+ "hook_stops": [
+ {
+ "label": "first_touch_pc",
+ "pc": 2214629396,
+ "pc_fw": 67145748,
+ "t0": 2305843009213696512,
+ "t1": 0,
+ "t2": 0
+ },
+ {
+ "label": "halt",
+ "pc": 2214629488,
+ "pc_fw": 67145840,
+ "t0": 0,
+ "t1": 359,
+ "t2": 368
+ }
+ ],
+ "instructions": 25,
+ "session": "harness/re_demo/sessions/native-lenient",
+ "session_hashes": {
+ "events.jsonl": "ccb568588738c0609071ee542c9b04f7bb2868ee8fae12c28b2bfc5d9343721b",
+ "session.json": "ec5944adce4b823a3d6fd7d5aa6385fb5b61c2f9542c7221f46f328601c12c66",
+ "summary.md": "b50820afa60c35da0d4701e54f9907d4b7090c52721e1ecf656942a41bcf44db"
+ },
+ "snapshot_bytes": 120,
+ "stop_pc": 2214629488,
+ "stop_pc_fw": 67145840,
+ "trace_exec_count": 25,
+ "trace_path_fw": [
+ 67145728,
+ 67145732,
+ 67145736,
+ 67145740,
+ 67145744,
+ 67145748,
+ 67145752,
+ 67145756,
+ 67145760,
+ 67145764,
+ 67145768,
+ 67145772,
+ 67145776,
+ 67145780,
+ 67145784,
+ 67145788,
+ 67145792,
+ 67145796,
+ 67145800,
+ 67145804,
+ 67145808,
+ 67145812,
+ 67145824,
+ 67145828,
+ 67145840
+ ],
+ "wall_s": 0.0006
+ },
+ "modeled": {
+ "assumed_chip_id": 354,
+ "disassembly_tail": [
+ "0x840070e4: 1e0002b7 lui t0,0x1e000",
+ "0x840070e8: 3002a073 csrs mstatus,t0",
+ "0x840070ec: 040072b7 lui t0,0x4007",
+ "0x840070f0: 0f828293 addi t0,t0,248",
+ "0x840070f4: 00028067 jalr zero,0(t0)"
+ ],
+ "fault_signal": 11,
+ "fault_stops": [
+ {
+ "pc_fw": 67137784,
+ "signal": 11
+ }
+ ],
+ "instructions": 74,
+ "jumps": [
+ {
+ "pc_fw": 67137780,
+ "t0_absolute_target": 67137784,
+ "target_fw": 67137784
+ }
+ ],
+ "modeled_id": 371195904,
+ "session": "harness/re_demo/sessions/native-modeled-id",
+ "session_hashes": {
+ "events.jsonl": "fdf0756c3543741b74c9267dc9e4d22156f43a4b54463ffbfb1671c327444515",
+ "session.json": "5ae7eeff56a19ed7eea093a164a3a3738965b56c39f35a563db3ee93a2c90d39",
+ "summary.md": "bd670d207d23079173b494a9be9f9a0e7d31a10a11f9dc48fb8a134888442d0b"
+ },
+ "stop_outside_ram": true,
+ "stop_pc": 67137784,
+ "stop_pc_fw": 67137784,
+ "trace_exec_count": 74,
+ "trace_path_fw": [
+ 67145728,
+ 67145732,
+ 67145736,
+ 67145740,
+ 67145744,
+ 67145748,
+ 67145752,
+ 67145756,
+ 67145760,
+ 67145764,
+ 67145816,
+ 67145820,
+ 67137536,
+ 67137540,
+ 67137544,
+ 67137548,
+ 67137552,
+ 67137556,
+ 67137560,
+ 67137564,
+ 67137568,
+ 67137572,
+ 67137576,
+ 67137580,
+ 67137584,
+ 67137588,
+ 67137592,
+ 67137596,
+ 67137600,
+ 67137604,
+ 67137608,
+ 67137612,
+ 67137616,
+ 67137620,
+ 67137624,
+ 67137628,
+ 67137632,
+ 67137636,
+ 67137640,
+ 67137644,
+ 67137648,
+ 67137652,
+ 67137656,
+ 67137660,
+ 67137664,
+ 67137668,
+ 67137672,
+ 67137676,
+ 67137680,
+ 67137684,
+ 67137688,
+ 67137692,
+ 67137696,
+ 67137700,
+ 67137704,
+ 67137708,
+ 67137712,
+ 67137716,
+ 67137720,
+ 67137724,
+ 67137728,
+ 67137732,
+ 67137736,
+ 67137740,
+ 67137744,
+ 67137748,
+ 67137752,
+ 67137756,
+ 67137760,
+ 67137764,
+ 67137768,
+ 67137772,
+ 67137776,
+ 67137780
+ ],
+ "wall_s": 0.0004
+ },
+ "wall_s": 0.1251
+ },
+ "firmware": {
+ "name": "gsp-570.144.fwimage.bin",
+ "sha256": "2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea"
+ },
+ "lenient_repeat": {
+ "disassembly": [
+ "0x84009000: 0010029b addiw t0,zero,1",
+ "0x84009004: 03129293 slli t0,t0,49",
+ "0x84009008: 00128293 addi t0,t0,1",
+ "0x8400900c: 00c29293 slli t0,t0,12",
+ "0x84009010: a0028293 addi t0,t0,-1536",
+ "0x84009014: 0002a283 lw t0,0(t0)",
+ "0x84009018: 0142d293 srli t0,t0,20",
+ "0x8400901c: 1ff2f293 andi t0,t0,511"
+ ],
+ "entry_regs_zero": false,
+ "first_touch_fw_pc": 67145748,
+ "first_touch_matches_qemu": true,
+ "first_unknown_mmio": {
+ "addr": 2305843009213696512,
+ "pc": 2214629396,
+ "rw": "r",
+ "size": 4
+ },
+ "hook_stops": [
+ {
+ "label": "first_touch_pc",
+ "pc": 2214629396,
+ "pc_fw": 67145748,
+ "t0": 2305843009213696512,
+ "t1": 0,
+ "t2": 0
+ },
+ {
+ "label": "halt",
+ "pc": 2214629488,
+ "pc_fw": 67145840,
+ "t0": 0,
+ "t1": 359,
+ "t2": 368
+ }
+ ],
+ "instructions": 25,
+ "session": "harness/re_demo/sessions/native-lenient-repeat",
+ "session_hashes": {
+ "events.jsonl": "2cd28b4da009447d70505cfbba59e9c1bf8bd277e3a7ea696e2690f54f66f756",
+ "session.json": "fe21ed52c05436d0f7886dfeb0886ec1a11b25678e4d827db3b9e4009d2187e4",
+ "summary.md": "357d95a664e33128a2a48da7ecfc058bdbbea56fae130557593fd59ff2efa597"
+ },
+ "snapshot_bytes": 120,
+ "stop_pc": 2214629488,
+ "stop_pc_fw": 67145840,
+ "trace_exec_count": 25,
+ "trace_path_fw": [
+ 67145728,
+ 67145732,
+ 67145736,
+ 67145740,
+ 67145744,
+ 67145748,
+ 67145752,
+ 67145756,
+ 67145760,
+ 67145764,
+ 67145768,
+ 67145772,
+ 67145776,
+ 67145780,
+ 67145784,
+ 67145788,
+ 67145792,
+ 67145796,
+ 67145800,
+ 67145804,
+ 67145808,
+ 67145812,
+ 67145824,
+ 67145828,
+ 67145840
+ ],
+ "wall_s": 0.0005
+ },
+ "lenient_repeat_hashes": {
+ "events.jsonl": "2cd28b4da009447d70505cfbba59e9c1bf8bd277e3a7ea696e2690f54f66f756",
+ "session.json": "fe21ed52c05436d0f7886dfeb0886ec1a11b25678e4d827db3b9e4009d2187e4",
+ "summary.md": "357d95a664e33128a2a48da7ecfc058bdbbea56fae130557593fd59ff2efa597"
+ },
+ "not_verified": [
+ "the modeled chip-ID value is an assumption (the QEMU baseline faults before reading it); only the field position and the compared IDs are firmware evidence",
+ "no timing/cycle claim: cycles are modeled (L2 not modelled, docs/06) and none are used in this answer",
+ "L3 device environment is absent: the register's on-silicon semantics are inferred from firmware comparisons, not from a device model",
+ "load-at-link-address and absolute-immediate relocation behaviour are not implemented; the B1 candidate fault is a relocation artifact of this harness",
+ "watchpoints are not exercised on this path: the bootstrap performs no RAM data access before the fault (RAM watchpoints are exercised by harness/sim_qemu_line.py on the demo firmware)",
+ "no hardware, GPU, mdev, module, guest or network involvement by design"
+ ],
+ "qemu_reference_first_fault": {
+ "access_addr": 2305843009213696512,
+ "cause": 5,
+ "desc": "load access fault",
+ "firmware_vaddr": 67145748,
+ "rw": "read",
+ "size": 4
+ },
+ "ran": true,
+ "rerun": {
+ "command": "python3 harness/re_demo.py",
+ "cwd": "open-iov/debug/risc-v-gsp-rm-vm"
+ },
+ "rerun_notes": [
+ "thresholds live in harness/re_demo.py (FROZEN_THRESHOLDS) and are not moved after the run",
+ "the QEMU baseline needs qemu-system-riscv64; without it the emulator columns still reproduce and the baseline is marked unverified",
+ "sessions are under harness/re_demo/sessions/ (hash-stamped); the report regenerates with the same command"
+ ],
+ "scenario": "What happens on the real GSP image between entry and the first fault/milestone, and what does the state tell us about boot?",
+ "schema": "greeneye-re-demo/1",
+ "script": "harness/re_demo.py",
+ "threshold_checks": {
+ "answers_complete": true,
+ "baseline_beat": true,
+ "deterministic_lenient_runs": true,
+ "emulator_wall_s": true,
+ "milestone_past_qemu": true,
+ "pass": true
+ },
+ "thresholds": {
+ "answers_required": 5,
+ "emulator_wall_s_max": 120.0,
+ "frozen_on": "2026-10-03",
+ "identical_lenient_runs": 2,
+ "manual_static_estimate_s": 600.0,
+ "milestone_past_qemu": true
+ }
+}
diff --git a/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py b/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py
new file mode 100644
index 0000000..b47a028
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py
@@ -0,0 +1,1247 @@
+#!/usr/bin/env python3
+"""S4 evidence harness: is the native GSP simulator above the QEMU line?
+
+Runs one A/B pair on the real TU104 570.144 GSP image:
+
+* **QEMU baseline** - the v0.1/v0.2 probe path (flat relocated load, bounded
+ wall-clock run, ``guest_errors,unimp,int`` log): first exception, address and
+ ladder rung, recorded as a normal ``greeneye-session/1``.
+* **Native baseline** - ``sim.gsp_profile`` (deterministic RV64IMAC, modeled
+ cycles): instructions, cycles, first modeled MMIO, stop reason and rung.
+ If S1's device model/profile is present (``sim/profiles/``, ``sim/devices/``,
+ or a Python ``sim.devices`` entry point) it is discovered defensively and
+ used as the MMIO map; otherwise the run is the lenient B0 baseline.
+
+The rung ladder is docs/35-GREENEYE-SIMULATOR-DESIGN.md section 3.3
+(``pre-B0`` < B0 < B1 < ... < B5). "Above the QEMU line" means the native
+rung ordinal is at least one past QEMU's, twice identically, within the frozen
+wall-clock threshold, and with a discovered S1 device model (docs/35 1.1);
+requiring the device model means a lenient run reports ``above_lenient_only``,
+not ``above``. The frozen record and the first measured results live in
+``docs/37-SIM-QEMU-LINE.md``.
+
+The harness also exercises the native feature matrix (hooks/breakpoints/
+watchpoints, memory/struct, sessions, compare, trace, injection) on the bundled
+demo firmware. MCP is deliberately ``not_run``: the native MCP surface is S3's
+deliverable (see docs/37 section "Not verified").
+
+Everything is stdlib-only, bounded (``<= 2M`` instructions per native run,
+``<= 10 s`` of QEMU) and touches no hardware, GPU, guest, module or network.
+
+CLI::
+
+ python3 harness/sim_qemu_line.py
+ python3 harness/sim_qemu_line.py --no-qemu --instructions 50000
+ python3 harness/sim_qemu_line.py --device-map sim/profiles/tu104-570.144.json
+"""
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import shutil
+import subprocess
+import sys
+import time
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+TOOL_ROOT = os.path.dirname(HERE)
+if TOOL_ROOT not in sys.path:
+ sys.path.insert(0, TOOL_ROOT)
+
+from greeneye.analyze import compare_sessions, parse_qemu_faults # noqa: E402
+from greeneye.device_map import ( # noqa: E402
+ QEMU_REFERENCE,
+ DeviceEntry,
+ DeviceMap,
+ firmware_sha256,
+ harvest_from_text,
+)
+from greeneye.errors import GreenEyeError # noqa: E402
+from harness.common import ( # noqa: E402
+ load_target,
+ open_flat_session,
+ qemu_available,
+ read_log_window,
+)
+from sim.gsp_profile import DEFAULT_IMAGE, run_profile # noqa: E402
+
+SCHEMA = "greeneye-sim-qemu-line/1"
+
+#: Boot-ladder rungs (docs/35 section 3.3), lowest first. ``pre-B0`` is the
+#: QEMU fault position: image loaded, first MMIO observed *as a fault*.
+RUNG_ORDER = ("pre-B0", "B0", "B1", "B2", "B3", "B4", "B5")
+RUNG_INDEX = {name: index for index, name in enumerate(RUNG_ORDER)}
+QEMU_RUNG = "pre-B0"
+NATIVE_LENIENT_RUNG = "B0"
+
+#: Default frozen thresholds (recorded 2026-10-03, before the first
+#: measurement). The measured run and rationale live in
+#: docs/37-SIM-QEMU-LINE.md section 3; the CLI uses these values when no
+#: override is passed. Never move silently.
+FROZEN_THRESHOLDS = {
+ "frozen_on": "2026-10-03",
+ "native_rungs_above_qemu": 1,
+ "identical_runs": 2,
+ "native_pair_wall_s": 20.0,
+ "qemu_first_fault_fw": QEMU_REFERENCE["firmware_vaddr"],
+ "qemu_first_touch": QEMU_REFERENCE["access_addr"],
+ # docs/35 1.1 requires the milestone to be reached "with the device model
+ # and shims documented": lenient MMIO is observation, not a device model,
+ # so it cannot satisfy the line contract on its own.
+ "device_model_required": True,
+}
+
+#: Bounds (eng-ops / task discipline).
+MAX_INSTRUCTIONS = 2_000_000
+MAX_QEMU_DURATION_S = 10.0
+DEFAULT_INSTRUCTIONS = 200_000
+DEFAULT_QEMU_DURATION_S = 0.5
+
+#: Default report directory (new, S4-owned).
+DEFAULT_OUT = os.path.join(HERE, "qemu_line")
+
+DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf")
+DEMO_SCHEMA = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "gsp_ctrl.schema.json")
+SDK_ELF = os.path.join(TOOL_ROOT, "sdk", "build", "injected_demo.elf")
+
+#: S1 device-model discovery: JSON conventions first, then a duck-typed Python
+#: API. See docs/37 section "Re-running when S1 lands".
+S1_JSON_DIRS = ("sim/profiles", "sim/devices")
+S1_PY_API_NAMES = (
+ "device_map_for_firmware",
+ "profile_for_firmware",
+ "load_device_map",
+ "load_profile",
+ "profile_for",
+)
+_RUNG_KEYS = ("rung", "highest_rung", "boot_ladder_rung", "milestone")
+
+
+class HarnessError(RuntimeError):
+ """A harness precondition failed in a way the caller should report."""
+
+
+# -- small helpers ---------------------------------------------------------
+
+
+def _relpath(path: str | None) -> str | None:
+ if path is None:
+ return None
+ return os.path.relpath(os.path.abspath(path), TOOL_ROOT)
+
+
+def _rm_tree(path: str) -> None:
+ shutil.rmtree(path, ignore_errors=True)
+
+
+def _write_text(path: str, text: str) -> str:
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "w", encoding="utf-8") as fh:
+ fh.write(text)
+ return path
+
+
+def _write_json(path: str, payload: dict) -> str:
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ tmp = f"{path}.tmp-{os.getpid()}"
+ with open(tmp, "w", encoding="utf-8") as fh:
+ json.dump(payload, fh, indent=2, sort_keys=True)
+ fh.write("\n")
+ os.replace(tmp, path)
+ return path
+
+
+def session_hashes(session_dir: str | None) -> dict:
+ """sha256 of a session's core evidence files ({} when the dir is absent)."""
+ if not session_dir or not os.path.isdir(session_dir):
+ return {}
+ hashes: dict = {}
+ for name in ("session.json", "events.jsonl", "summary.md"):
+ path = os.path.join(session_dir, name)
+ if not os.path.isfile(path):
+ continue
+ digest = hashlib.sha256()
+ with open(path, "rb") as fh:
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
+ digest.update(chunk)
+ hashes[name] = digest.hexdigest()
+ return hashes
+
+
+def _x86_vms_running() -> int | None:
+ """Count other agents' x86 QEMU VMs (never disturbed; recorded only)."""
+ try:
+ proc = subprocess.run(
+ ["pgrep", "-f", "qemu-system-x86_64"],
+ capture_output=True,
+ text=True,
+ timeout=5.0,
+ )
+ except (OSError, subprocess.SubprocessError):
+ return None
+ if proc.returncode not in (0, 1):
+ return None
+ pids = [line for line in proc.stdout.splitlines() if line.strip().isdigit()]
+ return len(pids)
+
+
+def rung_ordinal(rung: str | None) -> int | None:
+ if rung in RUNG_INDEX:
+ return RUNG_INDEX[rung]
+ return None
+
+
+def above_qemu_line(qemu_rung: str | None, native_rung: str | None, minimum: int = 1) -> bool:
+ """True when ``native_rung`` is at least ``minimum`` rungs past QEMU's."""
+ q = rung_ordinal(qemu_rung)
+ n = rung_ordinal(native_rung)
+ if q is None or n is None:
+ return False
+ return n >= q + minimum
+
+
+def _summary_fault(fault: dict, bias: int) -> dict:
+ access = fault.get("access") or {}
+ epc = int(fault.get("epc", 0) or 0)
+ return {
+ "desc": fault.get("desc"),
+ "cause": fault.get("cause"),
+ "epc": epc,
+ "epc_fw": (epc - bias) if (bias and epc and epc >= bias) else epc,
+ "access_addr": access.get("access_addr"),
+ "size": access.get("size"),
+ "rw": access.get("rw"),
+ "reason": access.get("reason"),
+ }
+
+
+# -- QEMU baseline ---------------------------------------------------------
+
+
+def run_qemu_baseline(
+ image: str = DEFAULT_IMAGE,
+ out_dir: str = DEFAULT_OUT,
+ duration: float = DEFAULT_QEMU_DURATION_S,
+) -> dict:
+ """Probe the real image under QEMU; skip cleanly when unavailable/absent."""
+ result: dict = {
+ "backend": "qemu",
+ "ran": False,
+ "reason": None,
+ "rung": None,
+ "session": None,
+ "wall_s": 0.0,
+ }
+ if duration <= 0 or duration > MAX_QEMU_DURATION_S:
+ raise HarnessError(f"qemu duration must be in (0, {MAX_QEMU_DURATION_S}], got {duration}")
+ if not os.path.isfile(image):
+ result["reason"] = f"firmware absent: {image}"
+ return result
+ if not qemu_available():
+ result["reason"] = "qemu-system-riscv64 not installed"
+ return result
+
+ start = time.monotonic()
+ image_obj = load_target(image)
+ session_dir = os.path.join(out_dir, "sessions", "qemu-probe")
+ _rm_tree(session_dir)
+ with open_flat_session(image_obj, image, record=session_dir, name="s4-qemu-probe") as flat:
+ flat.session.run(duration=duration)
+ pc = flat.session.pc
+ bias = flat.bias
+ # Read the log window once: `-d int` trap loops flood the file, so the
+ # first exception has to be parsed from the bounded head window.
+ log_text, capped = read_log_window(flat.target.qemu_log)
+ faults = parse_qemu_faults(log_text)
+ first = faults[0] if faults else None
+ device_map = harvest_from_text(log_text, bias=bias)
+ meta = {
+ "s4_qemu_rung": QEMU_RUNG,
+ "qemu_fault_count_window": len(faults),
+ "qemu_log_window_capped": capped,
+ "qemu_first_fault": first,
+ }
+ recorder = getattr(flat.session, "recorder", None)
+ if recorder is not None and hasattr(recorder, "meta"):
+ recorder.meta.update(meta)
+ if log_text and hasattr(recorder, "blob"):
+ recorder.meta["qemu_log_blob"] = recorder.blob(
+ "qemu.log", log_text.encode("utf-8")
+ )
+ result.update(
+ {
+ "ran": True,
+ "reason": "ok",
+ "rung": QEMU_RUNG,
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "stopped_pc": pc,
+ "fault_count_window": len(faults),
+ "log_window_capped": capped,
+ "first_fault": _summary_fault(first, bias) if first else None,
+ "first_touch_match": bool(
+ first
+ and (first.get("access") or {}).get("access_addr") == QEMU_REFERENCE["access_addr"]
+ ),
+ "device_map_entries": len(device_map),
+ "device_map": device_map,
+ }
+ )
+ result["wall_s"] = round(time.monotonic() - start, 4)
+ return result
+
+
+# -- S1 integration (defensive) -------------------------------------------
+
+
+def _coerce_device_map(obj) -> DeviceMap | None:
+ """Accept DeviceMap objects, paths, or device-map/profile dicts; else None."""
+ if isinstance(obj, DeviceMap):
+ return obj
+ if isinstance(obj, (str, os.PathLike)):
+ path = os.fspath(obj)
+ if os.path.isfile(path):
+ try:
+ return DeviceMap.load(path)
+ except GreenEyeError:
+ return None
+ return None
+ if isinstance(obj, dict):
+ if obj.get("schema") == "greeneye-device-map/1":
+ try:
+ return DeviceMap.from_dict(obj)
+ except (KeyError, TypeError, ValueError, GreenEyeError):
+ return None
+ if "device_map" in obj:
+ return _coerce_device_map(obj["device_map"])
+ entries = obj.get("entries")
+ if isinstance(entries, list):
+ dm = DeviceMap(meta=dict(obj.get("meta") or {}))
+ try:
+ for raw in entries:
+ dm.add(DeviceEntry.from_dict(raw))
+ except (KeyError, TypeError, ValueError):
+ return None
+ return dm
+ return None
+
+
+def _json_rung(data) -> str | None:
+ if not isinstance(data, dict):
+ return None
+ for key in _RUNG_KEYS:
+ value = data.get(key)
+ if isinstance(value, str) and value in RUNG_INDEX:
+ return value
+ if isinstance(value, dict):
+ inner = value.get("highest") or value.get("rung")
+ if isinstance(inner, str) and inner in RUNG_INDEX:
+ return inner
+ return None
+
+
+def discover_s1_device_model(image: str, sha: str | None = None) -> dict:
+ """Find S1's device model/profile without assuming it exists.
+
+ Returns ``{"device_map": DeviceMap|None, "profile": path|None,
+ "declared_rung": str|None, "source": str|None, "notes": [...]}``.
+ Discovery order: an explicit ``sim.devices`` Python entry point, then JSON
+ files under ``sim/profiles/`` and ``sim/devices/`` (a
+ ``greeneye-sim-profile/1`` profile, a ``greeneye-device-map/1`` file, a
+ profile with a ``device_map`` key, or a raw ``entries`` list). A profile
+ is the semantic device model (``sim.gsp_boot`` runs it); a device map only
+ replays observed accesses. A candidate is preferred when its firmware
+ hash matches the image; otherwise a single candidate is used and noted as
+ unverified pairing.
+ """
+ notes: list[str] = []
+ declared_rung: str | None = None
+
+ try:
+ import sim.devices as devices # type: ignore
+
+ for name in S1_PY_API_NAMES:
+ fn = getattr(devices, name, None)
+ if not callable(fn):
+ continue
+ for args in ((sha,), (image,), ()):
+ if sha is None and args == (sha,):
+ continue
+ try:
+ candidate = fn(*args)
+ except TypeError:
+ continue
+ except (OSError, UnicodeError):
+ # A path-taking helper handed a digest (or a helper handed
+ # the binary firmware) simply does not apply; not an error.
+ break
+ except Exception as exc: # discovery must never break the run
+ notes.append(f"sim.devices.{name}{args!r} failed: {exc}")
+ break
+ if candidate is None:
+ continue
+ dm = _coerce_device_map(candidate)
+ rung = getattr(candidate, "rung", None)
+ if isinstance(rung, str) and rung in RUNG_INDEX:
+ declared_rung = rung
+ if dm is not None:
+ notes.append(f"device map via sim.devices.{name}{args!r}")
+ return {
+ "device_map": dm,
+ "profile": None,
+ "declared_rung": declared_rung,
+ "source": f"sim.devices.{name}",
+ "notes": notes,
+ }
+ except ImportError:
+ notes.append("sim.devices package absent")
+ except Exception as exc:
+ notes.append(f"sim.devices import failed: {exc}")
+
+ candidates: list[tuple[str, DeviceMap, str | None, str | None]] = []
+ profiles: list[tuple[str, str, str | None]] = []
+ for rel in S1_JSON_DIRS:
+ root = os.path.join(TOOL_ROOT, rel)
+ if not os.path.isdir(root):
+ notes.append(f"{rel}/ absent")
+ continue
+ for name in sorted(os.listdir(root)):
+ if not name.endswith(".json"):
+ continue
+ path = os.path.join(root, name)
+ try:
+ with open(path, "r", encoding="utf-8") as fh:
+ data = json.load(fh)
+ except (OSError, ValueError) as exc:
+ notes.append(f"{rel}/{name}: unreadable ({exc})")
+ continue
+ if isinstance(data, dict) and data.get("schema") == "greeneye-sim-profile/1":
+ firmware = data.get("firmware") or {}
+ profiles.append((f"{rel}/{name}", path,
+ str(firmware.get("sha256") or "") or None))
+ continue
+ dm = _coerce_device_map(data)
+ rung = _json_rung(data)
+ if rung and declared_rung is None:
+ declared_rung = rung
+ if dm is not None:
+ candidates.append((f"{rel}/{name}", dm, rung, None))
+
+ exact_profiles = [p for p in profiles if sha and p[2] == sha]
+ if exact_profiles:
+ rel, path, _digest = exact_profiles[0]
+ notes.append(f"using {rel} (greeneye-sim-profile/1, firmware_sha256 matched)")
+ return {
+ "device_map": None,
+ "profile": path,
+ "declared_rung": declared_rung,
+ "source": rel,
+ "notes": notes,
+ }
+
+ if not candidates:
+ if len(profiles) == 1:
+ rel, path, _digest = profiles[0]
+ notes.append(f"using {rel} (single profile, pairing unverified)")
+ return {
+ "device_map": None,
+ "profile": path,
+ "declared_rung": declared_rung,
+ "source": rel,
+ "notes": notes,
+ }
+ notes.append("no S1 device-map/profile JSON found")
+ return {"device_map": None, "profile": None, "declared_rung": declared_rung,
+ "source": None, "notes": notes}
+
+ exact = [
+ c for c in candidates
+ if sha and str((c[1].meta or {}).get("firmware_sha256", "")) == sha
+ ]
+ chosen = exact[0] if exact else candidates[0]
+ note = "firmware_sha256 matched" if exact else "single candidate, pairing unverified"
+ notes.append(f"using {chosen[0]} ({note})")
+ return {
+ "device_map": chosen[1],
+ "profile": None,
+ "declared_rung": chosen[2] or declared_rung,
+ "source": chosen[0],
+ "notes": notes,
+ }
+
+
+# -- native baseline -------------------------------------------------------
+
+
+def estimate_native_rung(run: dict, *, device_model_used: bool,
+ declared_rung: str | None = None) -> tuple[str, str]:
+ """Conservative rung estimate from a bounded native run.
+
+ * matching lenient first touch -> B0 (observed, no device semantics);
+ * a device model that serves the QEMU first-touch and lets execution
+ continue -> B1 (frontier moved past the QEMU fault);
+ * a higher rung only when S1 declares it in profile data (honoured as
+ ``rung_source: declared``).
+
+ B2+ need boot-stage milestones, not just progress; absence of a fault is
+ not evidence of a boot stage. See docs/37 sections 3-4.
+ """
+ stop = run.get("stop") or {}
+ stop_reason = stop.get("reason")
+ first = run.get("first_unknown_mmio")
+ reached_b0 = bool(
+ (first and first.get("addr") == QEMU_REFERENCE["access_addr"])
+ or run.get("instructions", 0) > 0
+ )
+
+ inferred = None
+ basis = None
+ if device_model_used and reached_b0:
+ unmodelled_at_qemu = bool(first and first.get("addr") == QEMU_REFERENCE["access_addr"])
+ if not unmodelled_at_qemu:
+ inferred = "B1"
+ basis = (
+ "device model served the QEMU first-touch and execution continued "
+ "past the QEMU fault to the run bound"
+ )
+ else:
+ inferred = "B0"
+ basis = "device model present but the QEMU first-touch was still served leniently"
+ elif reached_b0:
+ inferred = NATIVE_LENIENT_RUNG
+ basis = "lenient first-touch observation; execution continued past the QEMU fault"
+
+ if inferred is None:
+ return "pre-B0", "no native progress observed"
+
+ rung, source = inferred, "inferred"
+ declared_index = rung_ordinal(declared_rung)
+ if declared_index is not None and declared_index > RUNG_INDEX[inferred]:
+ rung, source = declared_rung, "declared"
+ basis = f"S1 profile declares {declared_rung}; inference was {inferred} ({basis})"
+ return rung, f"{basis} [rung_source={source}, stop={stop_reason}]"
+
+
+def run_native_baseline(
+ image: str = DEFAULT_IMAGE,
+ out_dir: str = DEFAULT_OUT,
+ instructions: int = DEFAULT_INSTRUCTIONS,
+ repeat: int = 2,
+ device_map=None,
+ declared_rung: str | None = None,
+ profile: str | None = None,
+ use_shim: bool = True,
+) -> dict:
+ """Run the bounded deterministic native baseline ``repeat`` times.
+
+ ``profile`` selects the S1 semantic device model (``sim.gsp_boot`` with
+ the versioned profile and the recorded boot shim); without it the run is
+ the lenient ``sim.gsp_profile`` baseline. ``device_map`` remains a
+ legacy/observational MMIO map for the lenient path.
+ """
+ if instructions <= 0 or instructions > MAX_INSTRUCTIONS:
+ raise HarnessError(f"instructions must be in (0, {MAX_INSTRUCTIONS}], got {instructions}")
+ if repeat < 1:
+ raise HarnessError(f"repeat must be >= 1, got {repeat}")
+ if not os.path.isfile(image):
+ return {
+ "backend": "native",
+ "ran": False,
+ "reason": f"firmware absent: {image}",
+ "runs": [],
+ }
+
+ s1_model = bool(profile) or device_map is not None
+ runs: list[dict] = []
+ start_all = time.monotonic()
+ for index in range(repeat):
+ session_dir = os.path.join(out_dir, "sessions", f"native-{index + 1}")
+ _rm_tree(session_dir)
+ start = time.monotonic()
+ if profile:
+ from sim.gsp_boot import run_boot # deferred: only the S1 path needs it
+
+ boot = run_boot(
+ image=image,
+ profile_path=profile,
+ instructions=instructions,
+ record=session_dir,
+ name=f"s4-native-{index + 1}",
+ use_shim=use_shim,
+ )
+ wall = time.monotonic() - start
+ if not boot.ran:
+ raise HarnessError(f"native boot refused to run: {boot.reason}")
+ run = {
+ "instructions": boot.instructions,
+ "cycles": boot.cycles,
+ "cycle_model": boot.cycle_model,
+ "stop": {
+ "reason": boot.stop_reason,
+ "pc": boot.stop_pc,
+ "pc_fw": boot.stop_pc_fw,
+ },
+ "first_unknown_mmio": boot.first_unknown_mmio,
+ "mmio_unknown": boot.mmio_unknown,
+ "device_unhandled": boot.device_unhandled,
+ "device_accesses": boot.device_accesses,
+ "device_state_sha256": boot.device_state_sha256,
+ "milestones": list(boot.milestones),
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "wall_s": round(wall, 4),
+ }
+ run["rung"] = boot.rung
+ run["rung_basis"] = (
+ "observed S1 boot-ladder rung from sim.gsp_boot "
+ f"(milestones={list(boot.milestones)}, mmio_unknown={boot.mmio_unknown}, "
+ f"device_unhandled={boot.device_unhandled})"
+ )
+ else:
+ result = run_profile(
+ image=image,
+ instructions=instructions,
+ record=session_dir,
+ device_map=device_map,
+ name=f"s4-native-{index + 1}",
+ )
+ wall = time.monotonic() - start
+ if not result.ran:
+ raise HarnessError(f"native profile refused to run: {result.reason}")
+ run = {
+ "instructions": result.instructions,
+ "cycles": result.cycles,
+ "cycle_model": result.cycle_model,
+ "stop": {
+ "reason": result.stop_reason,
+ "pc": result.stop_pc,
+ "pc_fw": result.stop_pc_fw,
+ },
+ "first_unknown_mmio": result.first_unknown_mmio,
+ "mmio_unknown": result.mmio_unknown,
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "wall_s": round(wall, 4),
+ }
+ run["rung"], run["rung_basis"] = estimate_native_rung(
+ run, device_model_used=s1_model, declared_rung=declared_rung
+ )
+ runs.append(run)
+
+ keys = (
+ "instructions",
+ "cycles",
+ "stop",
+ "first_unknown_mmio",
+ "mmio_unknown",
+ "rung",
+ "device_state_sha256",
+ "milestones",
+ )
+ identical = all(
+ all(a.get(key) == runs[0].get(key) for key in keys) for a in runs[1:]
+ )
+ compare = {"ran": False}
+ if repeat >= 2:
+ try:
+ text = compare_sessions(
+ os.path.join(out_dir, "sessions", "native-1"),
+ os.path.join(out_dir, "sessions", "native-2"),
+ )
+ compare = {
+ "ran": True,
+ "chars": len(text),
+ "headers": [
+ line[3:].strip()
+ for line in text.splitlines()
+ if line.startswith("## ")
+ ],
+ }
+ except Exception as exc: # compare is evidence, never a fatal step
+ compare = {"ran": False, "error": f"{type(exc).__name__}: {exc}"}
+
+ return {
+ "backend": "native",
+ "ran": True,
+ "reason": "ok",
+ "instructions_budget": instructions,
+ "repeat": repeat,
+ "identical": identical,
+ "rung": runs[0]["rung"],
+ "rung_basis": runs[0]["rung_basis"],
+ "runs": runs,
+ "pair_wall_s": round(time.monotonic() - start_all, 4),
+ "compare": compare,
+ "device_model_used": s1_model,
+ "profile": _relpath(profile),
+ }
+
+
+# -- native feature matrix -------------------------------------------------
+
+
+def _events_summary(session_dir: str) -> dict:
+ counts: dict[str, int] = {}
+ path = os.path.join(session_dir, "events.jsonl")
+ try:
+ with open(path, "r", encoding="utf-8") as fh:
+ for line in fh:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ kind = json.loads(line).get("type")
+ except ValueError:
+ continue
+ if kind:
+ counts[kind] = counts.get(kind, 0) + 1
+ except OSError:
+ pass
+ return dict(sorted(counts.items()))
+
+
+def run_feature_matrix(out_dir: str = DEFAULT_OUT) -> dict:
+ """Exercise the native debugger feature set on the bundled demo firmware."""
+ import greeneye as ge
+
+ matrix: dict = {}
+ if not os.path.isfile(DEMO_ELF):
+ return {
+ "ran": False,
+ "reason": f"demo firmware absent: {DEMO_ELF} (run make demo)",
+ "features": {},
+ }
+
+ session_dir = os.path.join(out_dir, "sessions", "features")
+ _rm_tree(session_dir)
+ session = ge.launch(kernel=DEMO_ELF, backend="native", record=session_dir, name="s4-features")
+ try:
+ ctrl = session.symbols.lookup("gsp_ctrl")
+ session.run(duration=0.2)
+ magic = session.memory.read_u32(ctrl)
+ matrix["memory"] = {
+ "status": "pass" if magic == 0x47535031 else "fail",
+ "detail": {"ctrl": ctrl, "magic": magic},
+ }
+
+ with open(DEMO_SCHEMA, "r", encoding="utf-8") as fh:
+ schema = ge.schema_from_json(json.load(fh))
+ decoded = schema.decode(session.memory.read(ctrl, schema.size), base=0)
+ matrix["struct"] = {
+ "status": "pass" if decoded.get("magic") == 0x47535031 else "fail",
+ "detail": {"schema": os.path.basename(DEMO_SCHEMA), "fields": len(decoded)},
+ }
+
+ dispatches: list[int] = []
+ stores: list[int] = []
+ session.hook("rpc_dispatch", on_enter=lambda hit: dispatches.append(hit.arg(0)))
+ session.watch(ctrl + 0x40, size=8, kind="w", callback=lambda hit: stores.append(hit.pc))
+ session.memory.write_u64(ctrl + 0x28, 0x1234)
+ session.memory.write_u64(ctrl + 0x20, 1)
+ session.run(duration=0.25)
+ matrix["hooks"] = {
+ "status": "pass" if dispatches else "fail",
+ "detail": {"rpc_dispatch_hits": len(dispatches), "cmds": dispatches},
+ }
+ matrix["watch"] = {
+ "status": "pass" if stores else "fail",
+ "detail": {"result_field_stores": len(stores), "pcs": stores[:4]},
+ }
+
+ session.memory.write_u64(ctrl + 0x28, 0x55)
+ session.memory.write_u64(ctrl + 0x20, 2)
+ hit = session.run_until("rpc_dispatch", timeout=1.0)
+ bp = session.breakpoint("rpc_dispatch")
+ attached = bool(bp.attached)
+ session.remove_breakpoint(bp)
+ matrix["breakpoints"] = {
+ "status": "pass" if hit is not None and attached and not bp.attached else "fail",
+ "detail": {
+ "run_until_hit": hit.addr if hit is not None else None,
+ "attached_then_removed": attached and not bp.attached,
+ "kind": "sw",
+ },
+ }
+ finally:
+ session.close()
+
+ events = _events_summary(session_dir)
+ matrix["sessions"] = {
+ "status": "pass"
+ if os.path.isfile(os.path.join(session_dir, "session.json"))
+ and os.path.isfile(os.path.join(session_dir, "events.jsonl"))
+ else "fail",
+ "detail": {
+ "dir": _relpath(session_dir),
+ "event_counts": events,
+ "hashes": session_hashes(session_dir),
+ },
+ }
+
+ trace_dir = os.path.join(out_dir, "sessions", "features-trace")
+ _rm_tree(trace_dir)
+ trace_session = ge.launch(
+ kernel=DEMO_ELF,
+ backend="native",
+ record=trace_dir,
+ name="s4-features-trace",
+ trace={"exec": True, "max_events": 4000},
+ )
+ try:
+ trace_session.run(duration=0.1)
+ finally:
+ trace_session.close()
+ trace_path = os.path.join(trace_dir, "blobs", "trace.jsonl")
+ trace_lines = 0
+ if os.path.isfile(trace_path):
+ with open(trace_path, "r", encoding="utf-8", errors="replace") as fh:
+ trace_lines = sum(1 for _ in fh)
+ matrix["trace"] = {
+ "status": "pass" if trace_lines > 0 else "fail",
+ "detail": {
+ "dir": _relpath(trace_dir),
+ "blob": _relpath(trace_path),
+ "events": trace_lines,
+ "max_events": 4000,
+ "hashes": session_hashes(trace_dir),
+ },
+ }
+
+ if os.path.isfile(SDK_ELF):
+ from greeneye.inject import invoke_remote, load_service
+
+ inject_dir = os.path.join(out_dir, "sessions", "features-inject")
+ _rm_tree(inject_dir)
+ inject_session = ge.launch(
+ kernel=DEMO_ELF, backend="native", record=inject_dir, name="s4-features-inject"
+ )
+ try:
+ inject_session.run(duration=0.25)
+ service = load_service(inject_session, SDK_ELF, slot=6)
+ value = invoke_remote(inject_session, service, 1, 0x41)
+ finally:
+ inject_session.close()
+ matrix["injection"] = {
+ "status": "pass" if value == (0x41 ^ 0xBEEF) else "fail",
+ "detail": {
+ "dir": _relpath(inject_dir),
+ "blob": _relpath(SDK_ELF),
+ "slot": 6,
+ "ping_result": value,
+ "expected": 0x41 ^ 0xBEEF,
+ "hashes": session_hashes(inject_dir),
+ },
+ }
+ else:
+ matrix["injection"] = {
+ "status": "not_run",
+ "reason": "sdk/build/injected_demo.elf not built (run make sdk)",
+ }
+
+ matrix["mcp"] = {
+ "status": "not_run",
+ "reason": (
+ "native MCP surface is S3's deliverable (plugins/mcp_server.py); "
+ "S4 must not race that work. Re-run after S3 lands (docs/37)."
+ ),
+ }
+ analyze_dir = os.path.join(out_dir, "sessions", "native-1")
+ compare_dir = os.path.join(out_dir, "sessions", "native-2")
+ if os.path.isfile(os.path.join(analyze_dir, "session.json")) and os.path.isfile(
+ os.path.join(compare_dir, "session.json")
+ ):
+ try:
+ from greeneye.analyze import render_summary
+
+ summary = render_summary(analyze_dir)
+ delta_text = compare_sessions(analyze_dir, compare_dir)
+ matrix["analyze_compare"] = {
+ "status": "pass" if summary and delta_text else "fail",
+ "detail": {
+ "analyze_session": _relpath(analyze_dir),
+ "analyze_chars": len(summary),
+ "compare_pair": [_relpath(analyze_dir), _relpath(compare_dir)],
+ "compare_chars": len(delta_text),
+ "compare_headers": [
+ line[3:].strip()
+ for line in delta_text.splitlines()
+ if line.startswith("## ")
+ ],
+ },
+ }
+ except Exception as exc: # evidence step, never fatal to the matrix
+ matrix["analyze_compare"] = {
+ "status": "fail",
+ "reason": f"{type(exc).__name__}: {exc}",
+ }
+ else:
+ matrix["analyze_compare"] = {
+ "status": "not_run",
+ "reason": "native-1/native-2 sessions absent",
+ }
+ return {"ran": True, "reason": "ok", "features": matrix}
+
+
+# -- report ----------------------------------------------------------------
+
+
+def threshold_checks(report: dict, thresholds: dict | None = None) -> dict:
+ """Evaluate the frozen thresholds against a report; returns check -> bool."""
+ th = dict(FROZEN_THRESHOLDS)
+ th.update(thresholds or {})
+ qemu = report.get("qemu") or {}
+ native = report.get("native") or {}
+ checks: dict = {
+ "qemu_first_fault_reproduced": bool(
+ qemu.get("ran") and qemu.get("first_touch_match")
+ ),
+ "native_above_qemu": above_qemu_line(
+ qemu.get("rung"), native.get("rung"), int(th["native_rungs_above_qemu"])
+ ),
+ "identical_runs": bool(
+ native.get("ran")
+ and native.get("repeat", 0) >= int(th["identical_runs"])
+ and native.get("identical")
+ ),
+ "native_pair_wall_s": bool(
+ native.get("ran")
+ and float(native.get("pair_wall_s", 1e9)) <= float(th["native_pair_wall_s"])
+ ),
+ "device_model_documented": bool(
+ not th.get("device_model_required", True) or native.get("device_model_used")
+ ),
+ }
+ checks["all"] = all(checks.values())
+ return checks
+
+
+def build_report(
+ *,
+ image: str,
+ qemu: dict,
+ native: dict,
+ features: dict | None,
+ s1: dict,
+ thresholds: dict,
+) -> dict:
+ checks = threshold_checks({"qemu": qemu, "native": native}, thresholds)
+ rung_progress = bool(checks["native_above_qemu"] and checks["identical_runs"])
+ if not qemu.get("ran"):
+ verdict = "unverified_qemu_absent"
+ elif not checks["qemu_first_fault_reproduced"]:
+ verdict = "unverified_qemu_mismatch"
+ elif checks["all"]:
+ verdict = "above"
+ elif rung_progress and not checks["device_model_documented"]:
+ # Rung progress exists, but docs/35 1.1 also requires working device
+ # semantics; lenient MMIO cannot satisfy the line contract. Underclaim.
+ verdict = "above_lenient_only"
+ else:
+ verdict = "not_above"
+ return {
+ "schema": SCHEMA,
+ "harness": "harness/sim_qemu_line.py",
+ "line_definition": "docs/35-GREENEYE-SIMULATOR-DESIGN.md 1.1 + 3.3",
+ "firmware": {
+ "name": os.path.basename(image),
+ "sha256": firmware_sha256(image) if os.path.isfile(image) else None,
+ },
+ "rung_order": list(RUNG_ORDER),
+ "thresholds": thresholds,
+ "threshold_checks": checks,
+ "verdict": verdict,
+ "verdict_explanation": {
+ "above": "all frozen checks passed: QEMU fault reproduced, native >= QEMU + 1 rung, identical runs, wall bound, documented device model",
+ "above_lenient_only": "native progressed past the QEMU fault, but lenient MMIO is not a documented device model (docs/35 1.1) - contract NOT satisfied",
+ "not_above": "one or more frozen checks failed and no rung-progress excuse applies",
+ "unverified_qemu_absent": "QEMU baseline unavailable; no A/B comparison is possible",
+ "unverified_qemu_mismatch": "QEMU ran but did not reproduce its documented first fault; the rungs are not comparable",
+ }.get(verdict, ""),
+ "rung_progress": rung_progress,
+ "above_qemu_line": verdict == "above",
+ "qemu": {k: v for k, v in qemu.items() if k != "device_map"},
+ "native": native,
+ "features": features,
+ "s1_integration": {
+ "source": s1.get("source"),
+ "profile": _relpath(s1.get("profile")),
+ "declared_rung": s1.get("declared_rung"),
+ "notes": s1.get("notes", []),
+ },
+ "x86_vms_running": _x86_vms_running(),
+ "not_verified": [
+ "lenient MMIO (B0) is observation, not device semantics",
+ "B3/B4 are not reached: the S1 device model stops at the B2 DMEM-runspace unknown",
+ "the boot shim values (bootargs/WPR meta/DMEM mirror) are declared assumptions",
+ "MCP on native is S3-owned and not exercised here",
+ "L2 timing: cycles are modeled (docs/32), never silicon claims",
+ "no hardware/GPU/guest/module involvement by design",
+ ],
+ "rerun_when_s1_lands": [
+ "python3 harness/sim_qemu_line.py",
+ "S1 landed (2026-10-03): sim/profiles/*.json is discovered and the native side runs sim.gsp_boot",
+ "sim/devices/*.json with greeneye-device-map/1 (or a device_map key) is still picked up as an observational map",
+ ],
+ }
+
+
+def render_markdown(report: dict) -> str:
+ qemu = report.get("qemu") or {}
+ native = report.get("native") or {}
+ lines = [
+ "# Simulator vs the QEMU line - measured report",
+ "",
+ f"* schema: `{report.get('schema')}`",
+ f"* harness: `{report.get('harness')}`",
+ f"* line: {report.get('line_definition')}",
+ f"* firmware: `{(report.get('firmware') or {}).get('name')}` "
+ f"sha256 `{((report.get('firmware') or {}).get('sha256') or '')[:16]}...`",
+ f"* verdict: **{report.get('verdict')}** "
+ f"(above_qemu_line={report.get('above_qemu_line')})",
+ f"* verdict meaning: {report.get('verdict_explanation') or '-'}",
+ "",
+ "## Rung table",
+ "",
+ "| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence |",
+ "|---|---|---|---:|---:|---|---:|---|",
+ ]
+ runs = native.get("runs") or []
+ lines.append(
+ f"| QEMU probe | {qemu.get('ran')} | {qemu.get('rung')} | - | - | "
+ f"{'fault' if qemu.get('first_fault') else '-'} | {qemu.get('wall_s')} | "
+ f"`{qemu.get('session') or qemu.get('reason')}` |"
+ )
+ for index, run in enumerate(runs, start=1):
+ stop = run.get("stop") or {}
+ pc_fw = stop.get("pc_fw")
+ pc_fw_text = f"{pc_fw:#x}" if pc_fw is not None else "-"
+ lines.append(
+ f"| native run {index} | True | {run.get('rung')} | {run.get('instructions')} | "
+ f"{run.get('cycles')} | {stop.get('reason')}@{pc_fw_text} | "
+ f"{run.get('wall_s')} | `{run.get('session')}` |"
+ )
+ if not runs:
+ lines.append(f"| native | {native.get('ran')} | - | - | - | - | - | {native.get('reason')} |")
+
+ first = qemu.get("first_fault") or {}
+ lines += [
+ "",
+ "### QEMU first fault",
+ "",
+ f"* {first.get('desc')} at fw vaddr `{first.get('epc_fw') if first.get('epc_fw') is not None else 0:#x}` "
+ f"accessing `{(first.get('access_addr') or 0):#x}` ({first.get('size')} bytes, {first.get('rw')})",
+ f"* first-touch matches the documented reference: {qemu.get('first_touch_match')}",
+ f"* fault records in the capped log window: {qemu.get('fault_count_window')}",
+ "",
+ "## Frozen thresholds",
+ "",
+ "| check | pass |",
+ "|---|---|",
+ ]
+ for name, value in (report.get("threshold_checks") or {}).items():
+ lines.append(f"| {name} | {value} |")
+ lines += [
+ "",
+ f"Thresholds (frozen {report['thresholds'].get('frozen_on')}): "
+ f"native >= QEMU + {report['thresholds'].get('native_rungs_above_qemu')} rung(s); "
+ f"{report['thresholds'].get('identical_runs')} identical runs; "
+ f"pair wall <= {report['thresholds'].get('native_pair_wall_s')} s.",
+ "",
+ "## Determinism",
+ "",
+ f"* identical across {native.get('repeat')} native runs: {native.get('identical')}",
+ f"* session compare ran: {(native.get('compare') or {}).get('ran')} "
+ f"({(native.get('compare') or {}).get('headers')})",
+ "",
+ "## Session hashes (sha256)",
+ "",
+ "| session | file | sha256 |",
+ "|---|---|---|",
+ ]
+ hash_rows = [("qemu-probe", qemu.get("session"), qemu.get("session_hashes") or {})]
+ for index, run in enumerate(runs, start=1):
+ hash_rows.append((f"native-{index}", run.get("session"), run.get("session_hashes") or {}))
+ features_detail = ((report.get("features") or {}).get("features") or {})
+ for feature in ("sessions", "trace", "injection"):
+ detail = (features_detail.get(feature) or {}).get("detail") or {}
+ if isinstance(detail, dict):
+ hashes = detail.get("hashes") or {}
+ if hashes:
+ hash_rows.append((feature, detail.get("dir") or detail.get("blob"), hashes))
+ for label, session, hashes in hash_rows:
+ if not hashes:
+ lines.append(f"| {label} | - | (no hashes recorded) |")
+ continue
+ for name in sorted(hashes):
+ lines.append(f"| {label} | {name} | `{hashes[name]}` |")
+ lines += [
+ "",
+ "## Native feature matrix (demo firmware)",
+ "",
+ "| feature | status | detail |",
+ "|---|---|---|",
+ ]
+ feature_block = (report.get("features") or {}).get("features") or {}
+ for name in sorted(feature_block):
+ entry = feature_block[name]
+ detail = entry.get("detail") or entry.get("reason") or ""
+ lines.append(f"| {name} | {entry.get('status')} | {detail} |")
+ if not feature_block:
+ lines.append(f"| - | not_run | {(report.get('features') or {}).get('reason')} |")
+ lines += [
+ "",
+ "## S1 integration",
+ "",
+ f"* device source: {report['s1_integration'].get('source') or 'lenient (no S1 device model found)'}",
+ f"* profile: {report['s1_integration'].get('profile') or '-'}",
+ f"* declared rung: {report['s1_integration'].get('declared_rung')}",
+ ]
+ for note in report["s1_integration"].get("notes") or []:
+ lines.append(f"* note: {note}")
+ lines += [
+ "",
+ "## Not verified",
+ "",
+ ]
+ for item in report.get("not_verified") or []:
+ lines.append(f"* {item}")
+ lines += [
+ "",
+ "## S1 integration notes",
+ "",
+ ]
+ for item in report.get("rerun_when_s1_lands") or []:
+ lines.append(f"* `{item}`" if item.startswith("python3") else f"* {item}")
+ lines += [
+ "",
+ f"* other agents' x86 QEMU VMs observed (never touched): {report.get('x86_vms_running')}",
+ "",
+ ]
+ return "\n".join(lines)
+
+
+def run_harness(
+ *,
+ image: str = DEFAULT_IMAGE,
+ out_dir: str = DEFAULT_OUT,
+ instructions: int = DEFAULT_INSTRUCTIONS,
+ qemu_duration: float = DEFAULT_QEMU_DURATION_S,
+ repeat: int = 2,
+ device_map: str | None = None,
+ features: bool = True,
+ thresholds: dict | None = None,
+ run_qemu: bool = True,
+) -> dict:
+ """Run the full A/B harness and write ``sim-qemu-line.json`` + ``.md``."""
+ os.makedirs(out_dir, exist_ok=True)
+ frozen = dict(FROZEN_THRESHOLDS)
+ frozen.update(thresholds or {})
+
+ if device_map is not None:
+ dm = DeviceMap.load(device_map)
+ s1 = {
+ "device_map": dm,
+ "profile": None,
+ "declared_rung": None,
+ "source": _relpath(device_map),
+ "notes": ["explicit --device-map override"],
+ }
+ else:
+ sha = firmware_sha256(image) if os.path.isfile(image) else None
+ s1 = discover_s1_device_model(image, sha)
+
+ qemu = (
+ run_qemu_baseline(image=image, out_dir=out_dir, duration=qemu_duration)
+ if run_qemu
+ else {"backend": "qemu", "ran": False, "reason": "qemu=False", "rung": None}
+ )
+
+ native = run_native_baseline(
+ image=image,
+ out_dir=out_dir,
+ instructions=instructions,
+ repeat=repeat,
+ device_map=s1.get("device_map"),
+ declared_rung=s1.get("declared_rung"),
+ profile=s1.get("profile"),
+ )
+ if not native.get("ran"):
+ report = build_report(
+ image=image, qemu=qemu, native=native, features=None, s1=s1, thresholds=frozen
+ )
+ else:
+ feature_result = run_feature_matrix(out_dir) if features else None
+ report = build_report(
+ image=image,
+ qemu=qemu,
+ native=native,
+ features=feature_result,
+ s1=s1,
+ thresholds=frozen,
+ )
+
+ json_path = _write_json(os.path.join(out_dir, "sim-qemu-line.json"), report)
+ md_path = _write_text(os.path.join(out_dir, "sim-qemu-line.md"), render_markdown(report))
+ report["report_json"] = _relpath(json_path)
+ report["report_md"] = _relpath(md_path)
+ return report
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(
+ prog="python3 harness/sim_qemu_line.py",
+ description=(
+ "S4 A/B: QEMU probe vs native GSP simulator on the real 570.144 image; "
+ "emits sim-qemu-line.json + .md. Bounded, no hardware."
+ ),
+ )
+ parser.add_argument("--image", default=DEFAULT_IMAGE, help="flat GSP firmware image")
+ parser.add_argument("--out", default=DEFAULT_OUT, help="report directory")
+ parser.add_argument(
+ "--instructions", type=int, default=DEFAULT_INSTRUCTIONS,
+ help=f"native instruction budget per run (<= {MAX_INSTRUCTIONS})",
+ )
+ parser.add_argument(
+ "--qemu-duration", type=float, default=DEFAULT_QEMU_DURATION_S,
+ help=f"QEMU wall-clock probe bound (<= {MAX_QEMU_DURATION_S}s)",
+ )
+ parser.add_argument("--repeat", type=int, default=2, help="native runs for determinism")
+ parser.add_argument("--device-map", help="explicit greeneye-device-map/1 file (skips discovery)")
+ parser.add_argument("--no-qemu", action="store_true", help="native only (no baseline claim)")
+ parser.add_argument("--no-features", action="store_true", help="skip the feature matrix")
+ parser.add_argument("--json", action="store_true", help="print the report JSON")
+ args = parser.parse_args(argv)
+
+ try:
+ report = run_harness(
+ image=args.image,
+ out_dir=args.out,
+ instructions=args.instructions,
+ qemu_duration=args.qemu_duration,
+ repeat=args.repeat,
+ device_map=args.device_map,
+ features=not args.no_features,
+ run_qemu=not args.no_qemu,
+ )
+ except (HarnessError, GreenEyeError) as exc:
+ print(f"[sim-qemu-line] error: {exc}", file=sys.stderr)
+ return 1
+
+ if not os.path.isfile(args.image):
+ print(f"[sim-qemu-line] skipped: firmware absent: {args.image}", file=sys.stderr)
+ return 2
+ if args.json:
+ print(json.dumps(report, indent=1, sort_keys=True, default=str))
+ native = report.get("native") or {}
+ print(f"[sim-qemu-line] qemu rung: {(report.get('qemu') or {}).get('rung')}")
+ print(f"[sim-qemu-line] native rung: {native.get('rung')} (identical={native.get('identical')})")
+ print(f"[sim-qemu-line] verdict: {report.get('verdict')}")
+ print(f"[sim-qemu-line] thresholds: {report.get('threshold_checks')}")
+ print(f"[sim-qemu-line] report: {report.get('report_json')} + {report.get('report_md')}")
+ return 0 if report.get("above_qemu_line") else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/debug/risc-v-gsp-rm-vm/harness/triage.py b/debug/risc-v-gsp-rm-vm/harness/triage.py
new file mode 100644
index 0000000..2175e48
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/triage.py
@@ -0,0 +1,875 @@
+#!/usr/bin/env python3
+"""One-command new-version GSP firmware triage (docs/40, goal U46).
+
+For a NEW GSP firmware image (optionally against a ``--baseline``) this tool
+answers, in one bounded run and with one report:
+
+* **what it is** - sha256, size, ELF shape and version label (explicit
+ ``--version``, the ``gsp_version.txt`` sidecar convention, parent directory,
+ file stem, in that order);
+* **where it boots to** - the existing QEMU probe path (first exception /
+ first touch) and the S1 native boot ladder (``python3 -m sim.gsp_boot``,
+ rungs B0-B4);
+* **how the native backend compares** - the existing ``harness/sim_qemu_line``
+ A/B harness (QEMU vs native, deterministic repeat, frozen thresholds);
+* **what changed vs the baseline** - the existing ``harness/version_diff``
+ (image/symbol/corpus/probe/behavior sections), when ``--baseline`` is given;
+* **what capability surface is present** - hooks/watch/memory/session API,
+ native simulator, TCG trace plugin, MCP, demo, boot ladder;
+* **what work ``vgsp`` support needs** - a ``support_readiness`` verdict with
+ blockers and ordered next actions.
+
+Every step is wrapped: a failing step is recorded (``status: error``) and the
+others still run. Every session a step uses is listed with sha256 hashes.
+Nothing is downloaded; no hardware, GPU, mdev, kernel module or guest is
+touched; QEMU is the only process launched besides the bounded native boot.
+
+This is the flagship workflow of the simulator program (handoff
+``U46``) and remains **in development** (docs/40). The frozen value
+thresholds below were recorded on 2026-10-03 *before* the first measurement
+(docs/40 section 5); never move them silently.
+
+Run from the tool root::
+
+ python3 harness/triage.py --firmware ../../firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin --version 570.144 --out dumps/triage-570.144
+ python3 harness/triage.py --firmware <new> --baseline <old> --out dumps/triage-570.211 --dry-run
+"""
+from __future__ import annotations
+
+import argparse
+import importlib.util
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import time
+from dataclasses import dataclass
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+TOOL_ROOT = os.path.dirname(HERE)
+if TOOL_ROOT not in sys.path:
+ sys.path.insert(0, TOOL_ROOT)
+
+from harness.common import ( # noqa: E402
+ DEMO_ELF,
+ ensure_dir,
+ qemu_available,
+ sha256_file,
+ write_json,
+ write_text,
+)
+from harness.sim_qemu_line import ( # noqa: E402
+ MAX_INSTRUCTIONS,
+ run_harness,
+ run_qemu_baseline,
+ rung_ordinal,
+ session_hashes,
+)
+from harness.version_diff import ( # noqa: E402
+ RM610_TOKEN,
+ Rm610GateError,
+ VersionDiffOptions,
+ check_rm610_gate,
+ run_version_diff,
+)
+
+FORMAT = "greeneye-new-version-triage/1"
+PLAN_FORMAT = "greeneye-new-version-triage-plan/1"
+
+#: Instruction budget bounds (task rule: <= 2M per attempt).
+DEFAULT_BUDGET = 200_000
+#: QEMU probe wall-clock bound (matches the S4 harness default).
+DEFAULT_QEMU_DURATION_S = 0.5
+#: Per-step wall-clock net (subprocess wedge protection; no unbounded runs).
+DEFAULT_STEP_TIMEOUT_S = 300.0
+#: Native rung at which runtime observation (not just first touch) is possible.
+OBSERVABLE_RUNG = "B2"
+
+#: Frozen 2026-10-03, before the first measured triage run (docs/40 section 5).
+#: ``manual_baseline_s`` is the *conservative understatement* of the manual
+#: steps this one command replaces (run+interpret probe, native boot ladder,
+#: A/B, session hash collection and write-up for one new image).
+FROZEN_THRESHOLDS = {
+ "frozen_on": "2026-10-03",
+ "manual_baseline_s": 1800.0,
+ "triage_wall_s_max": 600.0,
+ "required_steps": ("identity", "probe", "native_boot", "ab_eval", "features"),
+}
+
+VERSION_RE = re.compile(r"v?(\d{3}\.\d+)([A-Za-z0-9._-]*)?")
+_UNAVAILABLE_MARKERS = (
+ "no module named",
+ "modulenotfounderror",
+ "importerror",
+ "cannot import",
+ "profile absent",
+ "image absent",
+ "unavailable",
+)
+
+
+# ---------------------------------------------------------------------------
+# options and helpers
+# ---------------------------------------------------------------------------
+
+
+@dataclass
+class TriageOptions:
+ firmware: str
+ baseline: str | None = None
+ version: str | None = None
+ baseline_label: str | None = None
+ out: str | None = None
+ budget: int = DEFAULT_BUDGET
+ qemu_duration: float = DEFAULT_QEMU_DURATION_S
+ step_timeout: float = DEFAULT_STEP_TIMEOUT_S
+ release: str | None = None
+ approval_token: str | None = None
+ dry_run: bool = False
+
+ def labels(self) -> list[str]:
+ return [
+ value
+ for value in (
+ self.release,
+ self.version,
+ self.baseline_label,
+ os.path.basename(self.firmware) if self.firmware else None,
+ os.path.basename(self.baseline) if self.baseline else None,
+ )
+ if value
+ ]
+
+
+def _rm_tree(path: str) -> None:
+ if path and os.path.isdir(path):
+ shutil.rmtree(path, ignore_errors=True)
+
+
+def _rel(path: str, root: str) -> str:
+ try:
+ return os.path.relpath(path, root)
+ except ValueError:
+ return path
+
+
+def _abs(path: str | None) -> str | None:
+ """Absolute, normalised path; session dirs from the harness may be tool-root relative."""
+ if not path:
+ return None
+ return os.path.normpath(path if os.path.isabs(path) else os.path.join(TOOL_ROOT, path))
+
+
+def _first_line(text: str | None, limit: int = 300) -> str | None:
+ if not text:
+ return None
+ line = text.strip().splitlines()[-1].strip() if text.strip() else ""
+ return line[:limit] or None
+
+
+def _looks_unavailable(text: str) -> bool:
+ lowered = (text or "").lower()
+ return any(marker in lowered for marker in _UNAVAILABLE_MARKERS)
+
+
+def extract_version(firmware: str, explicit: str | None = None) -> tuple[str | None, str]:
+ """Version label: explicit, ``gsp_version.txt``, parent dir, file stem.
+
+ The sidecar convention mirrors the existing probe/device-map helper
+ (``greeneye/device_map.py``); no firmware content is guessed.
+ """
+ if explicit:
+ return explicit, "explicit --version"
+ sidecar = os.path.join(os.path.dirname(os.path.abspath(firmware)), "gsp_version.txt")
+ if os.path.isfile(sidecar):
+ try:
+ with open(sidecar, "r", encoding="utf-8", errors="replace") as fh:
+ text = "".join(ch for ch in fh.read() if ch.isprintable()).strip()
+ if text:
+ return text, "gsp_version.txt sidecar"
+ except OSError:
+ pass
+ parent = os.path.basename(os.path.dirname(os.path.abspath(firmware)))
+ if parent and VERSION_RE.fullmatch(parent):
+ return parent, "parent directory name"
+ stem = os.path.splitext(os.path.basename(firmware))[0]
+ return (stem or None), "file stem"
+
+
+def _target_brief(path: str, label: str | None = None) -> dict:
+ info: dict = {"path": path, "name": os.path.basename(path)}
+ version, version_source = extract_version(path, label)
+ info["version"] = version
+ info["version_source"] = version_source
+ if os.path.isfile(path):
+ info["sha256"] = sha256_file(path)
+ info["size"] = os.path.getsize(path)
+ else:
+ info["error"] = "file not found"
+ return info
+
+
+# ---------------------------------------------------------------------------
+# steps
+# ---------------------------------------------------------------------------
+
+
+def step_identity(options: TriageOptions, out_dir: str) -> dict:
+ firmware = _target_brief(options.firmware, None)
+ version, version_source = extract_version(options.firmware, options.version)
+ firmware["version"] = version
+ firmware["version_source"] = version_source
+ if not os.path.isfile(options.firmware):
+ return {
+ "status": "error",
+ "reason": f"firmware not found: {options.firmware}",
+ "firmware": firmware,
+ }
+ result: dict = {"status": "ok", "firmware": firmware}
+ try:
+ from greeneye.elf import load_elf
+
+ image = load_elf(options.firmware)
+ result["elf"] = {
+ "is_riscv": bool(image.is_riscv),
+ "entry": image.entry,
+ "min_vaddr": image.min_vaddr,
+ "max_vaddr": image.max_vaddr,
+ "segments": len(getattr(image, "segments", ()) or ()),
+ }
+ except Exception as exc:
+ result["elf"] = {"unreadable": f"{type(exc).__name__}: {exc}"}
+ if options.baseline:
+ result["baseline"] = _target_brief(options.baseline, options.baseline_label)
+ return result
+
+
+def step_probe(options: TriageOptions, out_dir: str) -> dict:
+ result = run_qemu_baseline(
+ image=options.firmware, out_dir=out_dir, duration=options.qemu_duration
+ )
+ detail: dict = {
+ "status": "ok" if result.get("ran") else "skipped",
+ "reason": result.get("reason"),
+ "rung": result.get("rung"),
+ "stopped_pc": result.get("stopped_pc"),
+ "first_fault": result.get("first_fault"),
+ "first_touch_match": result.get("first_touch_match"),
+ "fault_count_window": result.get("fault_count_window"),
+ "device_map_entries": result.get("device_map_entries"),
+ "wall_s": result.get("wall_s"),
+ "sessions": [],
+ }
+ session = _abs(result.get("session"))
+ if session:
+ detail["sessions"].append({"name": "qemu-probe", "dir": session})
+ detail["session_hashes"] = result.get("session_hashes") or session_hashes(session)
+ return detail
+
+
+def step_native_boot(options: TriageOptions, out_dir: str) -> dict:
+ session_dir = os.path.join(out_dir, "sessions", "native-boot")
+ _rm_tree(session_dir)
+ argv = [
+ sys.executable,
+ "-m",
+ "sim.gsp_boot",
+ "--image",
+ options.firmware,
+ "--instructions",
+ str(options.budget),
+ "--record",
+ session_dir,
+ "--name",
+ "triage-native-boot",
+ "--json",
+ ]
+ try:
+ proc = subprocess.run(
+ argv,
+ cwd=TOOL_ROOT,
+ capture_output=True,
+ text=True,
+ timeout=options.step_timeout,
+ )
+ except subprocess.TimeoutExpired:
+ return {"status": "error", "reason": f"native boot timed out after {options.step_timeout}s"}
+ except OSError as exc:
+ return {"status": "skipped", "reason": f"native boot ladder unavailable: {exc}"}
+
+ stdout, stderr = proc.stdout or "", proc.stderr or ""
+ payload = None
+ if stdout.strip():
+ try:
+ payload = json.loads(stdout)
+ except ValueError:
+ payload = None
+ if proc.returncode == 0 and isinstance(payload, dict):
+ session = _abs(payload.get("session")) or session_dir
+ return {
+ "status": "ok",
+ "rung": payload.get("rung"),
+ "instructions_executed": payload.get("instructions_executed"),
+ "cycles": payload.get("cycles"),
+ "stop": payload.get("stop"),
+ "first_unknown_mmio": payload.get("first_unknown_mmio"),
+ "mmio_unknown": payload.get("mmio_unknown"),
+ "device_accesses": payload.get("device_accesses"),
+ "milestones": payload.get("milestones"),
+ "sessions": [{"name": "native-boot", "dir": session}],
+ "session_hashes": session_hashes(session),
+ }
+ reason = _first_line(stderr) or _first_line(stdout) or f"exit {proc.returncode}"
+ status = "skipped" if _looks_unavailable(stderr + stdout) or proc.returncode == 2 else "error"
+ if os.path.isdir(session_dir) and os.listdir(session_dir):
+ return {
+ "status": status,
+ "reason": reason,
+ "sessions": [{"name": "native-boot", "dir": session_dir}],
+ "session_hashes": session_hashes(session_dir),
+ }
+ return {"status": status, "reason": reason}
+
+
+def step_ab_eval(options: TriageOptions, out_dir: str) -> dict:
+ if not os.path.isfile(options.firmware):
+ return {"status": "skipped", "reason": f"firmware absent: {options.firmware}"}
+ ab_dir = os.path.join(out_dir, "ab-eval")
+ report = run_harness(
+ image=options.firmware,
+ out_dir=ab_dir,
+ instructions=options.budget,
+ qemu_duration=options.qemu_duration,
+ repeat=2,
+ features=False,
+ run_qemu=True,
+ )
+ qemu = report.get("qemu") or {}
+ native = report.get("native") or {}
+ detail: dict = {
+ "status": "ok" if native.get("ran") else "skipped",
+ "reason": native.get("reason") if not native.get("ran") else None,
+ "qemu_rung": qemu.get("rung"),
+ "native_rung": native.get("rung"),
+ "native_identical": native.get("identical"),
+ "native_repeat": native.get("repeat"),
+ "above_qemu_line": report.get("above_qemu_line"),
+ "verdict": report.get("verdict"),
+ "threshold_checks": report.get("threshold_checks"),
+ "report_json": _abs(report.get("report_json")),
+ "report_md": _abs(report.get("report_md")),
+ "sessions": [],
+ }
+ qemu_session = _abs(qemu.get("session"))
+ if qemu_session:
+ detail["sessions"].append({"name": "ab-qemu", "dir": qemu_session})
+ for index, run in enumerate(native.get("runs") or [], start=1):
+ session = _abs(run.get("session")) or os.path.join(ab_dir, "sessions", f"native-{index}")
+ detail["sessions"].append({"name": f"ab-native-{index}", "dir": session})
+ return detail
+
+
+def step_version_diff(options: TriageOptions, out_dir: str) -> dict:
+ if not options.baseline:
+ return {"status": "skipped", "reason": "no --baseline supplied"}
+ if not os.path.isfile(options.baseline):
+ return {"status": "skipped", "reason": f"baseline absent: {options.baseline}"}
+ vd_dir = os.path.join(out_dir, "version-diff")
+ old_version, _ = extract_version(options.baseline, options.baseline_label)
+ report = run_version_diff(
+ VersionDiffOptions(
+ old=options.baseline,
+ new=options.firmware,
+ out=vd_dir,
+ old_label=options.baseline_label or old_version,
+ new_label=options.version,
+ release=options.release,
+ probe_instructions=max(1, min(options.budget, 2000)),
+ behavior_duration=0.4,
+ approval_token=options.approval_token,
+ )
+ )
+ sections = {name: section.get("status") for name, section in report["sections"].items()}
+ symbols = report["sections"].get("symbols") or {}
+ detail: dict = {
+ "status": "error" if "error" in sections.values() else "ok",
+ "reason": None,
+ "old_label": report.get("old", {}).get("label"),
+ "new_label": report.get("new", {}).get("label"),
+ "sections": sections,
+ "sections_skipped": report.get("summary", {}).get("sections_skipped"),
+ "sections_error": report.get("summary", {}).get("sections_error"),
+ "report_json": os.path.join(vd_dir, "version-diff.json"),
+ "report_md": os.path.join(vd_dir, "version-diff.md"),
+ "image_changes": _image_changes(report),
+ "symbol_changes": _symbol_changes(symbols),
+ "sessions": [],
+ }
+ sessions_root = os.path.join(vd_dir, "sessions")
+ if os.path.isdir(sessions_root):
+ for name in sorted(os.listdir(sessions_root)):
+ path = os.path.join(sessions_root, name)
+ if os.path.isdir(path):
+ detail["sessions"].append({"name": f"version-diff/{name}", "dir": path})
+ return detail
+
+
+def _image_changes(report: dict) -> dict:
+ section = (report.get("sections") or {}).get("image") or {}
+ return {
+ "status": section.get("status"),
+ "same": section.get("same"),
+ "deltas": section.get("deltas"),
+ }
+
+
+def _symbol_changes(symbols: dict) -> dict:
+ totals = symbols.get("totals") or {}
+ return {
+ "status": symbols.get("status"),
+ "reason": symbols.get("reason"),
+ "shift": symbols.get("shift"),
+ "matched_named": symbols.get("matched_named"),
+ "matched_shifted": symbols.get("matched_shifted"),
+ "added": totals.get("added"),
+ "removed": totals.get("removed"),
+ "moved": totals.get("moved"),
+ "resized_named": totals.get("resized_named"),
+ }
+
+
+def _spec_available(module: str) -> bool:
+ try:
+ return importlib.util.find_spec(module) is not None
+ except (ImportError, ValueError):
+ return False
+
+
+def step_features(options: TriageOptions, out_dir: str) -> dict:
+ checks: dict = {}
+ notes: list[str] = []
+ try:
+ import greeneye as ge
+
+ checks["greeneye_version"] = ge.__version__
+ checks["hooks_watch_step"] = all(
+ hasattr(ge.Session, name) for name in ("hook", "watch", "run", "step", "close")
+ )
+ checks["memory_struct"] = hasattr(ge, "Memory")
+ checks["native_backend"] = _spec_available("greeneye.sim_backend")
+ except Exception as exc:
+ notes.append(f"greeneye import failed: {type(exc).__name__}: {exc}")
+ checks.update({"greeneye_version": None, "hooks_watch_step": False, "memory_struct": False, "native_backend": False})
+ checks["native_boot_ladder"] = os.path.isfile(os.path.join(TOOL_ROOT, "sim", "gsp_boot.py")) and os.path.isdir(
+ os.path.join(TOOL_ROOT, "sim", "profiles")
+ )
+ checks["ab_harness"] = os.path.isfile(os.path.join(TOOL_ROOT, "harness", "sim_qemu_line.py"))
+ checks["version_diff"] = os.path.isfile(os.path.join(TOOL_ROOT, "harness", "version_diff.py"))
+ checks["tcg_trace_plugin"] = os.path.isfile(os.path.join(TOOL_ROOT, "qemu", "greeneye-trace.so"))
+ checks["mcp_server"] = os.path.isfile(os.path.join(TOOL_ROOT, "plugins", "mcp_server.py"))
+ checks["demo_firmware"] = os.path.isfile(DEMO_ELF)
+ checks["qemu"] = qemu_available()
+ unavailable = sorted(name for name, value in checks.items() if value is False)
+ if unavailable:
+ notes.append("unavailable: " + ", ".join(unavailable))
+ return {"status": "ok", "checks": checks, "unavailable": unavailable, "notes": notes}
+
+
+DEFAULT_RUNNERS = (
+ ("identity", step_identity),
+ ("probe", step_probe),
+ ("native_boot", step_native_boot),
+ ("ab_eval", step_ab_eval),
+ ("version_diff", step_version_diff),
+ ("features", step_features),
+)
+
+
+# ---------------------------------------------------------------------------
+# readiness and report assembly
+# ---------------------------------------------------------------------------
+
+
+def support_readiness(report: dict) -> dict:
+ """Blockers and next actions derived from the recorded step results."""
+ steps = {step["name"]: step for step in report["steps"]}
+ blockers: list[str] = []
+ next_actions: list[str] = []
+ identity = steps.get("identity", {})
+ if identity.get("status") != "ok":
+ blockers.append(f"firmware identity failed: {identity.get('reason') or 'unreadable'}")
+ probe = steps.get("probe", {})
+ if probe.get("status") == "error":
+ blockers.append(f"QEMU probe failed: {probe.get('reason')}")
+ native = steps.get("native_boot", {})
+ native_rung = native.get("rung")
+ native_ok = native.get("status") == "ok"
+ if not native_ok:
+ next_actions.append(
+ "native boot ladder unavailable/again incomplete "
+ f"({native.get('reason') or native.get('status')}): the S1 device model/profile is the enabler"
+ )
+ elif rung_ordinal(native_rung) is not None and rung_ordinal(native_rung) >= rung_ordinal(OBSERVABLE_RUNG):
+ next_actions.append(
+ f"native ladder is at {native_rung} (runtime-observable): inspect the milestone/stop with "
+ "hooks and watchpoints, then route the finding through the accuracy loop"
+ )
+ else:
+ next_actions.append(
+ f"native ladder stops at {native_rung}: extend the device model past the next unknown MMIO "
+ "before claiming runtime observation"
+ )
+ ab = steps.get("ab_eval", {})
+ if ab.get("status") == "ok" and not ab.get("above_qemu_line"):
+ next_actions.append(
+ f"native A/B is not yet above the QEMU line (verdict={ab.get('verdict')}): "
+ "device model and documented shims gate the claim"
+ )
+ vd = steps.get("version_diff", {})
+ baseline_supplied = bool(report.get("input", {}).get("baseline_supplied"))
+ if vd.get("status") == "error":
+ blockers.append(f"version diff errored: {vd.get('sections_error') or vd.get('reason')}")
+ elif vd.get("status") == "ok" and baseline_supplied:
+ skipped = vd.get("sections_skipped") or {}
+ if "symbols" in skipped:
+ next_actions.append(
+ "symbol inventory skipped: supply Ghidra exports / symbol DBs so the handler "
+ "inventory delta can gate the vgsp support scope"
+ )
+ if "corpus" in skipped:
+ next_actions.append(
+ "corpus diff skipped: supply --corpus-diff indexes for decompiled-change hints"
+ )
+ else:
+ next_actions.append(
+ "re-run with --baseline <previous.fwimage> for the symbol/behavior delta "
+ "(the release-to-support gate needs the A/B)"
+ )
+ features = steps.get("features", {})
+ for name in features.get("unavailable") or []:
+ if name in ("qemu", "native_boot_ladder", "hooks_watch_step", "memory_struct"):
+ next_actions.append(f"capability unavailable: {name}")
+ if not blockers and native_ok and rung_ordinal(native_rung) is not None and rung_ordinal(native_rung) >= rung_ordinal(OBSERVABLE_RUNG):
+ verdict = "sim-observable"
+ elif not blockers:
+ verdict = "first-touch-only"
+ else:
+ verdict = "blocked"
+ return {
+ "verdict": verdict,
+ "baseline_compared": bool(baseline_supplied and vd.get("status") == "ok"),
+ "observable_rung": OBSERVABLE_RUNG,
+ "blockers": blockers,
+ "next_actions": next_actions,
+ }
+
+
+def _collect_sessions(steps: list[dict], out_dir: str) -> list[dict]:
+ rows: list[dict] = []
+ seen: set[str] = set()
+ for step in steps:
+ for entry in step.get("sessions") or []:
+ path = _abs(entry.get("dir"))
+ if not path or path in seen:
+ continue
+ seen.add(path)
+ hashes = step.get("session_hashes") or {}
+ rows.append(
+ {
+ "step": step["name"],
+ "name": entry.get("name") or step["name"],
+ "dir": _rel(path, out_dir),
+ "present": os.path.isdir(path),
+ "hashes": hashes or session_hashes(path),
+ }
+ )
+ return rows
+
+
+def render_markdown(report: dict) -> str:
+ firmware = report["input"].get("firmware") or {}
+ steps = report["steps"]
+ readiness = report["support_readiness"]
+ lines = [
+ f"# New-version triage — {firmware.get('version') or firmware.get('name')}",
+ "",
+ "> Status: in development (docs/40). Simulator-only: no hardware, GPU, mdev,",
+ "> kernel module or guest was used. Frozen thresholds were recorded before the run.",
+ "",
+ f"* format: `{report['format']}`",
+ f"* generated: {report['generated_at']}",
+ f"* wall: {report['wall_s']}s (budget {report['input'].get('budget')} instructions/attempt)",
+ f"* RM610 gate: {report['rm610_gate']['status']}",
+ "",
+ "## Firmware",
+ "",
+ "| field | value |",
+ "|---|---|",
+ f"| path | `{firmware.get('path')}` |",
+ f"| sha256 | `{firmware.get('sha256')}` |",
+ f"| size | {firmware.get('size')} |",
+ f"| version | {firmware.get('version')} ({firmware.get('version_source')}) |",
+ f"| ELF | {json.dumps(report['input'].get('elf') or {}, sort_keys=True)} |",
+ ]
+ baseline = report["input"].get("baseline")
+ if baseline:
+ lines += [
+ f"| baseline | `{baseline.get('name')}` {baseline.get('sha256')} |",
+ f"| baseline version | {baseline.get('version')} ({baseline.get('version_source')}) |",
+ ]
+ lines += ["", "## Steps", "", "| step | status | wall_s | key result |", "|---|---|---|---|"]
+ for step in steps:
+ key = (
+ step.get("reason")
+ or step.get("verdict")
+ or step.get("rung")
+ or step.get("status")
+ )
+ lines.append(f"| {step['name']} | {step['status']} | {step.get('wall_s')} | {str(key)[:120]} |")
+ lines += [
+ "",
+ "## Support readiness",
+ "",
+ f"* verdict: **{readiness['verdict']}**",
+ f"* baseline compared: {readiness['baseline_compared']}",
+ ]
+ for label, items in (("blockers", readiness["blockers"]), ("next actions", readiness["next_actions"])):
+ lines.append(f"* {label}:")
+ if not items:
+ lines.append(" * none")
+ for item in items:
+ lines.append(f" * {item}")
+ sessions = report.get("sessions") or []
+ lines += ["", "## Sessions used", "", "| step | name | dir | session.json sha256 |", "|---|---|---|---|"]
+ if not sessions:
+ lines.append("| - | - | - | (none recorded) |")
+ for row in sessions:
+ sha = (row.get("hashes") or {}).get("session.json") or "-"
+ lines.append(f"| {row['step']} | {row['name']} | `{row['dir']}` | `{sha}` |")
+ checks = report["threshold_checks"]
+ lines += [
+ "",
+ "## Value (frozen thresholds, docs/40 section 5)",
+ "",
+ "| metric | value |",
+ "|---|---|",
+ f"| triage wall_s | {checks['triage_wall_s']} |",
+ f"| wall_s max | {checks['triage_wall_s_max']} |",
+ f"| within max | {checks['wall_within_max']} |",
+ f"| manual baseline_s | {checks['manual_baseline_s']} |",
+ f"| speedup_x | {checks['speedup_x']} |",
+ f"| beats manual baseline | {checks['beats_manual_baseline']} |",
+ "",
+ "## Artifacts",
+ "",
+ ]
+ for key in ("report_json", "report_md"):
+ lines.append(f"* {key}: `{report['artifacts'][key]}`")
+ lines.append("")
+ return "\n".join(lines)
+
+
+def run_triage(options: TriageOptions, runners=None) -> dict:
+ """Run every step (wrapped), write ``triage-report.json|.md``, return it."""
+ check_rm610_gate(options.labels(), approval_token=options.approval_token)
+ if not options.out:
+ raise ValueError("--out is required unless --dry-run is used")
+ if options.budget <= 0 or options.budget > MAX_INSTRUCTIONS:
+ raise ValueError(f"--budget must be in 1..{MAX_INSTRUCTIONS}, got {options.budget}")
+ out = os.path.abspath(options.out)
+ ensure_dir(out)
+ started = time.monotonic()
+ steps: list[dict] = []
+ for name, fn in runners or DEFAULT_RUNNERS:
+ step_started = time.monotonic()
+ try:
+ detail = fn(options, out)
+ if not isinstance(detail, dict):
+ detail = {"status": "ok", "result": detail}
+ except Exception as exc:
+ detail = {"status": "error", "reason": f"{type(exc).__name__}: {exc}"}
+ entry = {"name": name, "wall_s": round(time.monotonic() - step_started, 3)}
+ entry.update(detail)
+ steps.append(entry)
+ wall_s = round(time.monotonic() - started, 3)
+ report: dict = {
+ "format": FORMAT,
+ "tool": "harness/triage.py",
+ "dry_run": False,
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+ "rm610_gate": {"status": "passed", "labels": sorted(set(options.labels()))},
+ "input": {
+ "firmware": _target_brief(options.firmware, options.version),
+ "baseline": _target_brief(options.baseline, options.baseline_label) if options.baseline else None,
+ "baseline_supplied": options.baseline is not None,
+ "budget": options.budget,
+ "qemu_duration": options.qemu_duration,
+ "triage_command": (
+ "python3 harness/triage.py --firmware <firmware>"
+ + (" --baseline <baseline>" if options.baseline else "")
+ + (f" --version {options.version}" if options.version else "")
+ + f" --budget {options.budget} --out <out>"
+ ),
+ },
+ "thresholds": dict(FROZEN_THRESHOLDS),
+ "steps": steps,
+ "support_readiness": {},
+ "wall_s": wall_s,
+ "artifacts": {},
+ }
+ firmware = report["input"]["firmware"]
+ if firmware.get("error"):
+ report["input"]["firmware"]["status"] = "error"
+ report["input"]["elf"] = next((s.get("elf") for s in steps if s["name"] == "identity"), None)
+ report["errors"] = [step["name"] for step in steps if step["status"] == "error"]
+ report["sessions"] = _collect_sessions(steps, out)
+ report["support_readiness"] = support_readiness(report)
+ checks = {
+ "triage_wall_s": wall_s,
+ "triage_wall_s_max": FROZEN_THRESHOLDS["triage_wall_s_max"],
+ "wall_within_max": wall_s <= FROZEN_THRESHOLDS["triage_wall_s_max"],
+ "manual_baseline_s": FROZEN_THRESHOLDS["manual_baseline_s"],
+ "speedup_x": round(FROZEN_THRESHOLDS["manual_baseline_s"] / wall_s, 1) if wall_s > 0 else None,
+ "beats_manual_baseline": wall_s < FROZEN_THRESHOLDS["manual_baseline_s"],
+ "required_steps_missing": [
+ name
+ for name in FROZEN_THRESHOLDS["required_steps"]
+ if steps_by_name(steps, name).get("status") not in ("ok", "skipped")
+ ],
+ }
+ report["threshold_checks"] = checks
+ artifacts = {
+ "report_json": os.path.join(out, "triage-report.json"),
+ "report_md": os.path.join(out, "triage-report.md"),
+ }
+ report["artifacts"] = artifacts
+ write_json(artifacts["report_json"], report)
+ write_text(artifacts["report_md"], render_markdown(report))
+ return report
+
+
+def steps_by_name(steps: list[dict], name: str) -> dict:
+ return next((step for step in steps if step["name"] == name), {})
+
+
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
+
+def build_plan(options: TriageOptions) -> dict:
+ out = os.path.abspath(options.out) if options.out else "<out>"
+ return {
+ "format": PLAN_FORMAT,
+ "tool": "harness/triage.py",
+ "dry_run": True,
+ "status": "planned",
+ "rm610_gate": {"status": "passed", "labels": sorted(set(options.labels()))},
+ "firmware": {"path": options.firmware, "version": options.version or "auto"},
+ "baseline": options.baseline,
+ "out": out,
+ "budget": options.budget,
+ "steps": [
+ {"name": name, "budget": options.budget if name in ("native_boot", "ab_eval") else None}
+ for name, _fn in DEFAULT_RUNNERS
+ ],
+ "side_effects": {
+ "writes": [
+ os.path.join(out, "triage-report.json"),
+ os.path.join(out, "triage-report.md"),
+ os.path.join(out, "sessions/"),
+ os.path.join(out, "ab-eval/"),
+ os.path.join(out, "version-diff/ (only with --baseline)"),
+ ],
+ "hardware": "none",
+ "network": "none",
+ "max_instructions_per_attempt": MAX_INSTRUCTIONS,
+ },
+ }
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(prog="risc-v-gsp-rm-debugger triage", description=__doc__.splitlines()[0])
+ parser.add_argument("--firmware", required=True, help="new GSP firmware image (read-only)")
+ parser.add_argument("--baseline", help="previous firmware image for the version diff (optional)")
+ parser.add_argument("--version", help="version label (default: gsp_version.txt / dir / stem)")
+ parser.add_argument("--baseline-label", help="version label recorded for the baseline")
+ parser.add_argument("--out", help="report directory (required unless --dry-run)")
+ parser.add_argument("--budget", type=int, default=DEFAULT_BUDGET,
+ help=f"instruction budget per native attempt (1..{MAX_INSTRUCTIONS})")
+ parser.add_argument("--qemu-duration", type=float, default=DEFAULT_QEMU_DURATION_S,
+ help="QEMU probe wall-clock bound in seconds")
+ parser.add_argument("--step-timeout", type=float, default=DEFAULT_STEP_TIMEOUT_S,
+ help="per-step wall-clock net in seconds")
+ parser.add_argument("--release", help="release label; an rm610 release is gated by docs/23")
+ parser.add_argument("--approval-token", help=f"explicit RM610 token ({RM610_TOKEN})")
+ parser.add_argument("--dry-run", action="store_true",
+ help="print the plan and exit without opening targets or creating --out")
+ return parser
+
+
+def options_from_args(args: argparse.Namespace) -> TriageOptions:
+ return TriageOptions(
+ firmware=args.firmware,
+ baseline=args.baseline,
+ version=args.version,
+ baseline_label=args.baseline_label,
+ out=args.out,
+ budget=args.budget,
+ qemu_duration=args.qemu_duration,
+ step_timeout=args.step_timeout,
+ release=args.release,
+ approval_token=args.approval_token,
+ dry_run=args.dry_run,
+ )
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = build_parser()
+ args = parser.parse_args(argv)
+ options = options_from_args(args)
+ try:
+ check_rm610_gate(options.labels(), approval_token=options.approval_token)
+ except Rm610GateError as exc:
+ print(f"[triage] RM610 gate refused: {exc}", file=sys.stderr)
+ return 3
+ if options.dry_run:
+ plan = build_plan(options)
+ print(json.dumps(plan, indent=2, sort_keys=True))
+ print(f"[triage] dry-run: {len(plan['steps'])} steps planned; no targets opened, no --out created")
+ return 0
+ if not options.out:
+ parser.error("--out is required unless --dry-run is used")
+ if options.budget <= 0 or options.budget > MAX_INSTRUCTIONS:
+ parser.error(f"--budget must be in 1..{MAX_INSTRUCTIONS}")
+ if not os.path.isfile(options.firmware):
+ print(f"[triage] skipped: firmware absent: {options.firmware}", file=sys.stderr)
+ return 2
+ try:
+ report = run_triage(options)
+ except Rm610GateError as exc:
+ print(f"[triage] RM610 gate refused: {exc}", file=sys.stderr)
+ return 3
+ except ValueError as exc:
+ parser.error(str(exc))
+ except Exception as exc:
+ print(f"[triage] error: {type(exc).__name__}: {exc}", file=sys.stderr)
+ return 2
+ for step in report["steps"]:
+ reason = f" reason={step['reason']}" if step.get("reason") else ""
+ print(f"[triage] {step['name']}: status={step['status']}{reason}")
+ readiness = report["support_readiness"]
+ print(f"[triage] readiness: {readiness['verdict']} (baseline_compared={readiness['baseline_compared']})")
+ for action in readiness["next_actions"][:6]:
+ print(f"[triage] next: {action}")
+ checks = report["threshold_checks"]
+ print(f"[triage] wall: {checks['triage_wall_s']}s vs manual baseline {checks['manual_baseline_s']}s "
+ f"(speedup {checks['speedup_x']}x)")
+ print(f"[triage] wrote {report['artifacts']['report_json']} + {report['artifacts']['report_md']}")
+ return 1 if report["errors"] else 0
+
+
+if __name__ == "__main__": # pragma: no cover
+ raise SystemExit(main())