aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_globalcap_audit.py
blob: c6e46d9a60586da8832dc922fa48dfb6b223abd1 (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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/usr/bin/env python3
"""Estimate capacity for copies whose destination is a GLOBAL, not a stack buffer.

The capacity pass could only decide copies into stack frames (`sub sp,#F`, dst `sp+k`). That
left 48 SYS_DRV sites undecided, their destinations being globals or caller pointers. For a
global destination the extent can be estimated the same way GCP's `cmd02` was read by hand:

    dst = 0x407000, length 0x1926 constant, and 0x408926 is itself a referenced object
    -> the object at 0x407000 is exactly 0x1926 bytes, and the copy exactly fits

So: collect every literal-pool pointer the module references, sort them, and take a
destination global's capacity as the distance to the next referenced address above it. That is
an ESTIMATE -- adjacent objects may be unreferenced, making it conservative (too small), which
biases toward over-reporting rather than silence.

HONEST STATUS: this tool has a NEGATIVE control (GCP `cmd02` must come out safe: 0x1926 into
0x1926) but NO positive control -- no known bug in this corpus copies into a global with an
over-long length. Per this tree's rule, its output is therefore a TRIAGE LIST, not a verdict,
and every candidate must be read. A zero result here is not evidence.

Usage:
  ./ta_globalcap_audit.py --selftest
  ./ta_globalcap_audit.py --module sys_drv|asd|<ta>
"""
from __future__ import annotations

import argparse
import re
import struct

import ta_capacity_audit as CAP


def referenced_globals(b: bytes, base: int, fns: list) -> list[int]:
    """Every literal-pool word that looks like an in-image pointer."""
    out = set()
    lo, hi = base, base + len(b)
    for off in range(0, len(b) - 4, 4):
        v = struct.unpack_from("<I", b, off)[0]
        if lo <= v < hi:
            out.add(v)
    return sorted(out)


def dest_global(ins, lo, at):
    """If the copy destination (r0) came from a literal-pool pointer, return it."""
    cur = "r0"
    for _ in range(6):
        for k in range(at - 1, lo - 1, -1):
            i = ins[k]
            m = re.match(r"(%s),\s*(.+)$" % re.escape(cur), i.op_str or "")
            if not m or i.mnemonic.startswith(("cmp", "str", "push", "tst", "b")):
                continue
            rhs = m.group(2).strip()
            if i.mnemonic.startswith("ldr") and "pc" in rhs:
                return ("literal", k)
            mm = re.fullmatch(r"(r\d+|sb|sl|fp|ip)", rhs)
            if mm:
                cur, at = mm.group(1), k
                break
            return (None, None)
        else:
            return (None, None)
    return (None, None)


def literal_value(b, base, ins, k):
    """Resolve `ldr rX,[pc,#imm]` at instruction index k to its loaded word."""
    i = ins[k]
    m = re.search(r"#(0x[0-9a-f]+|\d+)", i.op_str or "")
    if not m:
        return None
    lit = ((i.address + 4) & ~3) + int(m.group(1), 0)
    off = lit - base
    if not (0 <= off < len(b) - 4):
        return None
    return struct.unpack_from("<I", b, off)[0]


def analyse(name):
    b, base, fns = CAP.load(name)
    fns = CAP.split_merged(b, base, fns)
    hp = CAP.helpers(b, base, fns)
    globs = referenced_globals(b, base, fns)
    rows = []
    for ea, sz in fns:
        o = ea - base
        if o < 0 or sz <= 0 or o + sz > len(b):
            continue
        ins = list(CAP.THUMB.disasm(b[o:o + sz], ea))
        for n, i in enumerate(ins):
            if not (i.mnemonic in ("blx", "bl") and (i.op_str or "").startswith("#")):
                continue
            if int(i.op_str[1:], 16) not in hp:
                continue
            kind, k = dest_global(ins, 0, n)
            if kind != "literal":
                continue
            g = literal_value(b, base, ins, k)
            if g is None or not (base <= g < base + len(b)):
                continue
            nxt = next((x for x in globs if x > g), None)
            if nxt is None:
                continue
            cap = nxt - g
            mx, how = CAP.max_len(ins, 0, n, "r2")
            if mx is None:
                continue
            if mx > cap:
                rows.append((i.address, ea, g, cap, mx, how))
    return rows


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--selftest", action="store_true")
    ap.add_argument("--module", default="sys_drv")
    a = ap.parse_args()

    if a.selftest:
        # NEGATIVE control: GCP cmd02 copies 0x1926 into the object at 0x407000, which is
        # exactly 0x1926 bytes (0x408926 is the next referenced object). It must not appear.
        rows = analyse("gcp")
        bad = [r for r in rows if r[0] == 0x404C98]
        print("SELFTEST (global-destination capacity)")
        print("   NEGATIVE gcp cmd02 0x404c98 not reported: %s"
              % ("yes" if not bad else "NO -> %s" % (bad,)))
        print("   POSITIVE control: NONE AVAILABLE -- no known bug in this corpus copies")
        print("      over-long into a global, so this tool is a TRIAGE LIST, not a verdict.")
        print("\nverdict: usable for triage only (negative control holds: %s)"
              % ("yes" if not bad else "no"))
        return 0 if not bad else 1

    rows = analyse(a.module)
    print("=== %s: %d global-destination site(s) where max length exceeds the estimated extent"
          % (a.module, len(rows)))
    for site, fn, g, cap, mx, how in rows[:20]:
        print("   copy @%#x (fn %#x) dst global %#x extent ~%d, max %d (%s)"
              % (site, fn, g, cap, mx, how))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())