diff options
| author | user <user@clank> | 2026-08-07 01:31:57 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-07 01:31:57 +0200 |
| commit | 93240e2f0c0d716758c43154dd37802f556ac6d0 (patch) | |
| tree | 564ec41d72e9f4261b582a2b9c5ceb8995162fac /.auto/diff_spans.py | |
| parent | autoresearch: checks gate names the failing scenario and retries it alone (diff) | |
| download | ida-tui-93240e2f0c0d716758c43154dd37802f556ac6d0.tar.gz ida-tui-93240e2f0c0d716758c43154dd37802f556ac6d0.tar.xz ida-tui-93240e2f0c0d716758c43154dd37802f556ac6d0.zip | |
Stop ida-pro-mcp installing a sys.setprofile hook around every tool call. Its deadline mechanism profiles every python call/return so a pure-python tool loop can be interrupted; our tools are call-heavy, so it taxed the whole backend 3.3x. Worker now sets IDA_MCP_TOOL_TIMEOUT_SEC=0 and arms the deadline itself with one polling watchdog thread + ida_kernwin.set_cancelled() (the half that actually frees the IDA main thread). Also rewrote _idatui_spans to jump between colour tags instead of walking characters (byte-identical over 258k real lines).
Result: {"status":"keep","total_ms":26923.9,"lg_boot_ms":762.2,"lg_decomp_ms":2631.7,"lg_graph_ms":941.8,"lg_hex_ms":1052,"lg_index_ms":67.2,"lg_listing_cold_ms":440.6,"lg_listing_warm_ms":530.5,"lg_nav_ms":10598.3,"lg_palette_ms":4.6,"lg_render_ms":227.8,"lg_search_ms":5265.5,"pure_graph_ms":237.9,"sm_boot_ms":535.3,"sm_decomp_ms":631.8,"sm_graph_ms":702.1,"sm_hex_ms":841.1,"sm_index_ms":0,"sm_listing_cold_ms":268.2,"sm_listing_warm_ms":269.4,"sm_nav_ms":443.6,"sm_palette_ms":0.3,"sm_render_ms":277.8,"sm_search_ms":194.4,"fails":0}
Diffstat (limited to '.auto/diff_spans.py')
| -rw-r--r-- | .auto/diff_spans.py | 106 |
1 files changed, 106 insertions, 0 deletions
diff --git a/.auto/diff_spans.py b/.auto/diff_spans.py new file mode 100644 index 0000000..95721a5 --- /dev/null +++ b/.auto/diff_spans.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Differential check: the current `_idatui_spans` vs the one at a git ref. + +The span walker turns IDA's colour-tagged disassembly line into (spans, ops). +It is on the hot path of every listing row, so it is worth optimising -- but its +output drives highlighting, operand marking and the cursor's column arithmetic, +so "faster" is only acceptable if it is byte-identical. + +This pulls both implementations out of `server/patch_server.py` (the current +working tree, and whatever `--ref` names), runs them over every tagged line of +a real binary, and reports the first disagreement. + + /usr/bin/python3 .auto/diff_spans.py [--ref HEAD] [--target targets/bash] + [--limit 60000] +""" +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def load_impl(path: str, name: str): + """Exec just the span-walker section of a patch_server.py's injected BODY. + + BODY is a normal (non-raw) triple-quoted string, so the escapes in it are + resolved once by importing the module -- slicing the file text instead would + compile the *undecoded* source and silently test a different program (an + escaped backslash became a literal one, and every 's' in a comment turned + into a space). + """ + import importlib.util + spec = importlib.util.spec_from_file_location(f"_ps_{name}", path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) # IDA-free at import time + body = mod.BODY + a = body.index("#: IDA colour tag -> the semantic kind") + b = body.index("def _idatui_unknown_row") + g = {"__name__": name} + exec(compile(body[a:b], name, "exec"), g) # noqa: S102 + return g["_idatui_spans"] + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--ref", default="HEAD") + ap.add_argument("--target", default="targets/bash") + ap.add_argument("--limit", type=int, default=60000) + a = ap.parse_args() + + d = tempfile.mkdtemp(prefix="diffspans-") + old_path = os.path.join(d, "patch_server_old.py") + with open(old_path, "w") as fh: + fh.write(subprocess.run( + ["git", "-C", ROOT, "show", f"{a.ref}:server/patch_server.py"], + capture_output=True, text=True, check=True).stdout) + new = load_impl(os.path.join(ROOT, "server", "patch_server.py"), "new") + old = load_impl(old_path, "old") + + binary = os.path.join(ROOT, a.target) + tgt = os.path.join(d, os.path.basename(binary)) + shutil.copy2(binary, tgt) + shutil.copy2(binary + ".i64", tgt + ".i64") + try: + import idapro + if idapro.open_database(tgt, run_auto_analysis=False) != 0: + raise SystemExit("could not open database") + import ida_bytes + import ida_lines + import ida_segment + import idaapi + + checked = bad = 0 + for si in range(ida_segment.get_segm_qty()): + seg = ida_segment.getnseg(si) + if seg is None: + continue + ea = seg.start_ea + while ea < seg.end_ea and checked < a.limit: + line = ida_lines.generate_disasm_line(ea, 0) + if line: + checked += 1 + ra, rb = old(line), new(line) + if ra != rb: + bad += 1 + if bad <= 3: + print(f"MISMATCH @ {ea:#x}\n line={line!r}\n" + f" old={ra!r}\n new={rb!r}") + nxt = ida_bytes.get_item_end(ea) + ea = nxt if nxt > ea else ea + 1 + if checked >= a.limit: + break + print(f"checked {checked} lines, {bad} mismatches") + idapro.close_database(False) + return 1 if bad else 0 + finally: + shutil.rmtree(d, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) |
