Boot the VSH on every firmware, 1.50 through 6.61

Backwards from 5.01, one release at a time, against all 39 versions that ship
on a disc plus the download-only 6.61. Nothing here is an offset - it's almost
entirely Sony renumbering the kernel *_driver NIDs, which sends an import we
mean to HLE into the real firmware module instead.

Four more NIDs each for sceRtc_driver/sceRtcSetAlarmTick and
sceHprm_driver/sceHprmReadLatch, covering 1.50 up. The rtc one is what parked
every thread on a SceSysconSync semaphore; the hprm one runs once a frame, so
unresolved it was most of the boot log. Also sceImposeGetParam/sceImposeChanges
(1.50 - 2.xx) and sceKernelLoadModuleVSH (1.x, which is how the shell loads its
own plugins - unresolved it got module id 0 and StartModule failed).

sceRtcIsAlarmed had to be implemented too; it returns 0, as in JPCSP. As a null
entry it returned LIBRARY_NOT_YET_LINKED, and the 3.0x-3.5x VSH read that as
"ask the hardware instead" and went back to blocking on syscon.

Two structural findings:

- Up to 4.05, scePaf's heap allocator is a separate heaparea1.prx that paf
  imports as scePafHeaparea. Load it when it's there. Its pool pointer needs
  the same pre-fill paf's does, at gp - 0x7FCC rather than gp - 0x7E88.
- 1.50's vshmain.prx declares no module attributes at all - PSP_MODULE_VSH_MODE
  only appears from 1.52 - so the whole VSH bootstrap was being skipped. Accept
  the module name too.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-09-09 14:43:39 -06:00
1 parent 480c5e44e2
commit 63eb3d5b21
6 files changed
+178 -80

No files matched your search

