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
|
# Security review of all 55 version diffs
Every diff in this directory was screened for memory-safety and object-lifetime changes: added or
removed bounds checks, changed comparison constants, copy-size changes, refcount and
free/release restructuring, and validation added to host-facing entry points.
**Method note, and it matters more than any single finding.** Screening the decompiled C produced
mostly false positives. Three independent traps were hit and each had to be settled by reading the
instruction bytes:
* Hex-Rays **elides dead stores** to a local that is never read back, but cannot elide a call. A
function that initialised its whole stack frame with inline stores therefore decompiles to a bare
`SVC`, and the same function using `bzero_core()` decompiles with a visible call -- which looks
exactly like a zero-initialisation fix and is not one.
* IDA sometimes **mis-analyses a tail call as a thunk** and silently drops the rest of the function.
`merit_cmd03` lost 39 lines that way in one build, inventing an apparent "+3 bounds checks".
* **Semantically identical rewrites** are everywhere: `x >> 30 != 0` against `x >= 0x40000000`,
`x > 0x1F` against `x >= 0x20`, `len >= 0x2024` against `len > 0x2023`.
So each candidate below was verified by extracting the function's bytes from both ELFs and
diffing them. A function whose bytes differ only at a handful of isolated offsets with small deltas
is a relocation artifact, not a change.
---
## 1. CONFIRMED security fix: HDCP timing side-channel
**HDCP 23.0.0.52 -> 23.0.0.57**, `hdcp2_rx_LC_Send_L_prime`.
The HDCP 2.x **locality check** verifies L'. The comparison used changed:
```c
// 23.0.0.52
if ( memcmp_be(v9, (unsigned int *)a2, v10) != 0 ) // early exit:
// if (v3 != v4) return ±1;
// 23.0.0.57
if ( ct_memcmp(v9, a2, v10) != 0 ) // no early exit:
// v3 |= v4 ^ v6; over all bytes
```
`memcmp_be` returns as soon as a word differs. `ct_memcmp` accumulates every byte difference into
one accumulator and only then returns. L' verification is precisely where an early return leaks a
byte-at-a-time timing oracle, so this is a real hardening fix and the only confirmed one in the
corpus.
Note `receiver_id_lookup` still uses `memcmp_be` in 23.0.0.57, which is appropriate -- receiver IDs
are not secrets.
## 2. Behaviour change, security relevance unresolved: SYS_DRV service 0x208
A new error-path call was added to exactly the four content-protection TAs, within a narrow window,
and to none of the other twelve modules:
| module | first build with `sysdrv_rpc_208` |
|---|---|
| GCP | 22.0.0.62 |
| HDCP | 23.0.0.59 |
| MERIT | 24.0.0.56 |
| OPM | 25.0.0.62 |
absent in all builds of asd, auc, csu, dtm, fp, oc, powerplay, rap, ras, tmz, wspe, xgmi.
The shape is consistent: on every failure path, call the service with a magic first/second argument
pair (`(6, 0xF000)` in MERIT, `(3, 0xF000)` in GCP, `(227, 1, 0xF000)` in ASD's equivalent inline
form), then return `0xFFFF0000 | code`. ASD's variant adds `if (rpc_id <= 0xFFFE)` before logging a
16-bit-truncated id, confirming the data path is a reporting channel rather than validation.
This is a coordinated rollout of structured error reporting across the DRM TAs. Whether it also
tears down protection state on failure cannot be decided from this corpus -- it needs the SYS_DRV
image, which is not here. Flagged, not claimed.
## 3. NOT FIXED in any build: unbounded HDCP2 SRM length
Not a diff finding but a *non*-diff finding, and the most actionable item here.
`asd_srm_header_check` validates an attacker-supplied System Renewability Message:
```
byte0>>4 == 8 (HDCP1 SRM): if (len - 43 > 0x13D5) return 26; // bounded 43..5120
byte0>>4 == 9 (HDCP2 SRM): if (len > 0x186) { verify(buf, len - 379); }
// lower bound ONLY -- no upper bound, and len is never
// compared against the transport length
```
Checked in **all 18 ASD builds**, from 33.0.16.144 through 33.0.8.239 and the handover reference:
the HDCP1 path is bounded `43..5120` and the HDCP2 path carries a lower bound only, in every single
one. The asymmetry was never fixed over the whole version range available.
## 4. Verified NOT security fixes
Each of these screened as a candidate and was refuted at byte level.
| apparent change | verdict |
|---|---|
| `bzero_core(frame, 0x18)` added to 24 HDCP SVC wrappers (23.0.0.52 -> .57) | **Not a fix.** `.52` already wrote all six frame words via `MOVS`/`STRD`/`STM`. The bzero replaced inline stores; it costs +6 bytes and turns leaf functions non-leaf. Source refactor. |
| `merit_cmd03` gains three bounds checks (24.0.0.57 -> .61) | **Not a change at all.** 208 bytes in both, **4 bytes differ**, deltas -1/-32/-32 -- relocations. IDA mis-read `.57`'s tail as a thunk. |
| `gcp_cmdFE0D`: `arg10 >> 30 != 0` -> `arg10 >= 0x40000000` | Identical predicate. |
| `gcp_cmd05`: `arg10 > 0x1F` -> `arg10 >= 0x20` | Identical predicate. |
| `merit_TA_InvokeCommandEntryPoint`: `len >= 0x2024` -> `len > 0x2023` | Identical predicate. |
| GCP refcount restructure in `cmd05` / `cmdFE0D` (22.0.0.60 -> .62) | Equivalent. Both release via `rpc_305` when the refcount is already 0 or decrements to 0. |
| `csu_TA_InvokeCommandEntryPoint`: min length `0x14` -> `0x24` (42.0.0.2 -> .6) | **Not a fix.** Raised to accommodate the newly added `csu_cmd03`. `cmd01` touches only offsets `0x10..0x13` and `cmd02` only `0xC`, both inside the old 20-byte bound, so no shipped build under-validated. |
| `asd_link_call_D49818`: `v6 <= 0x1E` -> `v6 <= 0xF` | **Not a bounds tightening.** It is an ASIC-ID allowlist whose window was rebased: `.195` tests the raw id against mask `dword_D11308` over 0..30, `.239` tests `id - 20` against mask `0xFEF5` over 20..35. The actual index bound, `a1 <= 1`, is unchanged. |
| `asd_dispatch` gains/loses ~48 `case` labels | Representation. Hex-Rays alternates between an explicit `switch` and `if (rpc_id - 384 <= 0x40)`; 384 = 0x180 and 0x180+0x40 = 0x1C0, exactly the range that appears to vanish. |
| `asd_dispatch` `0xFFFE` guard appears | Guards `asd_log_event(227, 1, rpc_id & 0xFFFF)`. Logging, not validation. |
| `xgmi_vsnprintf_setup__h2` constant churn | Format-specifier characters (`'p'`, `'f'`, `'e'`, `'d'`, `'u'`, `'x'`...). Artifact of pairwise comparison inside one switch. |
| `gcp_cmdFE08` allowlist 14 -> 28 entries | Functional, not a fix: new ASIC/family IDs admitted to key exchange as silicon ships. `0xd` still absent; `0xe` backfilled at 22.0.0.68. |
## Scope and what was not reachable
2,944 functions change at byte level across the 55 diffs after filtering relocation-only changes.
The large majority are display and link programming (`asd_encoder_enable_seq`,
`asd_program_link_by_signal*`, `asd_link_sub_C930`), which is hardware sequencing. Screening
prioritised command handlers, dispatchers, session and crypto handlers, SRM parsing and the
host-facing entry points.
Two limits worth stating. ASD's largest changes sit in functions whose cross-version pairing is
`FUZZY` tier, meaning the identity itself is a similarity judgement -- a bound change on a FUZZY
pair is weak evidence and none is claimed here. And `asd_dispatch` genuinely grows 778 -> 1352
bytes inside branch 33.0.0; that growth is real and has not been fully attributed.
|