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: