From b21e40a53ff9ca36ee130548f202a2e510198e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 9 Sep 2026 12:56:34 -0600 Subject: [PATCH] Boot the VSH on firmware 6.60 too, and stop patching builds blind 6.60 ships byte-identical paf.prx and vshmain.prx to 6.61 - all 6338 + 669 functions disassemble the same - and boots to an interactive XMB, so let FirmwareVersionSupportsVSH accept it. That matters because no UMD carries 6.61 (it was download-only), so 6.60 is the best a disc-installed firmware can be. The two module patches were hardcoded offsets from the module base applied to any module of the right name, which is quietly wrong on any other build: - The scePaf heap arena slot moves with every build (0x18CCD8 on 6.00 through 0x18D728 on 6.60/6.61) but sits at gp - 0x7E88 in all of them, so find it that way. On its own this turns an immediate SIGSEGV inside scePaf into a clean stall on 6.00 through 6.39 - they still don't reach an XMB, they get stuck in sceVshBridge_Driver instead. - The vsh_module alarm-category offset has no such anchor, so check the word there is the one the patch was derived from. On 6.20 and 6.00 it's ASCII string data - the unconditional write was corrupting a string table. Also resolve the per-model kernel drivers (memlmd, loadexec, wlanfirm) to the model being emulated. They were asked for as _01g, which a firmware unpacked for a single model doesn't have - and our own updater unpack defaults to 02g. Co-Authored-By: Claude Opus 5 --- Core/HLE/sceKernelModule.cpp | 55 ++++++++++++++++++++++++++++++------ Core/Util/PSARUnpack.cpp | 7 +++-- docs/VSHBootInvestigation.md | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 66ad4b05fc..8c182bb557 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -50,6 +50,7 @@ #include "Core/ELF/PrxDecrypter.h" #include "Core/HLE/scePspNpDrm_user.h" #include "Core/Util/KL4E.h" +#include "Core/Util/PSARUnpack.h" #include "Core/FileSystems/FileSystem.h" #include "Core/FileSystems/MetaFileSystem.h" #include "Core/Util/BlockAllocator.h" @@ -1176,6 +1177,28 @@ static void LoadAndStartVshKernelModule(const char *path, SceKernelSMOption *smo } } +// Some of the kernel's drivers have a per-model build (memlmd, loadexec, wlanfirm, ...), and a +// firmware installed for one model ships only that model's - so asking for "_01g" unconditionally +// fails on, say, an 02g install, which is what PPSSPP's own updater unpack produces by default. +// Swap in the model we're emulating when the path names a model and that file is actually there. +static std::string ResolveVshModelModule(const char *path) { + std::string_view name(path); + const size_t model = name.find("_01g.prx"); + if (model == std::string_view::npos) { + return std::string(path); + } + const int generation = (int)EmulatedModelGeneration(); + if (generation == 1) { + return std::string(path); + } + std::string candidate = StringFromFormat("%.*s_%02dg.prx", (int)model, path, generation); + if (pspFileSystem.GetFileInfo(candidate).exists) { + return candidate; + } + // A dump unpacked for every model has the 01g one too, so this isn't necessarily a failure. + return std::string(path); +} + static void LoadAndStartVshKernelModules() { // These 11 are small, simple kernel drivers (a few KB to ~100KB of code each) that don't // declare their own smaller module_start_thread_stacksize, so __KernelStartModule's @@ -1202,7 +1225,7 @@ static void LoadAndStartVshKernelModules() { smallStackOption.stacksize = 0x40000; */ for (const char *path : vshSmallKernelModulePaths) { - LoadAndStartVshKernelModule(path, nullptr); + LoadAndStartVshKernelModule(ResolveVshModelModule(path).c_str(), nullptr); } static const char *const vshUiKernelModulePaths[] = { @@ -1545,9 +1568,6 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load strncpy(module->nm.name, modinfo->name, ARRAY_SIZE(module->nm.name)); if (equals(module->nm.name, "scePaf_Module")) { - // NOTE: This hackery is likely firmware-version-specific, so will need tweaking for - // other firmware versions than 6.61. - // // scePaf's own heap allocator expects a real memory-pool base address to already be // patched into this BSS slot before any of its code runs. Real hardware's loader (or // an early kernel init step) apparently does this - checked all 27 of scePaf's @@ -1556,10 +1576,16 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load // bump-allocator get used directly as absolute pointers, crashing almost immediately // when a client module (e.g. vsh_module) makes its first heap allocation. // See docs/VSHBootInvestigation.md for the full investigation. - const u32 scePafHeapArenaOffset = 0x18D728; // Offset from module base to the BSS pointer slot. + // + // The slot is the second of the two pool pointers scePaf's own init fills in with + // sceKernelTryAllocateFpl. Its offset from the module base moves with every build, but + // it sits at a fixed distance below gp in all of them - checked against the paf.prx of + // 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61, where the base-relative offset ranges + // over 0x18CCD8..0x18D728 and gp - slot is 0x7E88 every time. + const u32 scePafHeapArenaGpOffset = 0x7E88; u32 scePafHeapArenaSize = 0x00850000; // Matches scePaf's own compiled-in default heap size. u32 arenaAddr = userMemory.Alloc(scePafHeapArenaSize, false, "scePafHeapArena"); - u32 patchAddr = module->memoryBlockAddr + scePafHeapArenaOffset; + u32 patchAddr = module->nm.gp_value - scePafHeapArenaGpOffset; if (arenaAddr != (u32)-1 && Memory::IsValid4AlignedAddress(patchAddr)) { Memory::WriteUnchecked_U32(arenaAddr, patchAddr); } else { @@ -1587,12 +1613,23 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load // would look like (the scan's own code already handles count<=0 as "nothing to do" // for category 0 the same way). This is a narrow, targeted patch of one 4-byte value // vsh_module itself never properly initializes, not a general vsh_module patch. + // + // Unlike the scePaf patch above, this offset is into rodata rather than at a fixed + // distance from gp, and it moves with the build - so check that what's there is the + // value we identified before overwriting it, rather than writing blind into a firmware + // we haven't looked at. 6.60 and 6.61 ship byte-identical builds of vshmain.prx and + // both have the same float here; anything else is a version this patch wasn't derived + // from, and those don't reach an XMB for other reasons anyway. const u32 vshAlarmCategory1CountOffset = 0x455C4; // Offset from module base. + const u32 vshAlarmCategory1CountExpected = 0x3F666666; // Leftover 0.9f from a float array. u32 patchAddr = module->memoryBlockAddr + vshAlarmCategory1CountOffset; - if (Memory::IsValid4AlignedAddress(patchAddr)) { - Memory::WriteUnchecked_U32(0, patchAddr); - } else { + if (!Memory::IsValid4AlignedAddress(patchAddr)) { WARN_LOG(Log::sceModule, "Failed to patch vsh_module alarm category 1 count"); + } else if (Memory::ReadUnchecked_U32(patchAddr) != vshAlarmCategory1CountExpected) { + WARN_LOG(Log::sceModule, "vsh_module isn't the build the alarm-category patch was derived from (%08x at +%x), leaving it alone", + Memory::ReadUnchecked_U32(patchAddr), vshAlarmCategory1CountOffset); + } else { + Memory::WriteUnchecked_U32(0, patchAddr); } } diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 3993210bbd..cc92bfa474 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1160,8 +1160,11 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { } bool FirmwareVersionSupportsVSH(std::string_view version) { - // See the module patches in sceKernelModule.cpp - they're tied to 6.61's exact offsets. - return version == "6.61"; + // 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"; } std::string BundledUpdateInfo::Describe() const { diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md index 952eaa8da8..50f2e10b81 100644 --- a/docs/VSHBootInvestigation.md +++ b/docs/VSHBootInvestigation.md @@ -66,6 +66,56 @@ checking what is actually on screen means running the app build. From headless, the per-frame display list signature (see the red error screen section): a screen that is finished 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. + +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. + +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. + +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. + +### Making the two 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. + +- **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. + +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`. + +### Kernel modules with per-model builds + +`LoadAndStartVshKernelModules()` asked for `memlmd_01g.prx`, `loadexec_01g.prx` and +`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). + ## The red error screen What is known, all measured from a 40-emulated-second `--vsh` boot: