From 24a2f6110d7f9acccddbc40f79243d9a2decb91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 10 Oct 2026 19:33:10 +0200 Subject: [PATCH] VSH: Patch vsh_module so the XMB can launch unsigned homebrew Picking homebrew in the XMB gave "The game cannot be started. The data is corrupted." without ever calling LoadExec: vshmain.prx rejects a PARAM.SFO without a valid DISC_ID, and a DATA.PSP that's a plain ELF rather than ~PSP. NOP the three branches that do that, the same patches ProCFW's vshctrl makes and JPCSP applies. They're for 6.61's build, and are only applied if all three instructions are what we expect. With it, Kosmodrones launches from the XMB, through the PSP boot logo. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceKernelModule.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 9b7fccbf6d..ba7fd7a9a9 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1303,6 +1303,38 @@ static void LoadAndStartVshKernelModules() { // filename is only used for dumping/metadata. // prxSeed is the extra key a module that came out of an NPDRM container needs to decrypt - see // NpDrmDeriveModuleKey(). Null for everything else, which is the overwhelming majority. +struct ModulePatch { + u32 offset; // From the module base. + u32 expected; + u32 replacement; +}; + +// Lets the XMB launch unsigned homebrew, which it otherwise refuses with "The game cannot be +// started. The data is corrupted." before ever calling LoadExec. These are ProCFW's vshctrl +// patches, as JPCSP applies them (Loader.patchModule). In 6.61's vshmain.prx: +// - +0x12058 and +0x12060: the bne's that reject a PARAM.SFO without a valid DISC_ID. +// - +0x122B0: the beql that rejects the DATA.PSP read from the PBP, a plain ELF rather than ~PSP. +static const ModulePatch vshHomebrewPatches661[] = { + { 0x12058, 0x1440003B, 0 }, + { 0x12060, 0x14400039, 0 }, + { 0x122B0, 0x506000E0, 0 }, +}; + +// Only patches if every expected instruction is there, since the offsets move between firmware builds. +static void ApplyVshHomebrewPatches(u32 base) { + for (const ModulePatch &patch : vshHomebrewPatches661) { + const u32 addr = base + patch.offset; + if (!Memory::IsValid4AlignedAddress(addr) || Memory::ReadUnchecked_U32(addr) != patch.expected) { + WARN_LOG(Log::sceModule, "vsh_module isn't a build the homebrew patches are known for (+%x), leaving it alone", patch.offset); + return; + } + } + for (const ModulePatch &patch : vshHomebrewPatches661) { + Memory::WriteUnchecked_U32(patch.replacement, base + patch.offset); + } + INFO_LOG(Log::sceModule, "Patched vsh_module to launch unsigned homebrew"); +} + static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 loadAddress, bool fromTop, std::string *error_string, u32 *magic, std::string_view filename, u32 &error, const u8 *prxSeed = nullptr) { // The magic reads below need four bytes, and the ~SCE branch another four after that. Everything // downstream checks its own sizes; this is just so we can look at the magic at all. The PBP path @@ -1710,6 +1742,8 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load } else { Memory::WriteUnchecked_U32(0, patchAddr); } + + ApplyVshHomebrewPatches(module->memoryBlockAddr); } // Let's also get a truncated version.