aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/FINDING-SOS-RING-EMULATION-AND-3137-SKIP.md
blob: db78ae33dd69a5d4bdae282402b518315756e1f3 (plain) (blame)
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
# sOS ring dispatch: first emulation, and the 0x3137 validation skip

Static + emulator only (no hardware attached this session).

## New capability: sOS is now emulable

`pspemu` had maps for hdcp/dtm/asd/sys_drv/ras but **none for sOS** — the
component closest to the host had never been emulated, so `tee_cmd_dispatch`
(the actual x86 entry point) had only ever been read statically.

Added `memmap.sos_map()`:

* `sos2.elf` is one flat `PT_LOAD` at va=0 (size 0x18280), so IDB addresses are
  file offsets — but the **boot task really runs at VA 0x200000**
  (`sos_mmu_init` maps VA 0x200000 to `bl_entry`, and CODE2's literal pools
  reference VAs like `0x206068` = file `0x18068`). Hence `VA = FA + 0x1EE000`
  for `FA >= 0x12000`, as recorded in `ATTACK-SURFACE.md`. The map loads the
  image at 0 **and aliases CODE2 at 0x200000**.
* Cross-check that the alias is right: `tee_ring_drain`'s call site at
  VA `0x203992` disassembles as `bl #0x2027c4`, exactly the computed
  `tee_cmd_dispatch` VA. Self-consistent.
* Device MMIO mapped at `0x3000000..0x3240000` — `tee_cmd_dispatch`'s very
  first call (`bl_debug_gpio_pulse`) reads `0x32001E0`, and the shared
  `MMIO_BASE` window (`0x3010000`) does not cover it. Extents follow the
  published layout: CCP queues `0x3001000 + q*0x1000`, SMN slot registers
  `0x3220000`, Syshub slot registers `0x3230000`.
* `0xB0000..0xC0000` for the VA-region table (`0xB2000`) and PCB array
  (`0xB2408`).

## Bring-up facts recovered (each cost a vacuous "ok" until fixed)

1. **`tee_cmd_dispatch` spins on `*(ring_struct + 48) != 255`** issuing SVC 0x5A,
   so the harness must set that byte to 0xFF or the first call never returns.
2. **`bl_map_user_buffer` (FA 0x16C8A) contract**, needed or `src` stays NULL and
   the dispatcher goes straight to `tee_write_response` (measured: 86 blocks and
   *only* `tee_cmd_dispatch` reached):
   * `SVC 0x8B` (rw) / `0x6B` (ro): `R0` = descriptor `{count, len,
     pa_lo & ~0xFFF, pa_hi}`, `R1` = `&handle_byte` -> must return 0 and set the
     handle.
   * `SVC 0x6D`: `R0` = handle, `R1` = `&out_va` -> must return 0 and store the
     VA; the caller then computes `out_va = stored + (pa & 0xFFF)`.
   * `SVC 0x6C` releases.
3. **The command id is NOT in the ring frame.** `tee_cmd_dispatch` copies 0x400
   bytes of the host buffer and reads the selector from `dst+8` — the host
   struct is `buf_size@0, buf_version@4, cmd_id@8, ..., resp@864`. Writing the
   id into the 64-byte frame instead leaves every request in the default case.
   With this fixed, coverage went 92 -> **253 distinct blocks**, reaching
   `tee_load_ta` and `tee_ta_command`.
4. **`tee_write_response` writes the status through a host-supplied PA.**
   FA 0x143E4: `LDRD R1,R0,[R4,#0xC]` takes a 64-bit physical address from the
   *ring frame* at +12/+16, maps 4 bytes there, and stores the status word
   (`STR R0,[R1]` @0x143F6). Randomising those frame bytes makes every iteration
   fault on the writeback.

## FINDING: on consumer parts the SYS_DRV-side buffer validation is skipped

`bl_map_user_buffer` gates its range validation on the ring id:

```c
if ( ring == 0xFFFF || (result = sysdrv_rpc_3137(ring, pa_hi, v13, a4)) == 0 )
{
    ... SVC 0x8B / 0x6B ...     // actually map it
}
```

RPC **0x3137 is `sysdrv_validate_ring_buffer`** (dispatcher: `0x3137 ->
sysdrv_validate_ring_buffer(frame->f18[4], frame->f18[5], frame->arg[0], ...)`).

`ring == 0xFFFF` is exactly the consumer-Navi31 case — `bl_legacy_mailbox_loop`
hardcodes `MOVW R10,#0xFFFF` and is the loop that actually runs (the same
hardcoding that makes the 8-command allow-list dead code). **So every
host-supplied buffer physical address taken by `bl_map_rw_buffer` /
`bl_map_ro_buffer` on this part bypasses the SYS_DRV range validator entirely**,
including the 0x400 command buffer, the TA image in `tee_load_ta`, and the
status-writeback address above.

The remaining protection is kernel-side, inside `SVC 0x8B`, which reaches
`sos_check_phys_range`:

```c
v4 = a1 + a2;
if ( v4 < a1                                             // overflow
  || (a3 & 8) != 0 && dram_protect_cfg != 0 && ...       // CONDITIONAL on a flag
  || dword_81A0 < v4 && a1 < dword_81A0 + 0x800000       // unconditional 8MB window
  || HIWORD(HIDWORD(a1)) << 16 != 0 )                    // >48-bit addresses
    return 10;
```

Two things worth noting and **not yet pinned down**:
* the `dram_protect_cfg` branch is **conditional on `a3 & 8`**, so which flags
  `SVC 0x8B` forwards decides whether the DRAM carve-out is checked at all.
  `bl_map_user_buffer` passes `R3 = 0xFFFFFFFF` and `R2 = ring | a8<<16` to the
  SVC; the mapping from those to `sos_check_phys_range`'s `a3` has not been
  traced.
* this is a *Syshub/DRAM* mapping path (64MB slots, AXI base 0x4000000), so it
  addresses x86 DRAM rather than PSP SRAM. Writing there is not by itself a
  compromise — the host owns that memory. It matters only if the protected
  carve-outs (TMR / PSP private DRAM) can be reached, which is precisely what
  the conditional flag above determines.

This is the concrete next thing to chase, and it is reachable statically.

## Fuzzing result (negative, but now a meaningful one)

`psp_emu/harness/fuzz_sos_ring.py`: 900 iterations total (seeds 1 and 2) over
all 25 known ring command ids plus random ids, with the command buffer and ring
frame randomised.

Redzones tightened to the exact documented extents and **validated as
non-vacuous**:
* host command buffer guard at **+0x400** (the precise copy length; the initial
  +0x1000 guard would only have caught a 3KB overshoot),
* status guard at **+4** — confirmed the write happens (`resp[0:4]` goes
  `EE EE EE EE` -> `00 00 00 00`) and stops there,
* both guards proven to fire on synthetic 20-byte / 0x410-byte overruns.

Result: **no faults, no redzone violations.** sOS's own ring parsing —
frame copy, buffer mapping, command-buffer parse, `tee_load_ta` header checks,
`tee_ta_command` slot handling — is clean under this fuzzing.

Scope limit, stated plainly: the command handlers forward to SYS_DRV via
`sysdrv_rpc_*` (SVC 0xF2), stubbed to 0 here, so this exercises **sOS's parsing
layer only**. The SYS_DRV side is covered by
`harness/fuzz_sysdrv_hostreach.py`. Coverage is 253 blocks, not the whole
dispatcher.