summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 09:13:00 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 09:13:00 -0400
commit1e701396b1b6d204dfd8e5a8ddcd3007038029ac (patch)
tree8dde895cf9d3e66ce912d01cb1a72ff674dc850f /debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py
parentab48e44832db55fdedadcf44f55fa9904ca36f2f (diff)
downloadopen-iov-1e701396b1b6d204dfd8e5a8ddcd3007038029ac.tar.gz
open-iov-1e701396b1b6d204dfd8e5a8ddcd3007038029ac.zip
debug: green-eye wave 2 - injection SDK, Frida-like calls, Ghidra control bridge, long-term docs
Adds to debug/risc-v-gsp-rm-vm/: - Session.call / run_until: Frida-like remote function calls and run-to-cursor, with register restoration; verified live (rpc_ping -> 0xaffe). - symbol+offset target resolution (gsp_ctrl+0x10) across all APIs and the REPL. - Injection SDK (sdk/) and 'greeneye inject': position-independent RV64 service blobs (verified zero relocations), loaded into RAM, registered in the demo handler table, and invoked either directly or through the firmware mailbox dispatcher. Demo ABI extended: mailbox 16..23 selects slots, arg0 carries the service command, arg2 added (struct now 0x190; schema updated). - Ghidra control bridge (greeneye ghidra + GreenEyeControl.java): run-until-here, breakpoints, memory/register access, remote calls, hit export; verified end-to-end with Ghidra headless (run_until reached rpc_sum from a GhidraScript). - MarkGreenEyeHits.java fixed for the Ghidra 12 createBookmark API; verified headless together with ExportForGreenEye.java. - Analyzer renders a QEMU first-fault table for probe sessions (fault count, epc with firmware vaddr translation, access address/size, reason). - Tests now 35 green, including injection, Ghidra bridge, remote call, run-until and REPL command smoke tests. - Documentation: docs/11-16 (GSP RM service developer guide, injection SDK specification, Ghidra control, adversarial interoperability guide, agentic GSP IDE, custom simulator design) and GREEN-EYE-ROADMAP.md (long-term H1-H7 program); doc maps and the project RISC-V VM doc updated.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py220
1 files changed, 220 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py b/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py
new file mode 100644
index 0000000..cc06e43
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py
@@ -0,0 +1,220 @@
+"""Ghidra control bridge: let GhidraScripts drive a green-eye session.
+
+A tiny JSON-lines server over localhost. Each request is one line::
+
+ {"id": 1, "cmd": "run_until", "args": {"target": "0x800000ce", "timeout": 5}}
+
+and each response is one line::
+
+ {"id": 1, "ok": true, "result": {"addr": 2147483854}}
+
+This is the transport behind ``ghidra/GreenEyeControl.java``: a Ghidra user
+can "execute until here", set/clear breakpoints, read memory and export hit
+lists without leaving the decompiler. See ``docs/13-GHIDRA-CONTROL.md``.
+"""
+from __future__ import annotations
+
+import json
+import socket
+import threading
+
+from .errors import GreenEyeError
+
+
+class GhidraBridge:
+ def __init__(self, session, host: str = "127.0.0.1", port: int = 0):
+ self.session = session
+ self.host = host
+ self.port = port
+ self._server: socket.socket | None = None
+ self._thread: threading.Thread | None = None
+ self._clients: list[threading.Thread] = []
+ self._stop = threading.Event()
+
+ # -- lifecycle ---------------------------------------------------------
+
+ def start(self) -> int:
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ server.bind((self.host, self.port))
+ server.listen(4)
+ self.port = server.getsockname()[1]
+ self._server = server
+ self._thread = threading.Thread(target=self._accept_loop, name="greeneye-ghidra", daemon=True)
+ self._thread.start()
+ return self.port
+
+ def stop(self) -> None:
+ self._stop.set()
+ if self._server is not None:
+ try:
+ self._server.close()
+ except OSError:
+ pass
+ self._server = None
+
+ def serve_forever(self) -> None:
+ if self._thread is None:
+ self.start()
+ try:
+ while not self._stop.is_set() and (self._thread is not None and self._thread.is_alive()):
+ self._thread.join(timeout=0.5)
+ except KeyboardInterrupt:
+ pass
+ finally:
+ self.stop()
+
+ def _accept_loop(self) -> None:
+ assert self._server is not None
+ while not self._stop.is_set():
+ try:
+ client, _addr = self._server.accept()
+ except OSError:
+ return
+ handler = threading.Thread(target=self._client_loop, args=(client,), daemon=True)
+ handler.start()
+ self._clients.append(handler)
+
+ def _client_loop(self, client: socket.socket) -> None:
+ with client:
+ stream = client.makefile("rwb")
+ for line in stream:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ request = json.loads(line)
+ except json.JSONDecodeError as exc:
+ response = {"id": None, "ok": False, "error": f"bad json: {exc}"}
+ else:
+ response = self._dispatch(request)
+ stream.write((json.dumps(response) + "\n").encode("utf-8"))
+ stream.flush()
+ if response.get("close"):
+ self.stop()
+ return
+
+ # -- command dispatch --------------------------------------------------
+
+ def _dispatch(self, request: dict) -> dict:
+ request_id = request.get("id")
+ cmd = request.get("cmd", "")
+ args = request.get("args") or {}
+ try:
+ handler = getattr(self, f"cmd_{cmd}", None)
+ if handler is None:
+ raise GreenEyeError(f"unknown bridge command {cmd!r}")
+ result = handler(**args)
+ response = {"id": request_id, "ok": True, "result": result}
+ if cmd == "close":
+ response["close"] = True
+ return response
+ except Exception as exc: # never kill the bridge on a bad request
+ return {"id": request_id, "ok": False, "error": str(exc)}
+
+ # -- commands ----------------------------------------------------------
+
+ def cmd_ping(self) -> str:
+ return "pong"
+
+ def cmd_info(self) -> dict:
+ return {
+ "name": self.session.name,
+ "pc": self.session.pc,
+ "symbols": len(self.session.symbols),
+ "recording": getattr(self.session.recorder, "dir", None),
+ }
+
+ def cmd_resolve(self, name: str) -> int:
+ return self.session.resolve(name)
+
+ def cmd_bp_set(self, target, kind: str = "sw") -> int:
+ handle = self.session.hook(target, kind=kind, listener="ghidra")
+ return handle.addr
+
+ def cmd_bp_clear(self, addr) -> bool:
+ addr = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ for _key, bp in list(self.session._bps.items()): # noqa: SLF001 - bridge is a privileged client
+ if bp.addr == addr:
+ self.session.remove_breakpoint(bp)
+ return True
+
+ def cmd_bp_list(self) -> list[dict]:
+ return [
+ {"addr": bp.addr, "label": bp.label, "hits": bp.hits, "kind": bp.kind}
+ for bp in self.session._bps.values() # noqa: SLF001
+ ]
+
+ def cmd_watch_set(self, target, size: int = 8, kind: str = "w") -> int:
+ wp = self.session.watch(target, size=size, kind=kind, listener="ghidra")
+ return wp.addr
+
+ def cmd_run_until(self, target, timeout: float = 5.0) -> dict:
+ hit = self.session.run_until(target, timeout=timeout)
+ return {"addr": hit.addr, "symbol": hit.symbol.name if hit.symbol else None, "kind": hit.kind}
+
+ def cmd_run(self, duration: float = 1.0, max_stops: int | None = None) -> int:
+ return self.session.run(duration=duration, max_stops=max_stops)
+
+ def cmd_step(self, count: int = 1) -> list[int]:
+ return [hit.addr for hit in self.session.step(count)]
+
+ def cmd_read_mem(self, addr, size: int = 64) -> str:
+ real = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ return self.session.memory.read(real, size).hex()
+
+ def cmd_write_mem(self, addr, hex: str) -> bool:
+ real = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ self.session.memory.write(real, bytes.fromhex(hex))
+ return True
+
+ def cmd_read_reg(self, name: str) -> int:
+ return self.session.client.read_reg(name)
+
+ def cmd_write_reg(self, name: str, value) -> bool:
+ self.session.client.write_reg(name, int(value))
+ return True
+
+ def cmd_call(self, target, args: list | None = None, timeout: float = 5.0) -> int:
+ return self.session.call(target, tuple(args or ()), timeout=timeout)
+
+ def cmd_snapshot(self, addr, size: int, label: str = "ghidra") -> str:
+ real = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ self.session.memory.snapshot(real, size, name=label)
+ return label
+
+ def cmd_note(self, text: str) -> bool:
+ self.session.note(text)
+ return True
+
+ def cmd_hits(self) -> list[dict]:
+ from .record import load_events
+
+ directory = getattr(self.session.recorder, "dir", None)
+ if not directory:
+ return []
+ counts: dict[int, int] = {}
+ for event in load_events(directory):
+ if event.get("type") == "bp_hit":
+ addr = event.get("pc")
+ if isinstance(addr, int):
+ counts[addr] = counts.get(addr, 0) + 1
+ return [
+ {
+ "addr": addr,
+ "hits": hits,
+ "symbol": (self.session.symbols.resolve(addr).name if self.session.symbols.resolve(addr) else None),
+ }
+ for addr, hits in sorted(counts.items(), key=lambda item: -item[1])
+ ]
+
+ def cmd_export_hits(self, path: str) -> str:
+ from .ghidra import export_hits
+
+ directory = getattr(self.session.recorder, "dir", None)
+ if not directory:
+ raise GreenEyeError("session is not recording; hits cannot be exported")
+ return export_hits(directory, path, symbols=self.session.symbols)
+
+ def cmd_close(self) -> bool:
+ return True