From e9515d0f8ae5059df335bd78bfcbd3cab739e9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Jos=C3=A9=20Garc=C3=ADa=20Garc=C3=ADa?= Date: Tue, 20 Oct 2026 17:40:13 +0200 Subject: [PATCH 1/4] HLE: 6.60 kernel NIDs for module loading, memset and strnlen The firmware's POPS manager imports these by their 6.60 NIDs: sceKernelLoadModuleForKernel and sceKernelStartModule from ModuleMgrForKernel, SysMem's sceKernelMemset and sceKernelGetUIDcontrolBlock, Sysclib's strnlen, and sceKernelExtendKernelStack, which runs the callback like sceKernelExtendThreadStack does. sceKernelGetUIDcontrolBlock reports no such UID: the manager uses it to make a descriptor it opened in kernel mode usable from user mode, and ours aren't access-checked. --- Core/HLE/sceKernel.cpp | 7 ++++++- Core/HLE/sceKernelHeap.cpp | 16 ++++++++++++++++ Core/HLE/sceKernelInterrupt.cpp | 9 +++++++++ Core/HLE/sceKernelModule.cpp | 3 +++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index 2b7eda22f7..cb9b9901ca 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -916,6 +916,11 @@ const HLEFunction ThreadManForUser[] = {0x65F54FFB, &WrapI_IUU<_sceKernelAllocateTlspl>, "_sceKernelAllocateTlspl", 'i', "ixp" }, }; +// The type picks one of the kernel's preset stack sizes. This is enough for any of them. +static u32 sceKernelExtendKernelStack(MIPSState *mipsState, int type, u32 cb, u32 arg) { + return sceKernelExtendThreadStack(mipsState, 0x4000, cb, arg); +} + const HLEFunction ThreadManForKernel[] = { {0xCEADEB47, &WrapI_U, "sceKernelDelayThread", 'i', "x", HLE_NOT_IN_INTERRUPT | HLE_NOT_DISPATCH_SUSPENDED | HLE_KERNEL_SYSCALL }, @@ -970,7 +975,7 @@ const HLEFunction ThreadManForKernel[] = {0xB736E9FF, &WrapI_IU, "sceKernelFreeVpl", 'i', "ix", HLE_KERNEL_SYSCALL }, {0x1D371B8A, &WrapI_IU, "sceKernelCancelVpl", 'i', "ix", HLE_KERNEL_SYSCALL }, {0x39810265, &WrapI_IU, "sceKernelReferVplStatus", 'i', "ip", HLE_KERNEL_SYSCALL }, - {0xBC31C1B9, nullptr, "sceKernelExtendKernelStack"}, + {0xBC31C1B9, &WrapU_IUU, "sceKernelExtendKernelStack", 'x', "ixx", HLE_KERNEL_SYSCALL }, // New entries must go here at the end, not inserted earlier in this table - the funcindex // (this array position) gets baked directly into resolved-import syscall opcodes written // into guest RAM (see GetSyscallOp() in HLE.cpp), which savestates capture verbatim. Inserting diff --git a/Core/HLE/sceKernelHeap.cpp b/Core/HLE/sceKernelHeap.cpp index 1402c716df..b1f5f163a1 100644 --- a/Core/HLE/sceKernelHeap.cpp +++ b/Core/HLE/sceKernelHeap.cpp @@ -9,6 +9,7 @@ #include "Core/HLE/sceKernel.h" #include "Core/HLE/sceKernelHeap.h" #include "Core/HLE/sceKernelMemory.h" +#include "Core/MemMapHelpers.h" #include "Core/Reporting.h" #include "Core/Util/BlockAllocator.h" @@ -215,6 +216,19 @@ static int sceKernelSetUmdCacheOn(int on) { return hleLogWarning(Log::sceKernel, 0, "UNIMPL"); } +static u32 sceKernelMemset(u32 addr, int value, u32 size) { + if (Memory::IsValidRange(addr, size)) { + Memory::Memset(addr, (u8)value, size, "KernelMemset"); + } + return hleLogDebug(Log::sceKernel, addr); +} + +// The POPS manager uses it to make a file descriptor it opened usable from user mode. Ours +// aren't access-checked, so report no such UID and it skips that. +static int sceKernelGetUIDcontrolBlock(SceUID uid, u32 controlBlockPtr) { + return hleLogDebug(Log::sceKernel, SCE_KERNEL_ERROR_UNKNOWN_UID, "no control blocks"); +} + const HLEFunction SysMemForKernel[] = { { 0X96A3CE2C, &WrapI_U, "sceKernelSetRebootKernel", 'i', "x", HLE_KERNEL_SYSCALL }, { 0X1404C1AA, &WrapI_I, "sceKernelSetUmdCacheOn", 'i', "i", HLE_KERNEL_SYSCALL }, @@ -243,6 +257,8 @@ const HLEFunction SysMemForKernel[] = { { 0xF91FE6AA, &WrapI_V, "sceKernelSetParamSfo", 'i', "", HLE_KERNEL_SYSCALL }, { 0xC2460DD5, &WrapI_V, "sceKernelSetParamSfo", 'i', "", HLE_KERNEL_SYSCALL }, { 0x807179E7, &WrapI_V, "sceKernelSetParamSfo", 'i', "", HLE_KERNEL_SYSCALL }, + { 0x8AE776AF, &WrapU_UIU, "sceKernelMemset", 'x', "xix", HLE_KERNEL_SYSCALL }, + { 0xC90B0992, &WrapI_IU, "sceKernelGetUIDcontrolBlock", 'i', "ix", HLE_KERNEL_SYSCALL }, }; void Register_SysMemForKernel() { diff --git a/Core/HLE/sceKernelInterrupt.cpp b/Core/HLE/sceKernelInterrupt.cpp index 5bf75b07cd..66f60ba855 100644 --- a/Core/HLE/sceKernelInterrupt.cpp +++ b/Core/HLE/sceKernelInterrupt.cpp @@ -844,6 +844,14 @@ static u32 sysclib_strlen(u32 src) { } } +static u32 sysclib_strnlen(u32 src, u32 maxLen) { + u32 len = 0; + while (len < maxLen && Memory::IsValidAddress(src + len) && Memory::ReadUnchecked_U8(src + len) != 0) { + len++; + } + return hleLogVerbose(Log::sceKernel, len); +} + static int sysclib_memcmp(u32 dst, u32 src, u32 size) { if (Memory::IsValidRange(dst, size) && Memory::IsValidRange(src, size)) { const int retval = memcmp(Memory::GetCharPointerUnchecked(dst), Memory::GetCharPointerUnchecked(src), size); @@ -1015,6 +1023,7 @@ const HLEFunction SysclibForKernel[] = {0x4C0E0274, &WrapU_UI, "strrchr", 'x', "xx", HLE_KERNEL_SYSCALL }, {0xCE2F7487, &WrapU_U, "toupper", 'x', "x", HLE_KERNEL_SYSCALL }, {0XC2145E80, &WrapI_UIU, "snprintf", 'i', "xx", HLE_KERNEL_SYSCALL }, + {0x90C5573D, &WrapU_UU, "strnlen", 'x', "xx", HLE_KERNEL_SYSCALL }, }; void Register_Kernel_Library() diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 5506d0a0df..4790bc06c6 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -3703,6 +3703,9 @@ const HLEFunction ModuleMgrForKernel[] = { {0x329C89DB, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, {0x8909A807, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, {0xBDFEEC4F, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, + // 6.60 NIDs. + {0x939E4270, &WrapU_CUU, "sceKernelLoadModuleForKernel", 'x', "sxx", HLE_KERNEL_SYSCALL }, + {0x3FF74DF1, &WrapU_UUUUU, "sceKernelStartModule", 'v', "xxxxx", HLE_NOT_IN_INTERRUPT | HLE_NOT_DISPATCH_SUSPENDED | HLE_KERNEL_SYSCALL }, }; void Register_ModuleMgrForUser() { From 3d69708fca0e53e6163701f404000563d43828dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Jos=C3=A9=20Garc=C3=ADa=20Garc=C3=ADa?= Date: Tue, 20 Oct 2026 17:40:13 +0200 Subject: [PATCH 2/4] sceIo: PGD offset through a pointer, and plain data from that offset ioctl 0x04100002 (set the PGD offset) gets a pointer to the offset with inlen 4 from the firmware's POPS manager. Our own sceNpDrmEdataSetupKey still passes the offset itself. When ioctl 0x04100001 finds no PGD there, the file is read as plain data from that offset, not from the start (the same for ordinary files, whose offset is 0). A converted PS1 EBOOT's disc image is plain, and POPS reads it right after. A PGD with the wrong key still leaves the file at 0. --- Core/HLE/sceIo.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index b7d8719efa..74b523ae0f 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -2755,14 +2755,17 @@ int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 out f->pgdInfo = pgd_open(kirk, pgd_header, 2, key_ptr); if (!f->pgdInfo) { f->npdrm = false; + const u32 plainOffset = f->pgd_offset; f->pgd_offset = 0; // Reset PGD offset so file can be read as regular file - pspFileSystem.SeekFile(f->handle, (s32)0, FILEMOVE_BEGIN); if (memcmp(pgd_header, pgd_magic, 4) == 0) { + pspFileSystem.SeekFile(f->handle, (s32)0, FILEMOVE_BEGIN); // File is PGD file, but key mismatch ERROR_LOG(Log::sceIo, "%s is PGD file, but there's likely a key mismatch. Returning error.", f->fullpath.c_str()); return SCE_ERROR_PGD_INVALID_HEADER; } else { - // File is not encrypted. + // File is not encrypted. Read from where the PGD would start (0 for an ordinary file): a + // converted PS1 PBP's disc image is plain, and POPS reads it right after this. + pspFileSystem.SeekFile(f->handle, (s32)plainOffset, FILEMOVE_BEGIN); WARN_LOG(Log::sceIo, "%s is not an encrypted PGD file as was expected. Proceeding anyway.", f->fullpath.c_str()); return 0; } @@ -2777,7 +2780,13 @@ int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 out // Set PGD offset. Called from sceNpDrmEdataSetupKey case 0x04100002: - f->pgd_offset = indataPtr; + // The POPS manager passes a pointer to the offset, with inlen 4. Our sceNpDrmEdataSetupKey + // passes the offset itself. + if (inlen == 4 && Memory::IsValidRange(indataPtr, 4)) { + f->pgd_offset = Memory::ReadUnchecked_U32(indataPtr); + } else { + f->pgd_offset = indataPtr; + } break; // Get PGD data size. Called from sceNpDrmEdataGetDataSize From d037ca1b7b09fb2a892bbf0011fe7e34956275ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Jos=C3=A9=20Garc=C3=ADa=20Garc=C3=ADa?= Date: Tue, 20 Oct 2026 17:43:14 +0200 Subject: [PATCH 3/4] PBPReader: Read part of a subfile GetSubFile() reads a whole subfile, and refuses those over 256MB. ReadSubFile() reads a range of one, for the PSAR of a PS1 EBOOT, which holds its disc image. --- Core/ELF/PBPReader.cpp | 11 +++++++++++ Core/ELF/PBPReader.h | 2 ++ 2 files changed, 13 insertions(+) diff --git a/Core/ELF/PBPReader.cpp b/Core/ELF/PBPReader.cpp index 96f9819352..cdcb9c963f 100644 --- a/Core/ELF/PBPReader.cpp +++ b/Core/ELF/PBPReader.cpp @@ -79,6 +79,17 @@ bool PBPReader::GetSubFile(PBPSubFile file, std::vector *out) const { return true; } +bool PBPReader::ReadSubFile(PBPSubFile file, u32 offset, size_t size, void *out) const { + if (!file_) { + return false; + } + const size_t subFileSize = GetSubFileSize(file); + if (offset > subFileSize || size > subFileSize - offset) { + return false; + } + return file_->ReadAt(header_.offsets[(int)file] + offset, size, out) == size; +} + bool PBPReader::GetSubFileAsString(PBPSubFile file, std::string *out) const { if (!file_) { out->clear(); diff --git a/Core/ELF/PBPReader.h b/Core/ELF/PBPReader.h index 8781aa66a7..b11ff11baa 100644 --- a/Core/ELF/PBPReader.h +++ b/Core/ELF/PBPReader.h @@ -53,6 +53,8 @@ public: bool GetSubFile(PBPSubFile file, std::vector *out) const; bool GetSubFileAsString(PBPSubFile file, std::string *out) const; + // Part of a subfile, for the large ones. False if it doesn't lie within it. + bool ReadSubFile(PBPSubFile file, u32 offset, size_t size, void *out) const; size_t GetSubFileSize(PBPSubFile file) const { const int num = (int)file; From 3cda5fcea5a953bcb2b7d70a7e0e42c3229816f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Jos=C3=A9=20Garc=C3=ADa=20Garc=C3=ADa?= Date: Tue, 20 Oct 2026 17:43:46 +0200 Subject: [PATCH 4/4] Screenshot: Write the PNG when handed an empty buffer Save8888RGBAScreenshot() calls png_image_write_to_memory() with the buffer it's given, or with none when it's empty, which only measures the PNG. That call succeeds too, so it returned a buffer of the right size, all zeros. Its only caller so far passes one that's big enough. --- Core/Screenshot.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/Screenshot.cpp b/Core/Screenshot.cpp index bc12dd37ca..5572fba0d8 100644 --- a/Core/Screenshot.cpp +++ b/Core/Screenshot.cpp @@ -464,7 +464,8 @@ bool Save8888RGBAScreenshot(std::vector &bufferPNG, const u8 *bufferRGB png_alloc_size_t allocSize = bufferPNG.size(); int result = png_image_write_to_memory(&png, allocSize == 0 ? nullptr : bufferPNG.data(), &allocSize, 0, bufferRGBA8888, w * 4, nullptr); - bool success = result != 0 && png.warning_or_error <= 1; + // Without a buffer, that only measured it. + bool success = !bufferPNG.empty() && result != 0 && png.warning_or_error <= 1; if (!success && allocSize != bufferPNG.size()) { bufferPNG.resize(allocSize); png.warning_or_error = 0;