summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/greeneye/plugins.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 08:36:35 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 08:36:35 -0400
commitba58dcdc5ad6ca021c723d279f33e3f14e1184df (patch)
tree47e43a2dad9a2d4bb40da46243654bff8b1302de /debug/risc-v-gsp-rm-vm/greeneye/plugins.py
parentfe485080e159c983242d699f4a584659df159818 (diff)
downloadopen-iov-ba58dcdc5ad6ca021c723d279f33e3f14e1184df.tar.gz
open-iov-ba58dcdc5ad6ca021c723d279f33e3f14e1184df.zip
debug: add green-eye GSP/uproc RISC-V VM runtime analysis tooling
Adds open-iov/debug/risc-v-gsp-rm-vm: a GDB-free instrumentation stack that boots uproc/GSP firmware in qemu-system-riscv64 and provides Frida-like hooks, breakpoints, watchpoints, memory dumps, structure decoding, snapshot diffs, recorded sessions, declarative probes, a plugin API and a Ghidra symbol/hit bridge. Verified: 25 tests green (including a live QEMU smoke test); the demo uproc boots with correct hook/leave/watch/schema/snapshot/fault evidence; the optional TCG trace plugin builds and recorded 15M trace lines in 3 s; the TU104 570.144 fwimage probe reports its first exception (load access fault at firmware vaddr 0x4009014 reading 0x2000000000000A00). Documentation: lean tool docs under debug/risc-v-gsp-rm-vm/docs/00-10, a new docs/05-operations/03-risc-v-gsp-rm-vm.md debugging document, and '# TOOL IN DEVELOPMENT - NOT READY YET!' markers in docs/README.md, the onboarding tooling table and the debugging workflows doc. Vendors qemu-plugin.h from QEMU v8.2.2 (GPL-2.0-or-later), with its license header intact, so the optional TCG plugin builds without a QEMU source tree.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/plugins.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/plugins.py121
1 files changed, 121 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/plugins.py b/debug/risc-v-gsp-rm-vm/greeneye/plugins.py
new file mode 100644
index 0000000..af4545f
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/greeneye/plugins.py
@@ -0,0 +1,121 @@
+"""green-eye plugin system.
+
+A plugin is a normal Python module that exposes::
+
+ def register(ge):
+ ge.log("my plugin loaded")
+ ge.add_command("mycmd", run_mycmd, help="…")
+ ge.hook("rpc_sum", on_enter=lambda hit: ge.log(f"sum({hit.arg(0)})"))
+
+Plugins can extend green-eye with:
+
+* new CLI commands (including an MCP server later - roadmap);
+* runtime RE tools (extra watchpoint strategies, structure probes, coverage);
+* virtualisation-environment updates (e.g. record/replay harnesses);
+* AI-harness integration and reporting.
+
+Discovery order:
+
+1. explicit ``--plugin PATH`` arguments;
+2. colon-separated paths in ``GREENEYE_PLUGINS``;
+3. ``<tool>/plugins/*.py`` (the bundled examples).
+"""
+from __future__ import annotations
+
+import importlib.util
+import os
+import sys
+from typing import Callable
+
+
+class GreenEyeAPI:
+ """Handle passed to ``register(ge)``."""
+
+ def __init__(self, session, cli=None):
+ self.session = session
+ self.cli = cli
+ self.commands: dict[str, tuple[Callable, str]] = {}
+ self.loaded: list[str] = []
+
+ # -- convenience --------------------------------------------------------
+
+ @property
+ def memory(self):
+ return self.session.memory
+
+ @property
+ def symbols(self):
+ return self.session.symbols
+
+ @property
+ def recorder(self):
+ return self.session.recorder
+
+ def log(self, message: str, **fields) -> None:
+ self.session.recorder.event("plugin", message=message, **fields)
+
+ def hook(self, target, on_enter=None, on_leave=None, **kw):
+ return self.session.hook(target, on_enter=on_enter, on_leave=on_leave, **kw)
+
+ def watch(self, target, size: int = 8, kind: str = "w", callback=None, **kw):
+ return self.session.watch(target, size=size, kind=kind, callback=callback, **kw)
+
+ def on_event(self, kind: str, callback) -> None:
+ self.session.on_event(kind, callback)
+
+ def resolve(self, target):
+ return self.session.resolve(target)
+
+ def add_command(self, name: str, fn: Callable, help: str = "") -> None:
+ self.commands[name] = (fn, help)
+
+ def run_command(self, name: str, args: list[str]) -> int:
+ fn, _help = self.commands[name]
+ return int(fn(self.session, args) or 0)
+
+
+class PluginManager:
+ def __init__(self, session, cli=None):
+ self.session = session
+ self.api = GreenEyeAPI(session, cli=cli)
+ self.loaded: list[str] = []
+
+ def default_paths(self) -> list[str]:
+ here = os.path.dirname(os.path.abspath(__file__))
+ bundled = os.path.join(os.path.dirname(here), "plugins")
+ paths = []
+ env = os.environ.get("GREENEYE_PLUGINS", "")
+ paths += [p for p in env.split(os.pathsep) if p]
+ if os.path.isdir(bundled):
+ paths.append(bundled)
+ return paths
+
+ def load_all(self, explicit: list[str] | None = None) -> list[str]:
+ for path in (explicit or []) + self.default_paths():
+ if os.path.isdir(path):
+ for entry in sorted(os.listdir(path)):
+ if entry.endswith(".py") and not entry.startswith("_"):
+ self._load_file(os.path.join(path, entry))
+ elif path.endswith(".py") and os.path.isfile(path):
+ self._load_file(path)
+ return self.loaded
+
+ def _load_file(self, path: str) -> None:
+ name = os.path.splitext(os.path.basename(path))[0]
+ mod_name = f"greeneye_plugin_{name}"
+ try:
+ spec = importlib.util.spec_from_file_location(mod_name, path)
+ if spec is None or spec.loader is None:
+ return
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[mod_name] = module
+ spec.loader.exec_module(module)
+ register = getattr(module, "register", None)
+ if register is None:
+ self.session.recorder.event("plugin_error", plugin=name, error="no register(ge)")
+ return
+ register(self.api)
+ self.loaded.append(name)
+ self.session.recorder.event("plugin_loaded", plugin=name)
+ except Exception as exc: # plugins must never break the session
+ self.session.recorder.event("plugin_error", plugin=name, error=repr(exc))