aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_fieldcap_audit.py
blob: 10744c3f89248dc92319799f06165755fb9ac5cd (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
145
146
147
#!/usr/bin/env python3
"""Decide each field-flow site: writer WIDTH x scaling vs destination capacity.

Pass twelve left SYS_DRV's field-flow sites "characterised as type-bounded narrow fields" --
a description, not a verdict. The deciding facts are available:

  * the field's maximum value follows from the WIDTH of the stores that write it
    (`strb` -> 255, `strh` -> 65535, `str` -> unknown/32-bit);
  * the scaling applied between the load and the copy (`lsl #n`, `add rD,rA,rA,lsl #n`);
  * the destination capacity when the copy targets a stack buffer (`sub sp,#F`, dst `sp+k`).

A site is only dangerous when `max_value x scaling > capacity`. That is exactly the arithmetic
that makes HDCP's byte field (255 x 5 = 1275 into 276 bytes) a bug while a byte field copied
into a 4 KB buffer is not.

CONTROL: hdcp `0x402cde` must be decided UNSAFE; xgmi `0x402d4c` must be decided safe.

Usage:
  ./ta_fieldcap_audit.py --selftest
  ./ta_fieldcap_audit.py --module sys_drv
"""
from __future__ import annotations

import argparse
import re
from collections import Counter

import ta_capacity_audit as CAP
import ta_fieldflow2 as FF


def store_widths(ins, fns, off):
    """Widths of every verified store to field +off: 1, 2 or 4 bytes."""
    w = Counter()
    for i in ins:
        if not i.mnemonic.startswith("str"):
            continue
        m = FF.STOREF.match(i.op_str or "")
        if not m or int(m.group(3), 0) != off or m.group(2) in ("sp", "fp"):
            continue
        if i.mnemonic.startswith("strb"):
            w[1] += 1
        elif i.mnemonic.startswith("strh"):
            w[2] += 1
        else:
            w[4] += 1
    return w


def analyse(name):
    b, base, fns = FF.load_module(name) if hasattr(FF, "load_module") else CAP.load(name)
    fns = CAP.split_merged(b, base, fns)
    hp = CAP.helpers(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))
        frame = None
        for i in ins[:8]:
            m = re.match(r"sp,\s*(?:sp,\s*)?#(0x[0-9a-f]+|\d+)$", i.op_str or "")
            if m and i.mnemonic in ("sub", "subw", "sub.w"):
                frame = int(m.group(1), 0)
                break
        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
            off = FF.field_of(ins, 0, n, "r2")
            if off is None:
                continue
            mx, how = CAP.max_len(ins, 0, n, "r2")
            cap, koff = (CAP.capacity(ins, 0, n, frame) if frame else (None, None))
            rows.append((i.address, ea, off, mx, how, cap, koff))
    return rows


def widths_for(name, offs):
    b, base, fns = CAP.load(name)
    fns = CAP.split_merged(b, base, fns)
    allins = []
    for ea, sz in fns:
        o = ea - base
        if 0 <= o and o + sz <= len(b) and sz > 0:
            allins.extend(CAP.THUMB.disasm(b[o:o + sz], ea))
    return {off: store_widths(allins, fns, off) for off in offs}


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:
        hd = [r for r in analyse("hdcp") if r[0] == 0x402CDE]
        xg = [r for r in analyse("xgmi") if r[0] == 0x402D4C]
        ok = True
        print("SELFTEST (width x scaling vs capacity)")
        for tag, rows, want_unsafe in (("hdcp 0x402cde", hd, True), ("xgmi 0x402d4c", xg, False)):
            if not rows:
                print("   %-16s NOT FOUND -> untrustworthy" % tag)
                ok = False
                continue
            _s, _f, off, mx, how, cap, _k = rows[0]
            unsafe = (mx is not None and cap is not None and mx > cap)
            print("   %-16s max %s (%s) vs capacity %s -> %s  %s"
                  % (tag, mx, how, cap, "UNSAFE" if unsafe else "safe",
                     "OK" if unsafe == want_unsafe else "WRONG"))
            ok &= (unsafe == want_unsafe)
        print("\nverdict: %s" % ("TRUSTWORTHY" if ok else "NOT trustworthy"))
        return 0 if ok else 1

    rows = analyse(a.module)
    offs = sorted({r[2] for r in rows})
    wid = widths_for(a.module, offs)
    dec = Counter()
    unsafe = []
    print("=== %s: deciding %d field-flow copy site(s)" % (a.module, len(rows)))
    for site, fn, off, mx, how, cap, koff in rows:
        w = wid.get(off, Counter())
        maxw = max(w) if w else 4
        typemax = {1: 0xFF, 2: 0xFFFF, 4: None}[maxw]
        if cap is None:
            dec["no stack destination (capacity unknown)"] += 1
            continue
        if typemax is None:
            dec["32-bit field (width gives no bound)"] += 1
            unsafe.append((site, fn, off, "word", cap))
            continue
        if typemax > cap:
            dec["width %d-byte COULD exceed capacity" % maxw] += 1
            unsafe.append((site, fn, off, "%d-byte max %d" % (maxw, typemax), cap))
        else:
            dec["width %d-byte cannot exceed capacity %d" % (maxw, cap)] += 1
    for k, v in dec.most_common():
        print("   %-52s %d" % (k, v))
    print("\n   sites needing a human read: %d" % len(unsafe))
    for site, fn, off, why, cap in unsafe[:12]:
        print("      copy @%#x (fn %#x) field +%#x  %s  capacity %d" % (site, fn, off, why, cap))
    return 0


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