aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_rawimage_rpc.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/tests/test_rawimage_rpc.py b/tests/test_rawimage_rpc.py
new file mode 100644
index 0000000..ab7eb96
--- /dev/null
+++ b/tests/test_rawimage_rpc.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+"""The raw-image workflow over the RPC socket: spawn with load options, define,
+bulk-apply a symbol file.
+
+A headerless firmware image is the case where driving IDA from an agent used to
+fall apart:
+
+ * ``pane spawn`` could not pass ``--processor``/``--base``, so the pane came up
+ ready-but-empty (x86 at 0, zero functions) and the only way through was to
+ hand-write a project file;
+ * ``c``/``p``/``t`` (make code / make function / ARM-Thumb) existed as key
+ bindings but had no verb, so a driver had to guess raw keys and hope no
+ modal was on top;
+ * every name had to go through the typed rename prompt — a navigation plus two
+ prompt round-trips each, which is tens of minutes for a 400-symbol map.
+
+This test spawns a real pane on a real Thumb blob and checks all three.
+
+Requires: tmux, IDA (idalib). ~2min.
+
+ ~/ida-venv/bin/python tests/test_rawimage_rpc.py
+"""
+import json
+import os
+import subprocess
+import sys
+import tempfile
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from idatui.rpcclient import RpcClient, RpcError # noqa: E402
+
+REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+BLOB = os.path.join(REPO, "experiments", "fibonacci.bin") # real Thumb code
+
+PASS = FAIL = 0
+
+
+def check(name, ok, detail=""):
+ global PASS, FAIL
+ if ok:
+ PASS += 1
+ print(f" ok [{name}]")
+ else:
+ FAIL += 1
+ print(f" FAIL [{name}] {detail}")
+
+
+def spawn_pane(target, processor, timeout=420):
+ cmd = [sys.executable, "-m", "idatui.pane", "spawn", "--open", target,
+ "--processor", processor, "--detached", "--size", "60%",
+ "--timeout", str(timeout)]
+ r = subprocess.run(cmd, capture_output=True, text=True,
+ timeout=timeout + 60, cwd=REPO)
+ if not r.stdout.strip():
+ print(f" spawn produced no JSON: {r.stderr.strip()}", file=sys.stderr)
+ return None
+ return json.loads(r.stdout)
+
+
+def stop_pane(sock, timeout=60):
+ subprocess.run([sys.executable, "-m", "idatui.pane", "stop", "--sock", sock,
+ "--timeout", str(timeout)],
+ capture_output=True, text=True, timeout=timeout + 10, cwd=REPO)
+
+
+def main() -> int:
+ if not os.environ.get("TMUX"):
+ print("SKIP: not inside tmux")
+ return 0
+ if not os.path.exists(BLOB):
+ print(f"SKIP: no blob at {BLOB}")
+ return 0
+
+ # Work on a copy: the .i64 lands next to the binary and the load options
+ # only apply to a FIRST open, so a leftover database would silently decide
+ # what this test measures.
+ with tempfile.TemporaryDirectory() as tmp:
+ blob = os.path.join(tmp, "fib.bin")
+ with open(BLOB, "rb") as src, open(blob, "wb") as dst:
+ dst.write(src.read())
+
+ info = spawn_pane(blob, "arm:ARMv7-A")
+ if not info:
+ print("SKIP: could not spawn a pane")
+ return 0
+ sock = info["sock"]
+ try:
+ # -- load options actually reached IDA --------------------------- #
+ # Wrong processor => the disassembly is nonsense or absent; ARMv7-A
+ # also means a 32-bit database, without which Hex-Rays refuses.
+ check("spawn forwarded --processor", info.get("ok"),
+ json.dumps(info))
+ with RpcClient(sock) as c:
+ st = c.call("state")
+ check("pane is drivable", st.get("active") in
+ ("listing", "decomp", "hex"), json.dumps(st)[:200])
+
+ # -- define ------------------------------------------------- #
+ # fibonacci.bin is Thumb at 0x0; as ARM it does not decode.
+ r = c.call("define", kind="thumb", target="0x0")
+ d = r.get("define", {})
+ check("define thumb ran", "define" in r, json.dumps(r)[:200])
+ check("define thumb decoded instructions",
+ "instruction" in d.get("status", ""), d.get("status", ""))
+
+ r = c.call("define", kind="func", target="0x0")
+ check("define func created a function",
+ "function" in r["define"]["status"]
+ or "already" in r["define"]["status"],
+ r["define"]["status"])
+
+ bad = None
+ try:
+ c.call("define", kind="nonsense")
+ except RpcError as e:
+ bad = str(e)
+ check("define rejects an unknown kind", bad is not None
+ and "unknown define kind" in bad, str(bad))
+
+ # -- rename_many -------------------------------------------- #
+ fns = c.call("functions", limit=200)
+ ea = min(f["ea"] for f in fns) if fns else None
+ check("a function exists to rename", ea is not None)
+
+ symfile = os.path.join(tmp, "syms.json")
+ with open(symfile, "w") as f:
+ # 'start' (not 'addr') on purpose: symbol files in the wild
+ # use it, and accepting only one spelling is how a bulk
+ # import silently renames nothing.
+ json.dump([{"start": hex(ea), "name": "bulk_named_fn"},
+ {"start": "0xdeadbe", "name": "nowhere"}], f)
+ r = c.call("rename_many", file=symfile)
+ m = r.get("rename_many", {})
+ check("rename_many applied the good entry", m.get("ok") == 1,
+ json.dumps(m))
+ check("rename_many reports the bad entry",
+ m.get("failed") == 1 and m.get("errors"), json.dumps(m))
+
+ # The readback matters more than the return value: a driver
+ # trusts resolve/functions to decide what work is left.
+ check("renamed symbol resolves",
+ c.call("resolve", name="bulk_named_fn").get("ea") == ea,
+ json.dumps(c.call("resolve", name="bulk_named_fn")))
+ names = {f["name"] for f in c.call("functions", limit=200)}
+ check("function table shows the new name",
+ "bulk_named_fn" in names, str(sorted(names)[:10]))
+
+ r = c.call("rename_many", items=[{"addr": hex(ea),
+ "name": "inline_named_fn"}])
+ check("rename_many takes inline items",
+ r["rename_many"]["ok"] == 1, json.dumps(r["rename_many"]))
+
+ empty = None
+ try:
+ c.call("rename_many")
+ except RpcError as e:
+ empty = str(e)
+ check("rename_many without items errors", empty is not None
+ and "items" in empty, str(empty))
+ finally:
+ stop_pane(sock)
+
+ print(f"\n{PASS} passed, {FAIL} failed")
+ return 1 if FAIL else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())