diff options
| author | blasty <blasty@local> | 2026-07-25 23:23:14 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 23:23:14 +0200 |
| commit | 0a6fc6c215cc64b5da160ee755f7609913754c10 (patch) | |
| tree | caab3816eddef430dde4ddc2216f08773c741ebb /idatui | |
| parent | projects phase 4: cross-binary back, linkage-guided pre-warm, project verbs (diff) | |
| download | ida-tui-0a6fc6c215cc64b5da160ee755f7609913754c10.tar.gz ida-tui-0a6fc6c215cc64b5da160ee755f7609913754c10.tar.xz ida-tui-0a6fc6c215cc64b5da160ee755f7609913754c10.zip | |
xrefs: show project binaries that import this export
xrefs_to only ever sees the current database, so an exported function looks
unused from the inside even when the rest of the project calls it. The import
side of the phase-3 linkage index already knew better; nothing surfaced it.
_foreign_importers appends those callers to the xrefs dialog. From libc's
strrchr, with echo in the project:
0000C318 import [echo] strrchr
Read from the on-disk index, so a caller appears whether or not its worker is
resident. Choosing one carries a (binary, addr) payload instead of a bare
address; _on_xref_chosen routes that through _switch_then_goto — the same path a
project search hit takes — so it records a hop and Esc comes back.
Only fires for a symbol this binary actually EXPORTS. A local name that happens
to collide with some other binary's import is not a caller of ours, and without
that check every common name (main, read, error) would sprout fictional callers.
Names are compared after link_name(), so ELF versioning doesn't hide the match.
Verified on a real echo+libc project: the dialog lists echo's call site, and
selecting it switches to echo, lands on 0xc318 and leaves hops=['libc.so.6'].
tests: +3 project UI — a symbol we don't export gets no cross-binary callers,
the (binary, addr) payload jumps to the other binary, and it records the hop.
195/0 scenarios, 30/0 project UI.
Diffstat (limited to 'idatui')
| -rw-r--r-- | idatui/app.py | 59 |
1 files changed, 51 insertions, 8 deletions
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 |
