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
121
122
123
124
125
126
|
#!/usr/bin/env python3
"""Correctness gate for the listing after an item edit (run by .auto/checks.sh).
An item edit (`c`/`d`/`u`/`p`) changes structure, but only locally: every head
in front of it keeps its address and its row number. So `Program.bump_items(ea)`
keeps the walk up to there instead of discarding the model — worth 257x on a big
binary (4.9s to make one byte into data, against 19ms).
Keeping *anything* across a structural edit is the risky half of that, and it
fails silently: the pane shows rows that are no longer what the database says.
So this drives real edits and compares the kept model against one built from
scratch, row for row — narrow reads (what painting does) and wide ones (what
building the search body does).
The staged database is a throwaway copy and is never saved, so the edits here do
not need undoing and can be as destructive as they like.
~/ida-venv/bin/python .auto/check_edit.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 snapshot(model, total: int, wide: bool):
if wide:
rows = []
for base in range(0, total, 4096):
rows.extend(model.window(base, min(4096, total - base)))
else:
rows = [model.get(i) for i in range(total)]
return [(h.ea, h.kind, h.size, h.text, h.name) if h else None for h in rows]
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 = sorted(idx.all_loaded(), key=lambda f: f.addr)
if len(funcs) < 8:
print(f"{target}: too few functions to check")
return 1
seg = funcs[len(funcs) // 2].addr
model = prog.listing(seg)
model.load_all()
total = len(model)
# Edits at a spread of positions: near the end (where truncation keeps
# nearly everything), the middle, and early on (where it must give up).
spots = [funcs[int(len(funcs) * f)].addr for f in (0.9, 0.5, 0.05)]
for k, ea in enumerate(spots):
# Undefine, at three sizes. This is the edit that can coalesce
# BACKWARDS into the undefined run in front of it, which is the
# reason truncate_from drops two pages rather than one.
size = (1, 4, 16)[k % 3]
kind = f"undefine {size}B"
try:
prog.undefine(ea, size)
except Exception as e: # noqa: BLE001
fails.append(f"{kind} at {ea:#x} failed: {e}")
continue
prog.bump_items(ea)
kept = prog.listing(seg)
kept_total = None
got_wide = None
if kept is not None:
kept.load_all()
kept_total = len(kept)
got_wide = snapshot(kept, kept_total, wide=True)
got_narrow = snapshot(kept, kept_total, wide=False)
# ... against a model that knows nothing about what came before.
prog._listings.clear()
fresh = prog.listing(seg)
fresh.load_all()
want = snapshot(fresh, len(fresh), wide=False)
if kept_total != len(fresh):
fails.append(f"{kind} at {ea:#x}: kept model has {kept_total} "
f"rows, a rebuild has {len(fresh)}")
elif got_narrow != want or got_wide != want:
which = "narrow" if got_narrow != want else "wide"
bad = next((i for i, (a, b) in
enumerate(zip(got_narrow if which == "narrow"
else got_wide, want)) if a != b), None)
fails.append(
f"{kind} at {ea:#x}: {which} read differs from a rebuild at "
f"row {bad}: {(got_narrow if which == 'narrow' else got_wide)[bad]}"
f" vs {want[bad]}")
model = prog.listing(seg)
model.load_all()
total = len(model)
print(f"item edits: {len(spots)} edits checked against a rebuild "
f"({total} rows), {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())
|