+89 -53
View File
@@ -68,71 +68,100 @@ changing repeats byte-identically, and a live menu does not.
## Which firmware versions work
**5.01 through 6.61**, all of them. `FirmwareVersionSupportsVSH()`
(`Core/Util/PSARUnpack.cpp`) is the gate the UI uses.
**All of them - 1.50 through 6.61.** `FirmwareVersionSupportsVSH()` (`Core/Util/PSARUnpack.cpp`)
is the gate the UI uses, and it now only asks whether a firmware is installed at all.
Checked one release at a time against every version that ships on a UMD - 5.01, 5.02, 5.03, 5.50,
5.55, 6.00, 6.10, 6.20, 6.30, 6.31, 6.35, 6.37, 6.39, 6.60 - plus the download-only 6.61. Each
reaches an interactive XMB; 6.00 was confirmed by rendering a GE dump off the running shell, which
is the same picture 6.60 gives.
Checked one release at a time against every one of the 39 versions that ships on a UMD - 1.50,
1.52, 2.00, 2.50, 2.60, 2.71, 2.80, 2.81, 2.82, 3.03, 3.11, 3.30, 3.40, 3.50, 3.51, 3.52, 3.71,
3.72, 3.73, 3.80, 3.90, 3.95, 3.96, 4.01, 4.05, 5.01, 5.02, 5.03, 5.50, 5.55, 6.00, 6.10, 6.20,
6.30, 6.31, 6.35, 6.37, 6.39, 6.60 - plus the download-only 6.61. 1.50 and 6.00 were also
confirmed by rendering a GE dump off the running shell; both give the same interactive XMB 6.60
does.
**4.05 and below** still die on a `SIGSEGV at 00000000` inside `vsh_module` itself. Different
problem, not an offset to move.
### Sony renumbered the kernel NIDs, and that is what blocked everything below 6.60
5.55 was briefly the odd one out: none of its `flash0:/kd` decrypted, because its modules are
tagged `0x4C941AF0`/`0x4C941BF0` and those two were the only entries of JPCSP's PRX tag table we
were missing. The shell came up anyway - vshmain and paf are user modules - but with not a single
driver behind it. Both keys are in `PrxDecrypter.cpp` now.
Not offsets - NIDs. A function PPSSPP HLEs under its 6.6x `*_driver` NID is unrecognized on an
older build, so the import resolves to the **real firmware module** instead, and the real module
goes places the emulator can't follow.
### What it took to get below 6.60
| Function | 6.60/6.61 | 6.31-6.39 | 6.00-6.20 | 5.03-5.55 | 3.95-4.05 | 3.72-3.90 | 3.71 | 1.50-3.51 |
|---|---|---|---|---|---|---|---|---|
| `sceRtc_driver` `sceRtcSetAlarmTick` | `E09880CF` | `54B9C589` | `68AED59A` | `ADAF231F` | `55AC1C23` | `827BCB3F` | `329E8E3A` | `7D1FBED3` |
| `sceHprm_driver` `sceHprmReadLatch` | `E9B776BE` | `A3A87975` | `5FC5E53B` | `605DEA7A` | `A6E8D4F0` | `8C728076` | `F0AA1FB9` | `40D2F9F0` |
Two things, both the same shape: **Sony renumbered the kernel `*_driver` NIDs between firmware
versions**, so a function PPSSPP HLEs under the 6.6x NID is a stranger on an older build - and
the import then lands in the *real* firmware module instead, which is where the trouble starts.
The rtc one is the interesting failure. Without the HLE, the VSH's alarm call ran the real
`rtc.prx`, which called on into `syscon.prx` and blocked forever on a `SceSysconSync` semaphore.
The symptom was a boot where every thread was parked and `idle0` was running, and the tell was a
**fourth** `SceSysconSync` waiter that a healthy boot doesn't have (there are three, one each for
sceSYSCON_Driver, sceRTC_Service and SceWlanMac - that's their normal idle state).
`hle.backtrace thread=<SCE_VSH_GRAPHICS>` named the whole chain: vsh_module -> vshbridge -> rtc ->
syscon -> wait. The hprm one is only a performance bug, but a loud one: the VSH reads the latch
once a frame, so an older firmware's 12-second boot logged ~20000 lines of the same import.
- **`sceRtc_driver` `sceRtcSetAlarmTick`** is `0xE09880CF` on 6.60/6.61, `0x54B9C589` on
6.31-6.39, `0x68AED59A` on 6.00-6.20 and `0xADAF231F` on 5.03-5.55. Without the HLE the VSH's
alarm call ran the real `rtc.prx`, which called on into `syscon.prx` and blocked forever on a
`SceSysconSync` semaphore. That was the whole "stalls in sceVshBridge_Driver" symptom: every
thread parked, `idle0` running, and a fourth `SceSysconSync` waiter that a healthy boot doesn't
have.
- **`sceHprm_driver` `sceHprmReadLatch`** is `0xE9B776BE` on 6.60/6.61, `0xA3A87975` on 6.31-6.39,
`0x5FC5E53B` on 6.00-6.20 and `0x605DEA7A` on 5.03-5.55. The VSH calls it once a frame, so
before this an older firmware's boot log was ~20000 lines of one unresolved import.
Two more of the same shape, for 1.50-2.xx: `sceImpose_driver` exports `sceImposeGetParam` as
`0x531C9778` and `sceImposeChanges` as `0xB415FC59` there, with no user-mode alias. Changes runs
once a frame too. And `ModuleMgrForKernel` numbers `sceKernelLoadModuleVSH` `0xA4370E7C` on 1.x -
that's how the VSH loads its own plugins, so unresolved it got module id 0 back and the
`sceKernelStartModule` after it failed, exactly the way `0xD5DDAB1F` did on 6.61 before it was
implemented.
**How to map a NID between versions.** Disassemble the same module from both firmwares with
`--re-module` and match by address: the builds move code around a little, but each of these
functions is also exported from the plain user-mode library under a NID that never changed
(`sceRtc/0x7D1FBED3`, `sceHprm/0x40D2F9F0`), so anchor on that export's address in each build and
read off the `*_driver` NID sitting at the same place. That is mechanical, and it is how all eight
NIDs above were found.
`--re-module` and match by address. Don't compare raw addresses - the builds move code - anchor on
the plain user-mode export whose NID never changed (`sceRtc/0x7D1FBED3` for SetAlarmTick,
`sceHprm/0x40D2F9F0` for ReadLatch) and read off the `*_driver` NID at the same address. Below
3.95 `sceRtcSetAlarmTick` isn't in the user-mode library at all, so anchor on a neighbour instead:
it is the driver export immediately below `sceRtcIsAlarmed`. Where even that fails, match the
function body - that is how the two impose calls and the 1.x LoadModuleVSH were identified.
Expect more of these as other paths get exercised - `sceVshBridge`, `sceDisplay_driver`,
`sceImpose_driver` and `sceCtrl_driver` all have version-specific NIDs that are currently
unresolved on every version, 6.61 included, and the boot survives them.
`sceRtcIsAlarmed` also had to be implemented (it returns 0, as in JPCSP). Left as a null entry it
returned `SCE_KERNEL_ERROR_LIBRARY_NOT_YET_LINKED`, and the 3.0x-3.5x VSH read that as "ask the
hardware instead" and went back to blocking on syscon.
### Making the two module patches version-safe
### The scePaf heap pool, and where it lives
Both patches used to be hardcoded offsets from the module base, applied to any module of the right
name. That is fine for the version they were derived from and quietly wrong for every other one.
scePaf's allocator wants a pool base already written into one of its BSS slots. The module that
owns the allocator does fill that slot in itself, from its own module_start - but that start
thread hasn't been scheduled yet when vshmain makes its first allocation, so the pointer is still
null and the shell writes through it. Real hardware's kernel bootstrap starts these modules one at
a time and waits; `LoadAndStartVshKernelModules()` can't, so it pre-fills the slot instead.
- **The scePaf heap arena slot is now found via gp**, not the module base. Its base-relative offset
moves with every build (0x18CCD8 on 6.00 through 0x18D728 on 6.60/6.61), but it sits at
`gp - 0x7E88` in all of them - checked against 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61. The
slot is the second of the two pool pointers scePaf's own init fills in with
`sceKernelTryAllocateFpl`, which is how to re-find it in a build not listed here: disassemble
`paf.prx`, find the one function that calls `sceKernelTotalMemSize`, and read the address handed
to the second `sceKernelTryAllocateFpl` as `a1`.
- **The vsh_module alarm-category patch now checks what it is overwriting.** That offset is in
rodata, so there is no gp anchor for it; instead the patch only fires when the word at
`+0x455C4` is the `0x3F666666` it was derived from. On 6.39 that word is a different float, and
on 6.20/6.00 it is ASCII string data (`5f746c75`, `776f6461`) - the old unconditional write was
corrupting a string table on those. Only 6.60/6.61 need the patch at all; every older version
reaches the XMB without it.
Which module owns it moved. **Up to 4.05 the allocator is a separate `flash0:/vsh/module/heaparea1.prx`**,
which paf imports as `scePafHeaparea` and cannot allocate a byte without; from 5.01 it is compiled
into paf.prx and heaparea1 is gone. Loading heaparea1 when it's present was the missing piece for
every 3.x and 4.x version - without it `scePafHeaparea_ACCE25B2` was an unresolved import, paf
built a heap out of an uninitialized stack pair, and vshmain died storing the null the allocator
handed back.
Getting the paf patch right on its own turned an immediate `SIGSEGV at 0000000c` inside
`scePaf_Module` (a null pool base plus a field offset) into a clean stall for every 6.0x-6.3x
version, which is what moved the blocker on to the rtc NID above.
Either way the slot is the second of the two pool pointers that module's init fills in with
`sceKernelTryAllocateFpl`, and either way its offset from the module base moves with every build
while its offset from gp does not:
| Module | gp - slot | Checked against |
|---|---|---|
| `paf.prx` | `0x7E88` | 6.00, 6.20, 6.31, 6.37, 6.39, 6.60, 6.61 (base-relative 0x18CCD8..0x18D728) |
| `heaparea1.prx` | `0x7FCC` | 3.95, 4.05 |
To re-find it in a build not listed: disassemble the module, find the one function that calls
`sceKernelTotalMemSize`, and read the address handed to the **second** of its two
`sceKernelTryAllocateFpl` calls as `a1`. The shape is identical in both modules - TotalMemSize, a
`> 0x2400000` test picking 0xA00000/0xC50000 pool sizes over the compiled-in defaults, then two
`sceKernelCreateFpl` + `sceKernelTryAllocateFpl` pairs writing to adjacent slots.
### 1.50 declares no module attributes
`g_runningVSH` was set from `PSP_MODULE_VSH_MODE` in the module's attribute word. 1.50's
vshmain.prx has attribute `0000` - Sony only started setting the flag in 1.52 - so the entire VSH
bootstrap was skipped and the shell ran with none of its support modules loaded. The check also
accepts the module *name* `vsh_module` now.
### The vsh_module alarm-category patch only 6.6x needs
That offset is in rodata, so there is no gp anchor for it; instead the patch only fires when the
word at `+0x455C4` is the `0x3F666666` it was derived from. On 6.39 that word is a different
float, and on 6.20/6.00 it is ASCII string data (`5f746c75`, `776f6461`) - the old unconditional
write was corrupting a string table on those. Every version below 6.60 reaches the XMB without the
patch, which is itself a hint that whatever precondition makes that scan safe on real hardware was
lost somewhere between 6.39 and 6.60.
### Kernel modules with per-model builds
@@ -140,7 +169,14 @@ version, which is what moved the blocker on to the rtc NID above.
`wlanfirm_01g.prx` by name. A firmware unpacked for one model ships only that model's build, and
PPSSPP's own updater unpack defaults to 02g, so all three failed to load. `ResolveVshModelModule()`
now substitutes the emulated model's suffix when that file exists, falling back to `_01g` for a
dump unpacked with model `any` (which has every model's).
dump unpacked with model `any` (which has every model's). Firmwares older than about 3.60 predate
the PSP-2000 and have no per-model split at all, so they are unaffected.
### 5.55 needed two PRX keys
Its `flash0:/kd` modules are tagged `0x4C941AF0`/`0x4C941BF0`, and those two were the only entries
of JPCSP's PRX tag table PPSSPP was missing. The shell came up anyway - vshmain and paf are user
modules - but with not a single driver behind it.
## The red error screen