aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-24 15:33:17 +0200
committerblasty <blasty@local>2026-07-24 15:33:17 +0200
commit731bca5c31f3507424a391fecf081be1a6c6717d (patch)
treeb089dab655490cefe3f59554c0a4845a94e49035
parentlisting: shift+home jumps to the start of the instruction text (diff)
downloadida-tui-731bca5c31f3507424a391fecf081be1a6c6717d.tar.gz
ida-tui-731bca5c31f3507424a391fecf081be1a6c6717d.tar.xz
ida-tui-731bca5c31f3507424a391fecf081be1a6c6717d.zip
xrefs: show fine-grained kind (call/jump/read/write/offset) in the dialog
ida-pro-mcp's xref_query only classifies xrefs as code/data (xr.iscode). Add an injected `xref_types` tool (server/patch_server.py) that mirrors xref_query's query/envelope shape but derives a fine `kind` from the IDA xref type: call/jump/flow for code (fl_CF/CN/JF/JN/F), read/write/offset/text/info for data (dr_R/W/O/T/I). The worker self-injects it on startup like the other custom tools. * domain: Xref gains a `kind` field; _parse_xrefs reads it; xrefs_to() now calls xref_types (falling back to xref_query if absent). xrefs_from is unchanged. * app: the `x` dialog shows the kind as an aligned column after the address (`000034F4 read sub_34F0+0x4`). * rpc: the structured xrefs_to read carries `kind` too. Verified live over the worker/RPC harness on targets/echo: sub_2C00 callers -> call; __progname -> offset (GOT), read (sub_34F0), write (sub_3500); stdout -> read/offset.
-rw-r--r--idatui/app.py3
-rw-r--r--idatui/domain.py17
-rw-r--r--idatui/rpc.py2
-rw-r--r--server/patch_server.py79
4 files changed, 93 insertions, 8 deletions
diff --git a/idatui/app.py b/idatui/app.py
index ce07cd0..07325a1 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -3164,7 +3164,8 @@ class IdaTui(App):
# not inside a function: name the section it lives in (a GOT/reloc
# data slot or a loose thunk) instead of a bare '?'.
loc = self.program.section_of(x.frm) or "<no seg>"
- items.append((x.frm, f"{x.frm:08X} {loc:<26} [{x.type}]"))
+ kind = x.kind or x.type or "?"
+ items.append((x.frm, f"{x.frm:08X} {kind:<6} {loc}"))
preselect = self._xref_preselect(xr, here_ea, here_end)
self.app.call_from_thread(self._present_xrefs, label, items, focus, preselect)
diff --git a/idatui/domain.py b/idatui/domain.py
index d7ebf3c..8a127d7 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -127,9 +127,10 @@ class Ref:
class Xref:
frm: int # the referencing address
to: int | None # the referenced address
- type: str # "code" | "data" | ...
+ type: str # coarse: "code" | "data"
fn_name: str | None # function containing `frm`
fn_addr: int | None
+ kind: str | None = None # fine: call/jump/flow/read/write/offset/text/info
@dataclass(frozen=True)
@@ -1260,11 +1261,14 @@ class Program:
return _parse_xrefs(payload)
def xrefs_to(self, ea: int, limit: int = 2000) -> list[Xref]:
- payload = self.client.call(
- "xref_query",
- queries=[{"addr": hex(ea), "direction": "to", "include_fn": True,
- "dedup": True, "count": limit}],
- )
+ q = [{"addr": hex(ea), "direction": "to", "include_fn": True,
+ "dedup": True, "count": limit}]
+ try:
+ # xref_types adds a fine-grained `kind` (call/read/write/...) for the
+ # xref dialog; fall back to xref_query (code/data only) if absent.
+ payload = self.client.call("xref_types", queries=q)
+ except IDAToolError:
+ payload = self.client.call("xref_query", queries=q)
return _parse_xrefs(payload)
# -- address resolution ------------------------------------------------ #
@@ -1365,6 +1369,7 @@ def _parse_xrefs(payload) -> list[Xref]:
type=d.get("type", "?"),
fn_name=fn.get("name"),
fn_addr=_as_int(fn["addr"]) if fn.get("addr") else None,
+ kind=d.get("kind"),
))
return out
diff --git a/idatui/rpc.py b/idatui/rpc.py
index 908a26c..59eec86 100644
--- a/idatui/rpc.py
+++ b/idatui/rpc.py
@@ -310,7 +310,7 @@ def disassembly(app, target=None, max_lines: int = 2000) -> dict[str, Any]:
def _xref_dicts(xs, limit: int) -> list[dict[str, Any]]:
- return [{"frm": x.frm, "to": x.to, "type": x.type,
+ return [{"frm": x.frm, "to": x.to, "type": x.type, "kind": x.kind,
"fn_addr": x.fn_addr, "fn_name": x.fn_name} for x in xs[:limit]]
diff --git a/server/patch_server.py b/server/patch_server.py
index 6b69556..6862785 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -503,6 +503,85 @@ def heads(
ea = _advance(ea)
cursor = {"next": hex(ea)} if more else {"done": True}
return {"addr": str(addr), "heads": rows, "cursor": cursor}
+
+
+@tool
+@idasync
+def xref_types(
+ queries: Annotated[list, "[{addr, direction:'to'|'from'|'both', include_fn, dedup, count}]"],
+) -> dict:
+ """Like xref_query, but every row carries a fine-grained ``kind`` derived from
+ the IDA xref type \u2014 call/jump/flow for code, read/write/offset/text/info for
+ data \u2014 alongside the coarse ``type`` (code/data). Feeds the xref dialog's
+ r/w/call badges. Same query/envelope shape as xref_query."""
+ import idaapi, idautils, ida_funcs, ida_bytes, ida_xref
+ code_kind = {ida_xref.fl_CF: "call", ida_xref.fl_CN: "call",
+ ida_xref.fl_JF: "jump", ida_xref.fl_JN: "jump",
+ ida_xref.fl_F: "flow"}
+ data_kind = {ida_xref.dr_O: "offset", ida_xref.dr_W: "write",
+ ida_xref.dr_R: "read", ida_xref.dr_T: "text", ida_xref.dr_I: "info"}
+
+ def _kind(xr):
+ table = code_kind if xr.iscode else data_kind
+ return table.get(xr.type, "code" if xr.iscode else "data")
+
+ def _fn(ea):
+ f = ida_funcs.get_func(ea)
+ if not f:
+ return None
+ return {"addr": hex(f.start_ea), "name": ida_funcs.get_func_name(f.start_ea)}
+
+ def _resolve(raw):
+ raw = str(raw).strip()
+ try:
+ return int(raw, 16) # handles '0x2490' and '2490'
+ except ValueError:
+ return idaapi.get_name_ea(idaapi.BADADDR, raw)
+
+ qs = queries if isinstance(queries, list) else [queries]
+ result = []
+ for q in qs:
+ q = q if isinstance(q, dict) else {"addr": q}
+ raw = str(q.get("addr", "")).strip()
+ direction = str(q.get("direction", "to") or "to").lower()
+ include_fn = bool(q.get("include_fn", True))
+ dedup = bool(q.get("dedup", True))
+ try:
+ count = int(q.get("count", 2000) or 2000)
+ except (TypeError, ValueError):
+ count = 2000
+ target = _resolve(raw)
+ rows = []
+ if target is not None and target != idaapi.BADADDR and ida_bytes.is_mapped(target):
+ if direction in ("to", "both"):
+ for xr in idautils.XrefsTo(target, 0):
+ row = {"direction": "to", "addr": hex(xr.frm), "from": hex(xr.frm),
+ "to": hex(target), "type": "code" if xr.iscode else "data",
+ "kind": _kind(xr)}
+ if include_fn:
+ row["fn"] = _fn(xr.frm)
+ rows.append(row)
+ if direction in ("from", "both"):
+ for xr in idautils.XrefsFrom(target, 0):
+ row = {"direction": "from", "addr": hex(xr.to), "from": hex(target),
+ "to": hex(xr.to), "type": "code" if xr.iscode else "data",
+ "kind": _kind(xr)}
+ if include_fn:
+ row["fn"] = _fn(xr.to)
+ rows.append(row)
+ if dedup:
+ seen = set()
+ deduped = []
+ for r in rows:
+ k = (r["direction"], r["from"], r["to"], r["kind"])
+ if k in seen:
+ continue
+ seen.add(k)
+ deduped.append(r)
+ rows = deduped
+ rows = rows[:count]
+ result.append({"query": raw, "data": rows, "next_offset": None})
+ return {"result": result}
'''
SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n"