aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/PROJECTS.md11
-rw-r--r--idatui/app.py59
-rw-r--r--tests/test_project_ui.py39
3 files changed, 101 insertions, 8 deletions
diff --git a/docs/PROJECTS.md b/docs/PROJECTS.md
index e5359e0..f4acc91 100644
--- a/docs/PROJECTS.md
+++ b/docs/PROJECTS.md
@@ -187,6 +187,17 @@ trigram floor, which matters — plenty of real exports are one or two character
is evicted; that is the reason the index is on disk. The switch itself then pages
the provider in through the normal pool path.
+The reverse direction is in the xrefs dialog. `xrefs_to` only ever sees the
+current database, so an exported function looks unused from the inside even when
+half the project calls it. `_foreign_importers` appends the project binaries that
+IMPORT the symbol — read from the on-disk index, so a caller shows up whether or
+not its worker is resident — and choosing one carries a `(binary, addr)` payload
+through the same switch path as a search hit, hop included. From libc's
+`strrchr`: `0000C318 import [echo] strrchr`.
+
+It only fires for a symbol this binary actually exports. A local name that
+happens to collide with another binary's import is not a caller of ours.
+
When nothing in the project provides the symbol, `_follow_import` declines and
the local navigation proceeds — landing on the stub is still the honest answer,
and a single-binary session behaves exactly as before.
diff --git a/idatui/app.py b/idatui/app.py
index cef58f1..ce2c315 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -2008,8 +2008,10 @@ class XrefsScreen(ModalScreen):
BINDINGS = [Binding("escape", "close", "Close")]
- def __init__(self, label: str, items: list[tuple[int, str]],
+ def __init__(self, label: str, items: list[tuple[object, str]],
preselect: int = 0) -> None:
+ # payload is an int address, or (binary, address) for a caller in another
+ # project binary; dismiss() hands it back untouched.
super().__init__()
self._label = label
self._items = items
@@ -4467,9 +4469,45 @@ class IdaTui(App):
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)
+ # Callers in OTHER project binaries. xrefs_to only ever sees this
+ # database, so an exported function looks unused from the inside even
+ # when half the project calls it — the import side of the linkage index
+ # is the only place that knowledge exists.
+ for label_b, addr_b, text_b in self._foreign_importers(subj, subj_name, fn):
+ items.append(((label_b, addr_b), text_b))
self.app.call_from_thread(self._present_xrefs, label, items, focus, preselect)
- def _present_xrefs(self, label: str, items: list[tuple[int, str]],
+ def _foreign_importers(self, subj: int, subj_name, fn): # type: ignore[no-untyped-def]
+ """Project binaries that IMPORT the symbol at ``subj`` — the other half
+ of the phase-3 join, read from the on-disk index so a caller shows up
+ whether or not its worker is resident.
+
+ Only for a symbol this binary actually exports: a local name that
+ happens to collide with another binary's import isn't a caller of ours.
+ """
+ if self._index is None or self._project is None or self._binary is None:
+ return []
+ name = subj_name if self._looks_like_symbol(subj_name) else None
+ if name is None and fn is not None and fn.addr == subj:
+ name = fn.name
+ if not name:
+ return []
+ from .domain import link_name
+ name = link_name(name)
+ try:
+ _, exports = self.program.linkage()
+ except Exception: # noqa: BLE001
+ return []
+ if not any(e.name == name for e in exports):
+ return [] # we don't export it; nobody imports it FROM US
+ try:
+ hits = self._index.importers(name, exclude=self._binary)
+ except Exception: # noqa: BLE001
+ return []
+ return [(h.binary, h.addr, f"{h.addr:08X} import [{h.binary}] {name}")
+ for h in hits]
+
+ def _present_xrefs(self, label: str, items: list[tuple[object, str]],
focus_name: str | None = None, preselect: int = 0) -> None:
if not self._xref_active:
return # cancelled (Esc) while we were still gathering
@@ -4482,12 +4520,17 @@ class IdaTui(App):
self._status(f"{label}: {len(items)}")
self.push_screen(XrefsScreen(label, items, preselect), self._on_xref_chosen)
- def _on_xref_chosen(self, addr: int | None) -> None:
- if addr is not None:
- # If xrefs was invoked from the decompiler, land the jump back in the
- # decompiler (when the target is decompilable) rather than the listing.
- self._goto_ea(addr, push=True, focus_name=self._xref_focus_name,
- prefer_decomp=(self._active == "decomp"))
+ def _on_xref_chosen(self, addr) -> None: # type: ignore[no-untyped-def]
+ if addr is None:
+ return
+ if isinstance(addr, tuple): # a caller in another project binary
+ binary, ea = addr
+ self._switch_then_goto(binary, ea) # records a hop, so Esc returns
+ return
+ # If xrefs was invoked from the decompiler, land the jump back in the
+ # decompiler (when the target is decompilable) rather than the listing.
+ self._goto_ea(addr, push=True, focus_name=self._xref_focus_name,
+ prefer_decomp=(self._active == "decomp"))
# -- rename ------------------------------------------------------------ #
@staticmethod
diff --git a/tests/test_project_ui.py b/tests/test_project_ui.py
index 47e511a..54c06bd 100644
--- a/tests/test_project_ui.py
+++ b/tests/test_project_ui.py
@@ -201,6 +201,45 @@ async def run(bins):
check("the hop is consumed, not repeated",
not app._hops, f"hops={app._hops}")
+ # -- xrefs: callers in OTHER project binaries ------------------ #
+ # xrefs_to only sees this database, so an exported function looks
+ # unused from the inside even when the rest of the project calls it.
+ # The selection rule is "only for a symbol we actually export"; two
+ # executables share no linkage, so here it must stay quiet.
+ from idatui.app import XrefsScreen
+ fake = app._foreign_importers(app._cur.ea, "strrchr", None)
+ check("no cross-binary callers for a symbol this binary doesn't export",
+ fake == [], f"{fake}")
+
+ # The routing a real cross-binary caller takes: the dialog carries a
+ # (binary, addr) payload instead of a bare address, and choosing it
+ # goes through the same switch path as a search hit — hop included,
+ # so Esc comes back.
+ where_from = app._binary
+ other = first if where_from == second else second
+ hit = next((h for h in app._index.search("main", limit=200)
+ if h.binary == other), None)
+ if hit is None:
+ check("a cross-binary xref jumps to the other binary", False,
+ "no symbol found in the other binary")
+ else:
+ hops0 = len(app._hops)
+ app.push_screen(
+ XrefsScreen("xrefs to fake", [((hit.binary, hit.addr),
+ f"{hit.addr:08X} import [{hit.binary}]")]),
+ app._on_xref_chosen)
+ await settle(lambda: isinstance(app.screen, XrefsScreen), 20)
+ await pilot.press("enter")
+ jumped = await settle(lambda: app._binary == other
+ and app._func_index is not None
+ and app._func_index.complete, 180)
+ check("a cross-binary xref jumps to the other binary", jumped,
+ f"binary={app._binary} want={other}")
+ check("and records a hop so Esc returns",
+ len(app._hops) == hops0 + 1 and app._hops[-1] == where_from,
+ f"hops={app._hops}")
+ app._hops.clear()
+
# -- and the same toggle for strings --------------------------- #
from idatui.app import StringsPalette
await pilot.press("quotation_mark")