aboutsummaryrefslogtreecommitdiffstats

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".