aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/FINDING-ROUTEC-WINDOW-IDENTIFIED.md
blob: e126714ade720c1a2f0eaaf7710df19f59bded1c (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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# Route C's write window identified: PSP-internal physical space, deliberately unguarded

`VULN-LOADIPFW-UNSIGNED-REGWRITE.md` has carried "capability real, **impact
unestablished**" because nobody could say what `[0x21500, 0x21B78]` actually is.
A VCN reading was proposed and refuted on hardware
(`RERATE-ROUTEC-TARGET-MAY-BE-VCN.md`). This settles it from the firmware.

## The mechanism

Route C's writes go through `SVC 0x7C`, which resolves addresses via sOS's
`vawin_map_fixed` (FA 0x3D8C). That function splits its address argument:

    r1 = (addr >> 16) & 0xFFF0     ; programmed into a VA-window register
    r5 =  addr & 0xFFFFF           ; 20-bit offset within a 1 MB granule

So the address is a **PSP-internal physical address**, mapped a megabyte at a
time into the caller's VA — not a host SMN address, which is why every attempt
to read `0x21xxx` over the host SMN aperture mirrored.

## The access control, and what it does not cover

`vawin_map_fixed` gates every request through `sos_check_va_window_request`
(FA 0x6D20), which rejects exactly two ranges plus an overflow case. Measured by
calling it directly in the emulator:

| request | result |
| --- | --- |
| **Route C window `0x21500`** | **0 — allowed** |
| **Route C window `0x21B78`** | **0 — allowed** |
| protected range 1, `0x3700000` | `0xa` rejected |
| protected range 1, `0x3780000` | `0xa` rejected |
| protected range 2, `0x3900000` | `0xa` rejected |
| just below range 1, `0x36FF000` | 0 allowed |
| just above range 1, `0x3800000` | 0 allowed |
| `addr + size` overflow | `0xa` rejected |

The controls matter: the protection is demonstrably real and its boundaries are
exactly `[0x3700000, 0x3800000)` and `[0x3900000, 0x3A00000)`, and the overflow
guard fires. It simply **does not cover `0x21xxx`**.

## What the window contains

At least the bootloader's own boot-stage registers: `bl_set_boot_stage` writes
`0x215C0` and `0x2161C` through the same mechanism (recorded in
`VULN-LOADIPFW-UNSIGNED-REGWRITE.md`). So the window is PSP-internal state that
the bootloader itself uses, sitting outside the VA-window protection.

## Status

Upgraded from *unidentified* to **PSP-internal physical space, unprotected by
the VA-window access control, containing bootloader boot-stage state**.

That is a better answer than the tree has had, and it removes the VCN
distraction for good. It does **not** by itself establish impact: what the other
registers in `0x21500..0x21B78` do is still unknown, and the "nothing reads
them" scan that originally deflated this finding covered only the images we
hold. The right next question is narrow and concrete — enumerate which addresses
in that 1656-byte window the firmware itself touches, now that we know the
window is PSP-internal rather than host-visible.

---

# What the firmware actually touches in that window: 3 of 415 dwords

With the window identified as PSP-internal physical space, the follow-up is
narrow: which of its 415 dwords does firmware we hold reference at all?

Scanned sOS and SYS_DRV for 32-bit literals and `MOVW`/`MOVT` immediates landing
in `[0x21500, 0x21B78]`:

| address | referenced from | what it is |
| --- | --- | --- |
| `0x215C0` | **`bl_main+0x12` and `+0x1a`** (sOS bootloader) | the boot-stage register |
| `0x21B40` | literal at `0xE100AC` in SYS_DRV | Route C handler's own fixup constant |
| `0x21B50` | literal at `0xE100A8` in SYS_DRV | Route C handler's own fixup constant |

**3 of 415.** And two of the three are the handler's own special cases —
`VULN-LOADIPFW-UNSIGNED-REGWRITE.md` already records `0x21B40/44/50/54` as the
addresses that "get firmware-chosen values" rather than the host's. So the only
address in the window that firmware genuinely *uses* is `0x215C0`, and its user
is the **bootloader's `bl_main`**, which runs before SYS_DRV exists.

## A control correction

The first version of this scan used `0x2161C` as a positive control, because
`VULN-LOADIPFW-UNSIGNED-REGWRITE.md` records `bl_set_boot_stage` writing it. The
scan did not find it — and the reason is **not** a broken scan: in *our* build
`bl_set_boot_stage` is 28 bytes at FA 0x159E0 and references neither address.
That description came from a different firmware version, the same
version-mismatch trap that made the v53 map unusable for the TSU subsystem.

The working cross-check is `0x21B40`/`0x21B50`: independently documented as the
handler's special cases, and found by the scan.

## Impact, now bounded

Route C writes up to 64 attacker-chosen 32-bit values into PSP-internal register
space that the VA-window access control does not protect. But of the 1656 bytes
reachable, firmware we possess references exactly one address meaningfully, and
its consumer is the bootloader at boot time — before the code that would be
attacked is running.

So the original "capability real, impact unestablished" becomes **capability
real, impact bounded-low on the evidence available** — with the standing caveat
that consumers in hardware, or in code we do not hold, cannot be excluded by a
scan over the images we have. That caveat is why this is "bounded-low" and not
"none".

---

# CORRECTION: "3 of 415" is NOT established — the detector has a demonstrated blind spot

Prompted by the question "how did you establish there's only one consumer?".
Re-examining the method rather than the result, the answer is: **I did not
establish it.**

## What the scan could see

Only addresses appearing *whole*: 32-bit literals in constant pools, and
`MOVW`/`MOVT` immediate pairs. Over two images (sOS, SYS_DRV).

## What it structurally cannot see

Register blocks are normally accessed as **base + immediate offset**. A
sequence holding `0x21000` in a register and doing `ldr r0, [r1, #0x500]` never
materialises `0x21500` anywhere in the image, so the scan is blind to it — and
that is the *expected* access pattern for the very thing this window looks like.

Tested for it. Scanning the band `[window_lo - 0x1000, window_hi]` for values
usable as a base with a 12-bit offset finds **six candidates**:

    sOS       base 0x021000  (covers the ENTIRE window)   at FA 0x168ca
    SYS_DRV   base 0x020d04                               at 0xE0EC5C
    SYS_DRV   base 0x020d1c                               at 0xE104B4, 0xE1657C
    SYS_DRV   base 0x020d5c                               at 0xE180F8
    SYS_DRV   base 0x020e30                               at 0xE18288
    SYS_DRV   base 0x020f49                               at 0xE01DEC

An attempt to resolve the sOS one did not decode as code at the offset the scan
assumed, which additionally means **the scan's address model for sOS is itself
unverified**.

## Further weaknesses, stated plainly

* **The control set was effectively N=1.** Of two positive controls, `0x2161C`
  failed; the build-mismatch explanation for it is evidence-backed but post-hoc.
  A detector validated by one surviving control is thin — and this project's own
  standing lesson is that a scan is worthless until it can find a known positive.
* **Coverage was 2 images**, not the boot ROM, earlier bootloader stages, or any
  TA.

## Status of the impact rating

"Firmware references 3 of 415 dwords" should be read as **"3 whole-address
references in 2 images, with 6 unresolved indexable bases and an unverified
address model"** — not as a census of consumers.

The bounded-low rating rested on that count, so it is **downgraded to
provisional** until either the six candidates are resolved or the scan is
rebuilt to follow base+offset addressing. The capability itself is unaffected;
it remains confirmed on hardware.

## Dynamic attempt: the right instrument, and why its first answer is void

The static scan's blind spot (base+offset addressing) disappears if the question
is asked at run time, so `psp_emu/harness/routec_window_watch.py` traps every
read and write in `[0x21500, 0x21B78]` across a full boot, three TA loads and six
ring commands.

It reports **zero reads and zero writes**, and after fixing two broken controls
that result now **stands** (for the paths exercised).

The harness carries two mandatory controls, and getting them right took two
attempts — both failures worth recording, because they pointed in opposite
directions.

1. **Liveness** — the same hook observed 15,438,193 reads in a known-busy range
   (sOS globals) from 197 distinct PCs. The hook works.
2. **Spanning coverage** — firmware traffic must be seen in the same mapped
   region both **below and above** the window, which proves the watcher's
   address-space assumption holds across it. Final result: **20 pages below,
   1 above (highest `0x3d000`)**.

### Broken control #1: it retracted a correct result

The first version of control 2 injected an `ldr r1,[r0]` with `r0 = 0x21500` and
checked the hook fired. Post-boot it did not, and I concluded the MMU had made
the watcher blind and declared the zero void.

That was wrong. Instrumenting the probe with an unfiltered hook showed **no
memory access at all** post-boot, not a translated one — the injected
instruction never executed, because once the MMU is configured there is no
mapping for the scratch page. The control was measuring code injection, not
observability.

**A control that fails for a reason unrelated to the claim is worse than no
control**: it retracts a correct result and looks rigorous doing it.

### Broken control #2: the hook range again

The replacement control looked for traffic above the window and found none —
because the hook was registered with `end=0x22000`, so addresses above the
window could never be delivered. The same hook-range truncation that had already
broken two traces earlier in the session.

### What the corrected run shows

With the hook spanning the full region, firmware reads land on 21 pages from
`0x0` to `0x3d000`, including above the window — so a read at `0x21500` would
have been seen exactly as those are. **It never happens.**

This is strictly better evidence than the static scan, and it is immune to the
base+offset blind spot, because at run time the address exists whether or not it
ever appeared whole in the image. The six unresolved indexable bases are
therefore answered on these paths: none of them is used to reach the window.

### Residual limits, unchanged

* only **exercised paths** are covered (boot, 3 TA loads, 6 ring commands);
* the **boot ROM is not emulated**, and `0x215C0` is a bootloader-era register;
* this emulator backs the window with **plain RAM**, so a *hardware* consumer is
  invisible here by construction — which remains the main reason the rating is
  not "none".