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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
|
# Audit round: RAS / DTM / sOS-kernel / SYS_DRV ring surface
**Outcome: no new host-reachable memory-corruption or disclosure bug found.**
Everything below is a *negative* result with the evidence that produced it, so the
same ground doesn't get re-covered. Two findings are variants of already-documented
bugs, and one is a hardening gap that is not host-reachable.
Context: this round followed the first full reverse of **RAS** and **DTM**, which had
never been audited (no prior VULN doc mentions either).
--------------------------------------------------------------------------------
## Cleared: RAS TA (host-reachable, `'TA'` magic, invoke cmds 0-4)
* `ras_map_shared_buffer` (`0x402ED0`) enforces the memref slot's
`size >= 0x828`, so `ras_attach_response_buffer`'s `len - 2088` cannot
underflow. Both the request and response paths use that same validated length.
* **Hardware, sentinel-filled shm (`0x8000` B, position-dependent fill):** across
LOAD_TA + invoke cmds 0,1,2,3,4,9 the PSP writes only
`+0x04..+0x0F` (response header), `+0x410..+0x412` (state footer) and
`+0x810..+0x823` (the 20-byte `bzero`'d response area). No stale PSP data.
* **Emulator fuzz, 600 iters, vtable installed** exactly as `ras_device_register`
does for ASIC 49 (`off_4060A4/AC/B4` + the `0x405060..0x40507C` block):
0 faults, 0 redzone hits. (A first run with a NULL vtable was invalid — the
handlers returned the not-installed stubs without reaching real code.)
* Note: `psp_emu` needed `ras_map()` in `memmap.py` and a `loader.resolve`
entry. The default resolve order picked `elf/ras2.elf` (28928 B), a **different
build** from the analysed `psp_out/ras2.elf` (28672 B) and the disassembly did
not line up. Resolve order now prefers the analysed image.
## Cleared: DTM TA (host-reachable, 14 functions)
`sub_401058` gates on `paramTypes & 0xF == 7`, `params != 0`, and
`params[0].size >= 0x50`. Handlers receive `v5 + 4` where `v5` is `int *`, i.e.
**buffer+16**. Deepest read is `a1[14]` -> `buffer+76`, +4 = `buffer+80` == the
`0x50` gate exactly. Stack writes (`v16[11]` 44 B, `v17[5]` 20 B) fill their
frames exactly. Tight but in-bounds.
## Cleared: sOS kernel paths
* `tee_ta_command` (`0x16F28`) free-slot scan — **looked like an OOB write**
because Hex-Rays renders the exhaustion branch as an empty `if` body:
`while (...) { if (++v17 >= 0x30) <collapsed> }` followed by an unconditional
write at `ring_ctx + 32*v17`. Disassembly refutes it: `R6` is zeroed at
`0x16F74` and only set on the success path (`BEQ loc_16FE6`); exhaustion falls
to `0x16F8A`, unlocks, and `CBZ R6` -> `MOVW R8,#0x119` = **error 281, no write**.
* `bl_load_ta_image` (`0x15F24`) — signature verification `sysdrv_rpc_1005`
is at `0x16032`, **before** every `ta_prop_get*` call (`0x160BA` onward). No
pre-auth parser surface. `ta_prop_get_appid` copies a constant `0x10` bytes out
of the 132-byte property buffer.
* The BIOS C2P dispatcher (`bl_c2p_dispatch`) is the platform mailbox, not the
PCIe host path, and is out of scope for a discrete GPU.
## Cleared: SYS_DRV ring-forwarded RPCs
`tee_cmd_dispatch` forwards ring cmds to these opcodes. Resolution required
noting that Hex-Rays renders them as **decimal** compares and jump-table cases,
so literal `case 0x1234u:` searches give false negatives (this previously
produced a wrong "not handled" conclusion about the `0x7xxx` range).
| opcode | handler | verdict |
|---|---|---|
| 0x1006 | `sysdrv_tee_map_shared_mem` | params region-validated |
| 0x1007 | `sysdrv_tee_load_firmware` | known DoS (`size > 0xFF`), already documented |
| 0x101E | `sysdrv_get_system_info` | scalar |
| 0x109E | `sysdrv_vccc_request` | validates `len == 132` and `== 4` exactly |
| 0x310C/311A/3124/3127/3138/3139/312F/3106 | query/counter/ring helpers | small, bounded |
| 0x1024 0x1045 0x1046 0x1056 0x1059 0x105F 0x106A 0x1078 | — | **not implemented** in this build |
`sysdrv_rpc_dispatch` *does* region-validate every populated param pair via
`validate_client_range`, same as `gp_crypto_dispatch`.
**Emulator fuzz of the SYS_DRV RPC surface: 8000 iters (seeds 11, 12) — clean.**
**Emulator fuzz of the HDCP top-level entry: 5000 iters (seeds 7, 8) — only the
already-known `CANARY_SMASH`; no new signature.**
--------------------------------------------------------------------------------
## Hardening gap (NOT host-reachable)
`sysdrv_rpc_dispatch` skips param validation for one specific case:
```c
if ( param[2*v5] != 0 && (frame->opcode != 4098 || frame->f18[5] != 1 || v5 != 0) )
validate_client_range(...)
```
i.e. opcode **0x1002**, param slot 0, when `f18[5] == 1`. That param then flows
into `crypto_submit_op` as a pointer. Reachability: the only emitters of 0x1002
are ASD's `sysdrv_rpc_1002_a/b` (`0xD01918`/`0xD0197C`), both called solely from
`asd_select_timing_tables`, which supplies ASD's own static table addresses.
`f18[5]` is the stub's `R8` argument. No host-controlled pointer reaches it.
Worth reporting as defence-in-depth, not as a bug.
## Variants of already-documented bugs (not new)
* `hdcp2_rx_RepeaterAuth_ReceiverID_List` has a **second** `5*N` copy at
`memcpy(&v54, ctx+85, 5*v46)` with `v54` at `sp+0x20`. Reaches the canary at
`sp+0x210` only for `N >= 100`, and sits behind the same HMAC gate as the
documented overflow — strictly weaker than the known `v59` copy.
* Branch A (`5*N + 36 == a3`) gives `N <= 101` -> 505 bytes into the 504-byte
`rx_ids`: 1 byte past. Same family as the documented 11-byte branch-B OOB.
--------------------------------------------------------------------------------
## Where to look next
1. `crypto_submit_op` / `ecc_submit_op` / `hmac_sha256` (opcodes 0x1002-0x1004)
handler bodies — reached from ASD, never opened.
2. `sysdrv_tee_load_firmware` beyond the known `size > 0xFF` hang: the
`fw_type == 49/50/85` branches skip the `<= 0xFF` length gate entirely.
3. `asd_srm_buf` staleness: whether any sequence leaves
`asd_srm_stored_len` larger than the bytes actually written, which
`asd_srm_read` (0x300, reachable via HDCP cmd 0x13) would then copy out.
--------------------------------------------------------------------------------
## Cleared: the SRM read channel (HDCP cmd 0x13 -> ASD 0x300)
**Methodology correction — this invalidated an earlier sweep.** An HDCP command
sweep run *without* `LOAD_ASD` makes every `disp_*` RPC fail early, so the TA
returns a status code and touches nothing. Any leak/OOB sweep over HDCP commands
**must load ASD first**. Re-running the full sweep with ASD loaded changed the
result for cmd 0x13 from "4 bytes of status" to "60 bytes including 48 bytes of
ASD's global SRM buffer".
The channel is real and wider than it first looks. `hdcp_get_srm_status`
(`0x4034E0`) calls:
```
asd_srm_read(a1 = out, a2 = 0x1400, a3 = out+4, a4 = out+8)
```
so ASD will `memcpy(out+8, asd_srm_buf, asd_srm_stored_len)` — i.e. **up to 5120
bytes of ASD DATA can be copied into the host buffer**, with the length taken
solely from `asd_srm_stored_len`.
It is nevertheless **not** a disclosure, for two independently verified reasons:
1. **Hardware read-back matches the firmware image byte-for-byte.** cmd 0x13
returned `version=1`, `stored_len=0x30`, and 48 bytes
`800000010000002bd2489e49d057ae315b1abce00e4f6b92a6ba033b98cced4a978f5dd227292519`
— identical to `asd_srm_buf` (`0xD1F038`) in the image, whose `stored_len`
(`0xD20438`) also initialises to `0x30`. That is the default type-8 SRM
shipped in ASD's DATA (`byte0>>4 == 8`, 24-bit length field = 43 = the
minimum `asd_srm_header_check` accepts). Public firmware content.
2. **`asd_srm_stored_len` has exactly two writers**, both signature-gated:
`asd_srm_op_301` (`0xD13318`) sets it to the *transport* length and writes
that many bytes; `asd_srm_store` (`0xD134A6`) sets it to the header-derived
length and copies from a stack buffer that was `bzero`'d over its full
0x1400 bytes first — so bytes past the transport length read back as zeros,
not stale memory. Verified by enumerating every xref and every literal-pool
reference to `0xD20438`.
So the length can't be inflated past the bytes actually written, and the
"extra" 5072 bytes of the channel are unreachable.
**If a future build ever gains a third writer of `asd_srm_stored_len`, or one
that sets it without filling the buffer, this becomes a 5 KB ASD DATA leak
reachable from the host with a single INVOKE_CMD and no session.** Worth a
regression check across versions.
--------------------------------------------------------------------------------
## Cleared: the 21 unbounded variable-size copies in SYS_DRV
`VERSION-AUDIT-STATUS.md` established that SYS_DRV has **21 unbounded var-size
copies**, stable across all versions, and explicitly left them uninspected
("Not covered by the automated pass ... composite-guard integer-overflow fixes").
They are now enumerated (from `vdiff/sys_drv_0.49.0.54.json`) and individually
audited. **All 21 are safe.**
Seven are host-reachable (callers `gp_crypto_dispatch` — used by every TA — or
`sysdrv_rpc_dispatch`):
| site | bound | verdict |
|---|---|---|
| `attr_pack_4` 0xE0F3B4 | `CMP R7,#4 / BHI` **unsigned** -> `12*a4 <= 48` into a 48 B buffer | safe |
| `attr_unpack_4` 0xE0F4F0 | `CMP R6,#4 / BHI` unsigned, same | safe |
| `gp_asym_op_with_attrs` 0xE1DAF8 | `CMP #1 / BHI` unsigned | safe |
| `pk_verify_signature` 0xE08EBC | length = `keybits>>3` | safe |
| `sysdrv_derive_key_a` 0xE0B090 | fixed 0x113-stride record | safe |
| `read_otp_block` 0xE15C70 | OTP block count | safe |
| `gp_operation_copy_state` 0xE1CC60 | `CMP R0,R1 / BHI` before the clear | safe |
The remaining 14 are internal (crypto/record/slot helpers) with constant or
structurally-bounded lengths.
**Why the scanner flagged them:** the guard is on the *operand* (`a4 <= 4`), not
on the size *expression* (`12*a4`). Any future scan of this class must resolve the
guard through the expression, or it will keep reporting these same 21 sites.
`attr_pack_4`/`attr_unpack_4` looked like textbook signed-compare bugs in the
decompiler (`if (a4 <= 4) memcpy(dst, src, 12*a4)` into a 48-byte stack buffer) —
a negative `a4` would have given a ~4 GB copy. The instruction is `BHI`
(**unsigned**), so it is not. Checking at instruction level rather than trusting
Hex-Rays' `<=` was what settled it.
### Integer-overflow lead in `gp_object_enumerate` (0xE1D938) — NOT reachable
The allocation size and the copy length come from different expressions:
```c
v10 = srcobj->size + 112; // srcobj from a caller-supplied handle
v13 = v10 + v5; // v5 = caller buf size, gated <= 0x2000
v17 = heap_alloc(v13, ...);
memcpy(v18 + 28, srcobj_body + 112, srcobj->size); // full un-wrapped size
```
If `srcobj->size >= 2^32 - 8304` the adds wrap, `heap_alloc` returns a small
block and the `memcpy` overflows the SYS_DRV heap. Chased to the end:
* `gp_object_truncate` caps the new size at `0x2000` and writes the **data**
length at +104, not +4.
* `gp_object_write_data` has an explicit `__CFADD__` carry check *and* a
`> 0x2000` cap. Correct code.
* The `+4` field is written **only** by `attr_marshal` (0xE18EAC):
`v5 <= obj->maxbits>>3` **and** `validate_client_range(ptr, v5, ...)`, which
rejects any length `> 0x10000000`.
So `srcobj->size <= 0x10000000`; `v10` and `v13` cannot wrap. The pattern is real
but both addends are bounded three orders of magnitude below the wrap point.
Worth re-checking if a future build ever lets an object's attribute size be set
without a `validate_client_range` on the same value.
|