# 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) }` 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.