aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 22:33:16 +0200
committerblasty <blasty@local>2026-07-25 22:33:16 +0200
commitdcb76f92e8851f747e17cae848e800a394a073e1 (patch)
tree7b2ef9e92049e7a394f707e313cb79ba65309ced /tests
parentnav: one notion of "which pane you're in" — delete _pref (diff)
downloadida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.tar.gz
ida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.tar.xz
ida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.zip
projects phase 3: follow an import into the binary that implements it
Following a call to strcmp reached the PLT/extern entry and stopped there — Hex-Rays has nothing to decompile, because the code lives in a library this binary only references. With the other binary open in the same project we already had everything needed to cross that gap; we just weren't indexing it. Index each binary's imports and exports (KIND_IMPORT / KIND_EXPORT) alongside its functions and strings. On a follow, _import_stub asks whether the target address is one of this binary's import stubs; if so _cross_binary_impl asks the index who exports that name, and we switch there instead of landing on the thunk. Verified end to end on a real echo + libc project: Enter on `strrchr(a1, 47)` in echo's pseudocode switches to libc.so.6 and lands on strrchr at 0xaf960. Three things it turns on: * ELF symbol versioning. The importer sees strrchr@@GLIBC_2.2.5 while the provider may export any of three spellings, so raw names resolve almost nothing. domain.link_name() cuts at the first '@'; Linkage.raw keeps what IDA reported, which is what the listing shows. * Exact match, not substring — ProjectIndex.exact(), so `read` doesn't bind to pread/read_line/thread_start. It also answers below the 3-char trigram floor, and plenty of real exports are that short. * Resolution reads the on-disk index, so a provider resolves while its worker is evicted. That's what the index was for. When nothing in the project provides the symbol _follow_import declines and the normal navigation runs: landing on the stub is still the honest answer, and a single-binary session is unchanged. The PLT-stub PRESENTATION item stays open — an unprovided import should say "imported, provider not in project" rather than show a decompiler error. server/patch_server.py gains list_linkage (idautils.Entries + enum_import_names); a worker without it degrades to no linkage rather than failing. tests: index join +8 (exact vs substring, short names, exclude-self, reverse join, kind isolation, forget unresolves) and link_name +4. 36/0 index, 195/0 scenarios, 23/0 project UI, 33/0 project, 22/0 pool.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_index.py71
1 files changed, 70 insertions, 1 deletions
diff --git a/tests/test_index.py b/tests/test_index.py
index d1e8a48..e14f715 100644
--- a/tests/test_index.py
+++ b/tests/test_index.py
@@ -11,7 +11,8 @@ import tempfile
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from idatui.index import KIND_FUNC, KIND_STRING, ProjectIndex # noqa: E402
+from idatui.index import (KIND_EXPORT, KIND_FUNC, KIND_IMPORT, # noqa: E402
+ KIND_STRING, ProjectIndex)
PASS = FAIL = 0
@@ -116,6 +117,74 @@ def main() -> int:
idx.counts() == {"libfoo": 1} and idx.search("socket bind") == [],
f"{idx.counts()}")
+ # -- cross-binary linkage join (phase 3) ------------------------------ #
+ idx.reindex("app", [
+ (KIND_FUNC, 0x1000, "main"),
+ (KIND_IMPORT, 0x2000, "strcmp"),
+ (KIND_IMPORT, 0x2008, "read"),
+ (KIND_IMPORT, 0x2010, "SSL_new"),
+ ])
+ idx.reindex("libc", [
+ (KIND_EXPORT, 0x8000, "strcmp"),
+ (KIND_EXPORT, 0x8100, "read"),
+ (KIND_EXPORT, 0x8200, "pread"),
+ (KIND_EXPORT, 0x8300, "read_line"),
+ (KIND_FUNC, 0x8000, "strcmp"),
+ ])
+ idx.reindex("libssl", [(KIND_EXPORT, 0x9000, "SSL_new")])
+
+ prov = idx.providers("strcmp", exclude="app")
+ check("an import resolves to the binary that exports it",
+ [(h.binary, h.addr) for h in prov] == [("libc", 0x8000)],
+ f"{[(h.binary, hex(h.addr)) for h in prov]}")
+
+ # The whole point of exact(): substring search would drag in pread,
+ # read_line and thread_start, and 'read' is also below the trigram floor
+ # for some engines — an import must bind to its exact name or nothing.
+ prov = idx.providers("read", exclude="app")
+ check("the join is exact, not substring",
+ [(h.binary, h.addr) for h in prov] == [("libc", 0x8100)],
+ f"{[(h.binary, h.text) for h in prov]}")
+
+ check("a short name still resolves (below the trigram floor)",
+ [h.binary for h in idx.providers("SSL_new", exclude="app")] == ["libssl"],
+ f"{idx.providers('SSL_new')}")
+
+ check("an unprovided import resolves to nothing",
+ idx.providers("dlopen", exclude="app") == [])
+
+ check("exclude keeps a binary from resolving to itself",
+ idx.providers("strcmp", exclude="libc") == [],
+ f"{idx.providers('strcmp', exclude='libc')}")
+
+ imp = idx.importers("strcmp")
+ check("the reverse join finds who imports an export",
+ [(h.binary, h.addr) for h in imp] == [("app", 0x2000)],
+ f"{[(h.binary, hex(h.addr)) for h in imp]}")
+
+ check("kind keeps functions out of the linkage join",
+ [h.binary for h in idx.providers("strcmp")] == ["libc"],
+ "a KIND_FUNC row named strcmp must not answer as an export")
+
+ idx.forget("libc")
+ check("forgetting a provider unresolves its imports",
+ idx.providers("strcmp", exclude="app") == [])
+
+ # -- ELF symbol versioning -------------------------------------------- #
+ # The importer sees strrchr@@GLIBC_2.2.5 while the provider may export a
+ # different spelling; raw names would resolve almost nothing. link_name
+ # cuts at the first '@' so both sides meet on the bare symbol.
+ from idatui.domain import link_name
+ check("link_name strips an ELF version suffix",
+ link_name("strrchr@@GLIBC_2.2.5") == "strrchr",
+ link_name("strrchr@@GLIBC_2.2.5"))
+ check("link_name leaves an unversioned name alone",
+ link_name("strrchr") == "strrchr")
+ check("link_name handles a single-@ version",
+ link_name("SSL_new@OPENSSL_3.0.0") == "SSL_new")
+ check("link_name doesn't eat a leading @",
+ link_name("@weird") == "@weird", link_name("@weird"))
+
# -- persistence ------------------------------------------------------ #
path = idx.path
idx.close()