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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
#!/usr/bin/env python3
"""Generate every meaningful within-branch version diff into fw/diffs/.
Two correctness rules, both learned by getting them wrong first:
1. The third version field is a BRANCH, not a sequence. ASD ships 33.0.0.x, 33.0.4.x, 33.0.8.x,
33.0.12.x, 33.0.16.x, 33.0.24.x and 33.0.28.x in parallel. Comparing across them produces
nonsense -- asd_dispatch oscillating 842 <-> 1306 <-> 1352 bytes is branch identity, not
change over time. Only same-branch pairs are emitted, ordered by the final build number.
2. A raw diff is 6-17% of lines changed and almost none of it is firmware. Four noise sources are
normalised away: tier annotations, address-derived tokens (off_40480C -> off_40481C), long hex
literals, and local renumbering. See fw/ta_diff.py for the measurements.
Each pair gets a .diff with per-function hunks, and the size table from the index sidecars, which
is the one signal no decompiler instability can fake: same function, different byte size means the
code changed.
"""
import difflib
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
HERE = Path(__file__).resolve().parent
DECOMP = HERE / "decomp"
OUT = HERE / "diffs"
sys.path.insert(0, str(HERE))
import ta_metrics as M # noqa: E402
ADDR_TOKEN = re.compile(r"\b(off|dword|word|byte|loc|unk|sub|flt|dbl|jpt|a)_[0-9A-Fa-f]{4,8}\b")
HEX_LITERAL = re.compile(r"\b0x[0-9A-Fa-f]{5,8}\b")
TIER = re.compile(r"\[tier=\w+ size=\d+\]")
LOCAL = re.compile(r"\bv\d+\b")
BANNER = re.compile(r"/\* ==== (\S+)[^\n]*? ==== \*/")
DECL = re.compile(r"^\s*(int|unsigned|char|_BYTE|_DWORD|_WORD|__int64|bool|signed|void|struct"
r"|float|double|__int16|_QWORD)\b[^;]*;\s*(//.*)?$")
def ver(name):
m = re.search(r"_(\d+(?:\.\d+)+)_", name)
return tuple(int(x) for x in m.group(1).split(".")) if m else None
def norm(line):
line = TIER.sub("[..]", line)
line = ADDR_TOKEN.sub(lambda m: m.group(0).split("_")[0] + "_X", line)
line = HEX_LITERAL.sub("0xX", line)
return LOCAL.sub("vX", line)
def functions(path):
out, cur = {}, None
for line in path.read_text().splitlines():
m = BANNER.match(line.strip())
if m:
cur = m.group(1)
out[cur] = []
elif cur:
out[cur].append(line)
return out
def sizes(build):
p = DECOMP / f"{build}.index.json"
return {x["name"]: x.get("size") for x in json.loads(p.read_text())["functions"]
if M.is_meaningful(x.get("name"))}
def main():
builds = defaultdict(lambda: defaultdict(list))
for p in DECOMP.glob("*.index.json"):
b = p.name.replace(".index.json", "")
if "reference" in b or not ver(b):
continue
builds[b.split("_")[0]][ver(b)[:3]].append(b)
if OUT.exists():
for f in OUT.rglob("*"):
if f.is_file():
f.unlink()
OUT.mkdir(exist_ok=True)
index = []
for mod in sorted(builds):
for branch in sorted(builds[mod]):
group = sorted(builds[mod][branch], key=ver)
if len(group) < 2:
continue
for a, b in zip(group, group[1:]):
va, vb = ".".join(map(str, ver(a))), ".".join(map(str, ver(b)))
if va == vb:
continue
fa, fb = functions(DECOMP / f"{a}.c"), functions(DECOMP / f"{b}.c")
sa, sb = sizes(a), sizes(b)
resized = sorted(((n, sa[n], sb[n]) for n in sa.keys() & sb.keys()
if sa[n] != sb[n]), key=lambda t: -abs(t[2] - t[1]))
new, gone = sorted(sb.keys() - sa.keys()), sorted(sa.keys() - sb.keys())
hunks = []
for name in sorted(fa.keys() & fb.keys()):
na = [norm(x) for x in fa[name]]
nb = [norm(x) for x in fb[name]]
if na == nb:
continue
d = [l for l in difflib.unified_diff(na, nb, lineterm="", n=1)
if l[:1] in "+- " and not l.startswith(("+++", "---"))]
d = [l for l in d if not DECL.match(l[1:])]
if any(l[:1] in "+-" for l in d):
hunks.append((name, d))
if not (resized or new or gone or hunks):
continue
mdir = OUT / mod
mdir.mkdir(exist_ok=True)
f = mdir / f"{va}__to__{vb}.diff"
with f.open("w") as fh:
fh.write("# %s %s -> %s\n" % (mod, va, vb))
fh.write("# branch %s, consecutive builds\n#\n" % ".".join(map(str, branch)))
fh.write("# %d functions resized, %d new, %d removed, %d with body changes\n"
% (len(resized), len(new), len(gone), len(hunks)))
fh.write("# total named bytes %+d\n#\n"
% (sum(sb.values()) - sum(sa.values())))
fh.write("# Normalised: tier annotations, off_/loc_/sub_ address tokens,\n"
"# long hex literals and vN renumbering. Local declarations are\n"
"# dropped. Raw diffs are 6-17% of lines and almost all noise.\n\n")
if resized:
fh.write("## resized functions (byte size from the index sidecar)\n")
for n, x, y in resized:
fh.write(" %-56s %6d -> %-6d %+d\n" % (n, x, y, y - x))
fh.write("\n")
if new:
fh.write("## functions present only in %s\n" % vb)
for n in new:
fh.write(" + %-54s %s bytes\n" % (n, sb[n]))
fh.write("\n")
if gone:
fh.write("## functions present only in %s\n" % va)
for n in gone:
fh.write(" - %-54s %s bytes\n" % (n, sa[n]))
fh.write("\n")
fh.write("## body changes, per function\n\n")
for name, d in hunks:
fh.write("### %s\n" % name)
for l in d:
fh.write(" %s\n" % l.rstrip())
fh.write("\n")
index.append((mod, ".".join(map(str, branch)), va, vb,
len(resized), len(new), len(gone), len(hunks),
sum(sb.values()) - sum(sa.values()),
f.relative_to(OUT).as_posix()))
with (OUT / "INDEX.md").open("w") as fh:
fh.write("# TA version diffs\n\n")
fh.write("Generated by `fw/ta_diff_all.py`. Only **same-branch consecutive builds**: the\n"
"third version field is a branch, not a sequence, so cross-branch comparison is\n"
"meaningless (ASD's `asd_dispatch` merely oscillates 842/1306/1352 bytes between\n"
"branches). Noise normalised per `fw/ta_diff.py`.\n\n")
fh.write("""## Findings worth starting from
**HDCP 23.0.0.52 -> 23.0.0.57 is a timing side-channel fix.** `hdcp2_rx_LC_Send_L_prime`, the
HDCP 2.x locality check, stopped verifying L' with `memcmp_be` (early exit on the first differing
word) and switched to `ct_memcmp`, which ORs all 16 bytes with no early exit. That comparison is
exactly where an early return leaks a byte-at-a-time oracle. `receiver_id_lookup` still uses
`memcmp_be`, which is fine -- receiver IDs are not secret.
**GCP extends an ASIC allowlist at nearly every release.**
`gcp_cmdFE08_KeyExchangeAuthenticatedSession__h4` grows +10 bytes almost every version. It calls
`gcp_sysdrv_rpc_200()` and switches on the returned word, yielding 0 to proceed or 611 to reject:
14 allowed IDs at 22.0.0.53, then 16, 19, 20, 22, 24, 26, 28 by 22.0.0.77. `0xe` arrives late, at
.68, out of order. `0xd` is still absent.
**GCP 22.0.0.60 -> 22.0.0.62 adds teardown to the session lifecycle.** +1598 bytes concentrated in
`gcp_cmd01` AllocateDRMSession (+90), `gcp_cmd05` CloseDRMSession (+96) and `gcp_cmdFE0D`
DestroyCryptoSession (+68), each gaining the same new call sequence on its error and exit paths.
The callee is still unnamed, so what it releases is not established.
**Do not read ASD's dispatch case lists as change.** `asd_dispatch` appears to lose 48 `case`
labels at 163 -> 173 and regain them at 195 -> 239. That is Hex-Rays alternating between an
explicit `switch` and the range check `if (req->rpc_id - 384 <= 0x40)` -- 384 is 0x180 and
0x180+0x40 is 0x1C0, exactly the range that seems to vanish. The function does genuinely grow
778 -> 1352 bytes inside the 33.0.0 branch, but the case delta is not evidence of new RPCs.
""")
fh.write("`resized` is the trustworthy column -- same function, different byte size, taken\n"
"from the index sidecar rather than the decompiler. `new`/`gone` can also reflect a\n"
"function being named in one build and not another, so read them as leads.\n\n")
fh.write("| module | branch | from | to | resized | new | gone | bodies | bytes | file |\n")
fh.write("|---|---|---|---|--:|--:|--:|--:|--:|---|\n")
for r in index:
fh.write("| %s | %s | %s | %s | %d | %d | %d | %d | %+d | [%s](%s) |\n"
% (r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[9]))
print("%d diffs written to %s" % (len(index), OUT))
return 0
if __name__ == "__main__":
raise SystemExit(main())
|