Audit: are any sOS->SYS_DRV forwards a genuine TOCTOU?
Result: NO. Closed negative. Every live host pointer sOS passes to SYS_DRV is a response-write destination, never a parsed input. All request parsing is done against sOS's private snapshot. This closes the double-fetch hunt and supersedes two retracted findings.
Method: for each sysdrv_rpc_* call in tee_cmd_dispatch (0x147C4) that
mentions src, determine (a) whether the stub actually sends the pointer to
SYS_DRV, and (b) if sent, whether the handler dereferences it for reading.
This is the check that was missing from the earlier, wrong analyses.
The structural fact
tee_cmd_dispatch does bl_memcpy(dst, src, 0x400) and then passes slot
as the request. dst is at [sp+18h], slot at [sp+34h], so
slot == dst + 28 — it points into the snapshot, at the offset of the
cmd union in struct psp_gfx_cmd_resp (cmd@28). Handlers dereference the
first rpc parameter, which is slot.
The src+N pointers exist so results land directly in the host's buffer without
a second copy.
Per-call results
| RPC | src arg |
sent to SYS_DRV? | role | verdict |
|---|---|---|---|---|
0x3124 |
src+32 (a1) |
no | stub does STR R1,[R4] after SVC |
OUTPUT |
0x1007 |
src+864 (a1) |
no (stub packs slot) |
handler derefs R6=slot |
OUTPUT/unused |
0x310C |
src+864 (a2) |
yes, with len 0x400 |
handler only STRs to [R2,#0x38], [R2,#0x40] |
OUTPUT |
0x109E |
src+928 (a2) |
yes (R9) |
R9 never appears in a load |
OUTPUT/unused |
0x3127 |
src+932,src+928 |
no args sent at all | STR R2,[R5]; STR R1,[R4] |
OUTPUT |
0x3138 |
src+924 (a0) |
no | result stored to *R4 |
OUTPUT |
0x1024 |
src+864 (a1) |
yes | unimplemented in this build | n/a |
0x106A |
src+864 (a1) |
yes | unimplemented in this build | n/a |
0x1078 |
src+924 (a0) |
no | STR R0,[R4] after SVC |
OUTPUT |
Evidence for the "OUTPUT" calls is the post-SVC store in the stub, e.g.
sysdrv_rpc_1078 @0x12BC0:
0x12BDA STR R5,[SP,#var_44] ; only slot[0] is sent
0x12BE6 SVC 0xF2
0x12BEA LDR R0,[SP,#var_48]
0x12BEC STR R0,[R4] ; R4 = src+924 <- WRITE-BACK
sysdrv_rpc_3127 @0x12798:
(no parameters stored into the frame at all)
0x127BA SVC 0xF2
0x127BE LDRD.W R1,R2,[SP,#var_48]
0x127C2 STR R2,[R5] ; R5 = src+932
0x127C4 STR R1,[R4] ; R4 = src+928 <- WRITE-BACK
0x310C — the last open item, resolved
sysdrv_tee_query_12 (0xE1BAB0) read [R1,#0x168] three times, which looked
like a triple-fetch. It is not host memory:
0xE1BAC6 ADD.W R4, R4, R1,LSL#2 ; uses the INCOMING R1 (index, bounded <0xF)
0xE1BACA LDR R1, [R7] ; R7 = off_E257A0 -- R1 RELOADED from a GLOBAL
0xE1BAF0 LDR.W R1, [R1,#0x168] ; ...so this derefs the global, not the host
The incoming index is bounds-checked (CMP R1,#0xF; BCC). The host pointer is
R2, which appears only as a store target (STR R0,[R2,#0x38],
STR R4,[R2,#0x40]) — both inside the 1024-byte buffer (src+920, src+928).
Conclusion
sOS consistently applies snapshot-then-parse, and uses live host pointers only for writing results back. There is no double-fetch, no TOCTOU, and no attacker-controllable window in any of these forwarders.
The two earlier findings were wrong for the same reason: they observed a live pointer in an argument list and assumed it was the request. Always confirm which argument the handler dereferences, and in which direction.
What remains genuinely open elsewhere
0x1024/0x106Aare unimplemented here but are sent(src+864, 0x400)by their stubs. If a firmware version implements them, they would receive a live pointer with a length that overruns the 1024-byte buffer (864+1024 = 1888). Worth checking on other PSP builds.- The allow-list bypass (
ring == 0xFFFF, seeATTACK-SURFACE.mdaddendum) stands and is unrelated to this audit.
