aboutsummaryrefslogtreecommitdiffstats

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:

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:

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.