aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_name_audit.py
blob: 303e371baab26be04d4b40ca333ea5a42309d262 (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
"""Audit every derived function name against the firmware's own log strings.

The TAs log `AMD_TA_<X>: <FunctionName> ENTER` on entry to most command handlers. That
string is ground truth for what AMD calls the function, so it is a mechanical check on
names that were derived by hand -- and it catches drift that no amount of re-reading the
disassembly would.

Reports three classes:
  MISMATCH  the handler logs a name that our symbol does not contain
  OK        our symbol contains the logged name
  NOSTRING  no ENTER-style string in the function (nothing to check against)

Usage:  ./ta_name_audit.py [--prefix gcp] [--fix-json]
"""
from __future__ import annotations

import argparse
import json
import re
import struct
import sys
from pathlib import Path

import capstone as cs

HERE = Path(__file__).resolve().parent
BASE = 0x400100


def load(prefix: str):
    meta = json.loads((HERE / "syms" / ("%s.meta.json" % prefix)).read_text())
    body = Path(meta["path"])
    if not body.is_absolute():
        body = HERE.parent / body
    return json.loads((HERE / "libdumps" / ("%s.json" % prefix)).read_text()), body.read_bytes()


def cstr(b: bytes, addr: int) -> str | None:
    o = addr - BASE
    if not (0 <= o < len(b)):
        return None
    e = b.find(b"\x00", o)
    if e < 0 or e - o > 200:
        return None
    try:
        s = b[o:e].decode("ascii")
    except Exception:  # noqa: BLE001
        return None
    return s if s.isprintable() or "\n" in s else None


def strings_in(b: bytes, ea: int, size: int) -> list[str]:
    md = cs.Cs(cs.CS_ARCH_ARM, cs.CS_MODE_THUMB)
    out = []
    off = ea - BASE
    if off < 0 or size <= 0 or off + size > len(b):
        return out
    for i in md.disasm(b[off:off + size], ea):
        addr = None
        if i.mnemonic == "adr":
            m = re.search(r"#(-?0x[0-9a-f]+|-?\d+)", i.op_str)
            if m:
                v = m.group(1)
                addr = ((i.address + 4) & ~3) + (int(v, 16) if v.startswith(("0x", "-0x")) else int(v))
        elif i.mnemonic.startswith("ldr") and "pc" in i.op_str:
            m = re.search(r"#(0x[0-9a-f]+)", i.op_str)
            if m:
                a = ((i.address + 4) & ~3) + int(m.group(1), 16)
                try:
                    addr = struct.unpack_from("<I", b, a - BASE)[0]
                except Exception:  # noqa: BLE001
                    addr = None
        if addr is None:
            continue
        s = cstr(b, addr)
        if s and len(s) > 4:
            out.append(s)
    return out


NAME_RE = re.compile(r"AMD_TA_[A-Za-z0-9_]+:\s*([A-Za-z_][A-Za-z0-9_]*)")


def logged_name(strs: list[str]) -> str | None:
    """Prefer the ENTER string; fall back to the most common logged identifier."""
    for s in strs:
        m = NAME_RE.search(s)
        if m and "ENTER" in s:
            return m.group(1)
    counts: dict[str, int] = {}
    for s in strs:
        m = NAME_RE.search(s)
        if m:
            counts[m.group(1)] = counts.get(m.group(1), 0) + 1
    if not counts:
        return None
    return max(counts, key=lambda k: counts[k])


def norm(s: str) -> str:
    return re.sub(r"[^a-z0-9]", "", s.lower())


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--prefix")
    ap.add_argument("--json", default=None, help="write findings to this path")
    args = ap.parse_args()

    prefixes = [args.prefix] if args.prefix else sorted(
        p.name.split(".")[0] for p in (HERE / "libdumps").glob("*.json")
    )
    allrows = []
    for prefix in prefixes:
        try:
            fns, b = load(prefix)
        except Exception as exc:  # noqa: BLE001
            print("%-10s skipped (%s)" % (prefix, exc))
            continue
        mism, ok, nostr = [], 0, 0
        for f in fns:
            nm, ea, sz = f["name"], f["ea"], f.get("size", 0)
            if not sz:
                nostr += 1
                continue
            ln = logged_name(strings_in(b, ea, sz))
            if not ln:
                nostr += 1
                continue
            if norm(ln) in norm(nm):
                ok += 1
            else:
                mism.append({"prefix": prefix, "ea": hex(ea), "name": nm, "logged": ln})
        print("%-10s checked-against-strings: %3d OK, %3d MISMATCH, %3d no string"
              % (prefix, ok, len(mism), nostr))
        for m in mism:
            print("    MISMATCH %-12s %-46s logs %r" % (m["ea"], m["name"], m["logged"]))
        allrows += mism
    if args.json:
        Path(args.json).write_text(json.dumps(allrows, indent=2))
        print("\nwrote %s (%d mismatches)" % (args.json, len(allrows)))
    return 0


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