aboutsummaryrefslogtreecommitdiffstats
path: root/.auto/check_rename.py
blob: c617aa65c120247ecfb10aba4dee404e88b9b08e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/usr/bin/env python3
"""Correctness gate for the listing's rename handling (run by .auto/checks.sh).

A rename does not move any listing row, so ``Program.bump_names`` keeps the
segment's walk and only marks the rendered text stale; ``ListingModel`` re-renders
a block at a time as rows are read. That is worth 500x on a big binary (the
alternative re-walks the whole segment to find a row the cursor was already on),
and it is exactly the kind of optimisation that fails *quietly*: the pane keeps
showing the old name and nothing errors.

Two things are checked, because they exercise different paths and only the first
was ever caught by accident:

* a NARROW read (``get`` per row, what painting does), and
* a WIDE read (``window`` over thousands of rows, what search's body build does)

both have to come back with the new name -- and the whole model has to match one
built from scratch, row for row.

    ~/ida-venv/bin/python .auto/check_rename.py [targets/echo]
"""
from __future__ import annotations

import os
import shutil
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
sys.path.insert(0, ROOT)
sys.path.insert(0, HERE)

from bench import stage  # noqa: E402
from idatui.domain import Program  # noqa: E402
from idatui.worker_client import WorkerClient  # noqa: E402


def rename(client, ea: int, name: str) -> None:
    client.call("rename", batch={"func": [{"addr": hex(ea), "name": name}]})


def main() -> int:
    target = sys.argv[1] if len(sys.argv) > 1 else "targets/echo"
    d, path = stage(os.path.join(ROOT, target))
    fails: list[str] = []
    client = WorkerClient(path)
    try:
        prog = Program(client)
        idx = prog.functions()
        idx.load_all()
        funcs = idx.all_loaded()
        if len(funcs) < 4:
            print(f"{target}: too few functions to check")
            return 1
        seg = sorted(funcs, key=lambda f: f.addr)[len(funcs) // 2].addr
        model = prog.listing(seg)
        model.load_all()
        total = len(model)

        for k, victim in enumerate(sorted(funcs, key=lambda f: -f.size)[:2]):
            new = f"_check_rename_{os.getpid()}_{k}"
            rename(client, victim.addr, new)
            prog.bump_names()

            # WIDE read -- what building the search body does. This is the one
            # that used to come back with the old names: an oversized refetch
            # overflowed the heads tool's row cap, failed its sequence check and
            # left the block untouched.
            wide = []
            for base in range(0, total, 4096):
                wide.extend(model.window(base, min(4096, total - base)))
            # "Is the old name gone" is not a sound test -- `main` is a token of
            # `__libc_start_main` and can legitimately appear in a comment or a
            # string. That the NEW name arrived proves the refresh ran; that the
            # model matches a rebuild, below, proves it ran correctly.
            shown = sum(1 for h in wide if h is not None
                        and (new in (h.text or "") or new == (h.name or "")))
            if not shown:
                fails.append(f"wide read after renaming {victim.name} -> {new}: "
                             f"no row shows the new name")

            # NARROW read -- what painting does -- and the whole model against a
            # rebuild, row for row.
            kept = [(h.ea, h.kind, h.text, h.name) if h else None
                    for h in (model.get(i) for i in range(total))]
            prog._listings.clear()
            fresh_model = prog.listing(seg)
            fresh_model.load_all()
            fresh = [(h.ea, h.kind, h.text, h.name) if h else None
                     for h in (fresh_model.get(i) for i in range(len(fresh_model)))]
            if kept != fresh:
                bad = next((i for i, (a, b) in enumerate(zip(kept, fresh))
                            if a != b), None)
                fails.append(f"kept model != rebuilt model after renaming "
                             f"{victim.name}: first difference at row {bad}: "
                             f"{kept[bad] if bad is not None else None} vs "
                             f"{fresh[bad] if bad is not None else None}")

            rename(client, victim.addr, victim.name)
            prog.bump_names()
            prog._listings.clear()
            model = prog.listing(seg)
            model.load_all()
            total = len(model)

        print(f"rename handling: {total} rows checked wide and narrow, "
              f"{len(fails)} problems")
        for f in fails:
            print("  FAIL", f)
        return 1 if fails else 0
    finally:
        try:
            client.close()
        except Exception:  # noqa: BLE001
            pass
        shutil.rmtree(d, ignore_errors=True)


if __name__ == "__main__":
    sys.exit(main())