From 480c5e44e2563e17cd06819f433411d2ba44e173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 9 Sep 2026 13:40:37 -0600 Subject: [PATCH] Boot the VSH on every firmware from 5.01 up The blocker below 6.60 wasn't offsets, it was that Sony renumbered the kernel *_driver NIDs between versions. A function we HLE under its 6.6x NID is a stranger on an older build, so the import lands in the real firmware module instead - and that's where it goes wrong: - sceRtc_driver sceRtcSetAlarmTick. 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 with every thread parked" symptom; the tell was a fourth SceSysconSync waiter a healthy boot lacks. - sceHprm_driver sceHprmReadLatch, called once a frame - so before this an older firmware's 12-second boot logged ~20000 lines of one unresolved import. Three extra NIDs each, found by disassembling the module from both firmwares and matching on the address of the user-mode export whose NID never changed (sceRtc/0x7D1FBED3, sceHprm/0x40D2F9F0). 5.55 additionally needed two PRX decryption keys we didn't have (0x4C941AF0 and 0x4C941BF0) - without them none of flash0:/kd decrypted and the shell came up with no drivers behind it at all. Checked one release at a time against every version that ships on a disc, plus 6.61. 4.05 and below still die on a null write inside vsh_module. Co-Authored-By: Claude Opus 5 --- Core/ELF/PrxDecrypter.cpp | 4 +++ Core/HLE/sceHprm.cpp | 9 ++++++ Core/HLE/sceRtc.cpp | 13 +++++++- Core/Util/PSARUnpack.cpp | 32 ++++++++++++++++--- docs/VSHBootInvestigation.md | 60 ++++++++++++++++++++++++++---------- 5 files changed, 95 insertions(+), 23 deletions(-) diff --git a/Core/ELF/PrxDecrypter.cpp b/Core/ELF/PrxDecrypter.cpp index e2f85c9a47..35acc1348f 100644 --- a/Core/ELF/PrxDecrypter.cpp +++ b/Core/ELF/PrxDecrypter.cpp @@ -61,6 +61,8 @@ static const u8 keys500_c[] = {0xA3, 0x5D, 0x51, 0xE6, 0x56, 0xC8, 0x01, 0xCA, 0 static const u8 keys505_a[] = {0x7B, 0x94, 0x72, 0x27, 0x4C, 0xCC, 0x54, 0x3B, 0xAE, 0xDF, 0x46, 0x37, 0xAC, 0x01, 0x4D, 0x87}; static const u8 keys505_0[] = {0x2E, 0x8E, 0x97, 0xA2, 0x85, 0x42, 0x70, 0x73, 0x18, 0xDA, 0xA0, 0x8A, 0xF8, 0x62, 0xA2, 0xB0}; static const u8 keys505_1[] = {0x58, 0x2A, 0x4C, 0x69, 0x19, 0x7B, 0x83, 0x3D, 0xD2, 0x61, 0x61, 0xFE, 0x14, 0xEE, 0xAA, 0x11}; +static const u8 keys555_k1[] = {0x9F, 0xFD, 0x4C, 0x28, 0x20, 0xB1, 0x3E, 0x76, 0x36, 0x4A, 0xAB, 0x1C, 0x54, 0xBC, 0x3B, 0xDC}; +static const u8 keys555_k2[] = {0xAB, 0x1A, 0x74, 0x43, 0xF7, 0x4F, 0xE5, 0xFF, 0x04, 0xA5, 0xFC, 0x3B, 0xEC, 0xD4, 0xF8, 0xF0}; static const u8 keys570_5k[] = {0x6D, 0x72, 0xA4, 0xBA, 0x7F, 0xBF, 0xD1, 0xF1, 0xA9, 0xF3, 0xBB, 0x07, 0x1B, 0xC0, 0xB3, 0x66}; static const u8 keys600_1[] = {0xE3, 0x52, 0x39, 0x97, 0x3B, 0x84, 0x41, 0x1C, 0xC3, 0x23, 0xF1, 0xB8, 0xA9, 0x09, 0x4B, 0xF0}; static const u8 keys600_2[] = {0xE1, 0x45, 0x93, 0x2C, 0x53, 0xE2, 0xAB, 0x06, 0x6F, 0xB6, 0x8F, 0x0B, 0x66, 0x91, 0xE7, 0x1E}; @@ -404,6 +406,8 @@ static const TAG_INFO2 g_tagInfo2[] = { 0x4C9422F0, keys600_2, 0x43 }, { 0x4C941EF0, keys600_1, 0x43 }, { 0x4C9429F0, keys570_5k, 0x43 }, + { 0x4C941BF0, keys555_k2, 0x43 }, + { 0x4C941AF0, keys555_k1, 0x43 }, { 0x457B0BF0, keys505_a, 0x5B }, { 0x4C9419F0, keys505_1, 0x43 }, { 0x4C9418F0, keys505_0, 0x43 }, diff --git a/Core/HLE/sceHprm.cpp b/Core/HLE/sceHprm.cpp index 718fc9e781..d6b9474152 100644 --- a/Core/HLE/sceHprm.cpp +++ b/Core/HLE/sceHprm.cpp @@ -91,6 +91,15 @@ const HLEFunction sceHprm_driver[] = // Purpose unknown - JPCSP names it after its NID too, and returns 0. Present so the VSH's // one startup call resolves instead of trapping. {0XDC895B2B, &WrapU_V, "sceHprm_driver_DC895B2B", 'x', ""}, + // Older firmwares number these two differently. Same functions - matched by address against + // the same module's user-mode sceHprm exports (sceHprmReadLatch is sceHprm/0x40D2F9F0 in + // every build). The VSH calls ReadLatch once a frame, so without these an older firmware's + // XMB spends the whole boot trapping on an unresolved import. + // NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array. + {0XA3A87975, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 6.31 - 6.39 + {0X5FC5E53B, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 6.00 - 6.20 + {0X748FC3C8, &WrapU_V, "sceHprm_driver_DC895B2B", 'x', ""}, // 6.31 - 6.39 + {0X605DEA7A, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 5.03 - 5.55 }; void Register_sceHprm_driver() diff --git a/Core/HLE/sceRtc.cpp b/Core/HLE/sceRtc.cpp index 8379d12091..b6e808b559 100644 --- a/Core/HLE/sceRtc.cpp +++ b/Core/HLE/sceRtc.cpp @@ -1154,14 +1154,25 @@ void Register_sceRtc() RegisterHLEModule("sceRtc", ARRAY_SIZE(sceRtc), sceRtc); } -// sceRtc_driver is the kernel-only alias some firmware-660+ modules (e.g. the VSH's +// sceRtc_driver is the kernel-only alias some firmware modules (e.g. the VSH's // sceVshBridge_Driver) import from instead of plain sceRtc - same underlying functions, // just also exported under a second, kernel-suffixed module name. Confirmed by cross- // checking jpcsp's sceRtc.java, which registers 0xE09880CF as an alternate NID on the exact // same sceRtcSetAlarmTick() method (JPCSP doesn't distinguish import module names the way // PPSSPP's HLE dispatch does, but the NID->function mapping is the same either way). +// +// Sony renumbered the kernel NIDs across firmware versions, so the same function has three of +// them. All three are the export at rtc.prx+0xB28, which every one of these builds also +// exports as the user-mode sceRtc/0x7D1FBED3 (sceRtcSetAlarmTick) - that's how they line up. +// Covering the older two matters for the VSH: without the HLE, sceVshBridge_Driver's alarm +// call lands in the real rtc.prx, which goes on into syscon.prx and blocks forever on a +// SceSysconSync semaphore that never gets signalled. const HLEFunction sceRtc_driver[] = { {0XE09880CF, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, + // NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array. + {0X54B9C589, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.31 - 6.39 + {0X68AED59A, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.00 - 6.20 + {0XADAF231F, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 5.03 - 5.55 }; void Register_sceRtc_driver() diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index cc92bfa474..9b37c6e75b 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1160,11 +1160,33 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { } bool FirmwareVersionSupportsVSH(std::string_view version) { - // See the module patches in sceKernelModule.cpp - they're offsets into paf.prx and - // vshmain.prx, so they only hold for versions those two modules are unchanged in. 6.60 and - // 6.61 ship byte-identical builds of both (all 6338 + 669 functions disassemble the same), - // and both boot to an interactive XMB. - return version == "6.61" || version == "6.60"; + // Every firmware from 5.01 up boots to an interactive XMB, checked one release at a time + // against every version that ships on a disc (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. 4.05 and below + // still die on a null write inside vsh_module - a separate problem, not an offset to move. + // + // "6.61" -> 661. Sony always writes the minor part with two digits, but don't rely on it: + // a single-digit one is a tens value ("5.5" is 5.50, not 5.05). + const size_t dot = version.find('.'); + if (dot == std::string_view::npos || dot == 0 || dot + 1 >= version.size()) { + return false; + } + int numeric = 0; + for (size_t i = 0; i < version.size(); i++) { + if (i == dot) { + continue; + } + if (version[i] < '0' || version[i] > '9') { + return false; + } + numeric = numeric * 10 + (version[i] - '0'); + } + if (version.size() - dot == 2) { // One digit after the dot. + numeric *= 10; + } else if (version.size() - dot != 3) { + return false; + } + return numeric >= 501; } std::string BundledUpdateInfo::Describe() const { diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md index 50f2e10b81..adc828bef4 100644 --- a/docs/VSHBootInvestigation.md +++ b/docs/VSHBootInvestigation.md @@ -68,25 +68,50 @@ changing repeats byte-identically, and a live menu does not. ## Which firmware versions work -6.61 and **6.60**. `FirmwareVersionSupportsVSH()` (`Core/Util/PSARUnpack.cpp`) is the gate the UI -uses, and it lists exactly those two. +**5.01 through 6.61**, all of them. `FirmwareVersionSupportsVSH()` +(`Core/Util/PSARUnpack.cpp`) is the gate the UI uses. -6.60 needed no new offsets: it ships byte-identical builds of both modules the boot patches touch. -Disassembling 6.60's and 6.61's `paf.prx` and `vshmain.prx` with `--re-module` and diffing gives -zero differences across all 6338 + 669 functions - same size, same gp, same entry points, only the -CRC (which covers the encrypted file, signature included) differs. A 6.60 boot reaches the same -9-thread steady state at 6 emulated seconds and renders an interactive XMB. +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. -6.61 is not on any UMD - it was a download-only update - so a firmware installed from a game disc -tops out at 6.60. That is the case the 6.60 support exists for. +**4.05 and below** still die on a `SIGSEGV at 00000000` inside `vsh_module` itself. Different +problem, not an offset to move. -Everything below that stops short, measured on 6.00, 6.20, 6.30, 6.31, 6.35, 6.37 and 6.39 (there -is no released version between 6.39 and 6.60). They get past module loading and start -`ScePafThread`, then stall in `sceVshBridge_Driver` on repeated unresolved `SysMemForKernel` -imports without ever starting a `ScePafJob`. That is a fresh investigation, not a matter of moving -an offset. +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. -### Making the two patches version-safe +### What it took to get below 6.60 + +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. + +- **`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. + +**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. + +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. + +### Making the two module patches version-safe 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. @@ -102,11 +127,12 @@ name. That is fine for the version they were derived from and quietly wrong for 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. + corrupting a string table on those. Only 6.60/6.61 need the patch at all; every older version + reaches the XMB without it. 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 to `sceVshBridge_Driver`. +version, which is what moved the blocker on to the rtc NID above. ### Kernel modules with per-model builds