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'ssize >= 0x828, soras_attach_response_buffer'slen - 2088cannot underflow. Both the request and response paths use that same validated length.- Hardware, sentinel-filled shm (
0x8000B, 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-bytebzero'd response area). No stale PSP data. - Emulator fuzz, 600 iters, vtable installed exactly as
ras_device_registerdoes for ASIC 49 (off_4060A4/AC/B4+ the0x405060..0x40507Cblock): 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_emuneededras_map()inmemmap.pyand aloader.resolveentry. The default resolve order pickedelf/ras2.elf(28928 B), a different build from the analysedpsp_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 emptyifbody:while (...) { if (++v17 >= 0x30) <collapsed> }followed by an unconditional write atring_ctx + 32*v17. Disassembly refutes it:R6is zeroed at0x16F74and only set on the success path (BEQ loc_16FE6); exhaustion falls to0x16F8A, unlocks, andCBZ R6->MOVW R8,#0x119= error 281, no write.bl_load_ta_image(0x15F24) — signature verificationsysdrv_rpc_1005is at0x16032, before everyta_prop_get*call (0x160BAonward). No pre-auth parser surface.ta_prop_get_appidcopies a constant0x10bytes 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:
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_Listhas a second5*Ncopy atmemcpy(&v54, ctx+85, 5*v46)withv54atsp+0x20. Reaches the canary atsp+0x210only forN >= 100, and sits behind the same HMAC gate as the documented overflow — strictly weaker than the knownv59copy.- Branch A (
5*N + 36 == a3) givesN <= 101-> 505 bytes into the 504-byterx_ids: 1 byte past. Same family as the documented 11-byte branch-B OOB.
Where to look next
crypto_submit_op/ecc_submit_op/hmac_sha256(opcodes 0x1002-0x1004) handler bodies — reached from ASD, never opened.sysdrv_tee_load_firmwarebeyond the knownsize > 0xFFhang: thefw_type == 49/50/85branches skip the<= 0xFFlength gate entirely.asd_srm_bufstaleness: whether any sequence leavesasd_srm_stored_lenlarger than the bytes actually written, whichasd_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:
- Hardware read-back matches the firmware image byte-for-byte. cmd 0x13
returned
version=1,stored_len=0x30, and 48 bytes800000010000002bd2489e49d057ae315b1abce00e4f6b92a6ba033b98cced4a978f5dd227292519— identical toasd_srm_buf(0xD1F038) in the image, whosestored_len(0xD20438) also initialises to0x30. That is the default type-8 SRM shipped in ASD's DATA (byte0>>4 == 8, 24-bit length field = 43 = the minimumasd_srm_header_checkaccepts). Public firmware content. asd_srm_stored_lenhas 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 wasbzero'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 to0xD20438.
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:
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_truncatecaps the new size at0x2000and writes the data length at +104, not +4.gp_object_write_datahas an explicit__CFADD__carry check and a> 0x2000cap. Correct code.- The
+4field is written only byattr_marshal(0xE18EAC):v5 <= obj->maxbits>>3andvalidate_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.
