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())
|