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 /tests/test_project_ui.py | |
| 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 'tests/test_project_ui.py')
| -rw-r--r-- | tests/test_project_ui.py | 39 |
1 files changed, 39 insertions, 0 deletions
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") |
