From a8d39781a7b03504556b5df6ff73ab2fd145efaf Mon Sep 17 00:00:00 2001 From: blasty Date: Mon, 27 Jul 2026 22:03:08 +0200 Subject: rpc: a `trace` verb, and --trace for pane spawn Driving the trace viewer needed the same treatment as everything else: seeking by hand through a few hundred keypresses to reach an interesting timestamp is not a way to test it. trace {seek: 120} absolute timestamp trace {seek: "!50"} halfway through, like Tenet's timestamp shell trace {goto: "main"} first execution of a name or 0xADDR trace {step: 20} relative, negative goes back trace {step: 5, over: true} Returns the usual state snapshot plus {idx, length, pc, changed} so a driver can see where it landed and what that instruction wrote. pane spawn --trace FILE passes it through to the launcher, so a trace pane is one command. --- idatui/pane.py | 4 ++++ idatui/rpc.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/idatui/pane.py b/idatui/pane.py index 53afef3..1a46a4f 100644 --- a/idatui/pane.py +++ b/idatui/pane.py @@ -165,6 +165,8 @@ def spawn(args) -> int: inner += ["--rpc", sock] else: inner = [args.python, "-m", "idatui.launch", target, "--rpc", sock] + if getattr(args, "trace", None): + inner += ["--trace", os.path.abspath(os.path.expanduser(args.trace))] cmd = f"cd {REPO!r} && exec " + " ".join(_q(a) for a in inner) split = ["split-window", "-v" if args.vertical else "-h", @@ -316,6 +318,8 @@ def main(argv: list[str]) -> int: sp = sub.add_parser("spawn", help="open a TUI pane and wait until ready") sp.add_argument("--open", metavar="PATH", help="binary to open (its dir must be writable)") + sp.add_argument("--trace", metavar="FILE", + help="Tenet execution trace to load alongside the binary") sp.add_argument("--project", metavar="FILE", help="project file to open instead of a single binary; " "any --open paths are added to it (created if absent)") diff --git a/idatui/rpc.py b/idatui/rpc.py index 0921b89..7741f48 100644 --- a/idatui/rpc.py +++ b/idatui/rpc.py @@ -72,6 +72,7 @@ METHODS = { "search": "{term,direction?=1} incremental search in the code view", "select": "{index?} choose the highlighted/nth item in the open modal", "save": "persist the .i64 (Ctrl+S)", + "trace": "{seek|goto|step,over?} navigate the execution trace (seek '!50' = percent)", "binaries": "-> project binaries {label,active,resident,indexed} (project mode)", "switch": "{binary,addr?} make another project binary active (addr also jumps)", "close": "dismiss a modal (Escape)", @@ -531,6 +532,38 @@ class RpcServer: return functions(app, params.get("filter"), int(params.get("limit", 50))) # -- projects ------------------------------------------------------ # + if method == "trace": + if app._trace is None: + raise ValueError("no trace loaded (launch with --trace FILE)") + t = app._trace + if "seek" in params: + v = params["seek"] + # "!50" seeks a percentage, like Tenet's timestamp shell. + if isinstance(v, str) and v.startswith("!"): + idx = int(float(v[1:]) * (t.length - 1) / 100.0) + else: + idx = int(str(v).replace(",", ""), 0) if isinstance(v, str) else int(v) + app._seek(idx) + elif "goto" in params: # first execution of an address/name + tgt = params["goto"] + ea = (int(str(tgt), 0) if str(tgt).lower().startswith("0x") + else app.program.resolve(str(tgt))) + first = t.first_execution(ea) + if first is None: + raise ValueError(f"{tgt} never executed in this trace") + app._seek(first) + elif "step" in params: + n = int(params.get("step") or 1) + over = bool(params.get("over")) + for _ in range(abs(n)): + (app._step_over if over else app._step)(1 if n > 0 else -1) + await settle(app, timeout=float(params.get("timeout", 20.0))) + snap = snapshot(app) + snap["trace"] = {"idx": app._t, "length": t.length, + "pc": hex(t.ip(app._t)), + "changed": sorted(t.changed(app._t))} + return snap + if method == "binaries": if app._project is None: raise ValueError("not a project session (launch with --project)") -- cgit v1.3.1-sl0p