From 8a02d1ee0f70330a58bcc4c3b454c57fb8f7e4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 8 Sep 2026 14:16:05 -0600 Subject: [PATCH 1/5] Keep the MD5 context in game memory, and make MT19937 actually be MT19937 Three fixes, all of them things the new hardware tests turned up. sceMd5Block* and sceKernelUtilsMd5Block* shared one static md5_context and ignored the context pointer the caller passed in, with a TODO saying it would do "unless games do several MD5 concurrently". hash/md5ctx shows a real PSP keeps everything in the caller's 96 bytes and happily runs two digests at once, so do that instead: the state, the counters and the block buffer now live at ctxAddr in the game's own memory, in the layout the test pins down. Two interleaved digests come out right, and a context that gets copied mid-digest carries on correctly. As a side effect the state is now covered by savestates, which a file-static never was. MersenneTwister masked both halves with 0x80000000 where the low half needs 0x7FFFFFFF, so sceMt19937UInt and sceKernelUtilsMt19937UInt were returning a sequence that isn't MT19937 at all - every number differed from hardware from the first draw. hash/mt19937ctx computes the reference sequence itself and confirms the PSP is plain MT19937; with the mask fixed we match it for both seeds tested. Init also twists the array immediately, as hardware does, so a context that has been seeded but not drawn from now holds what a real one would. sceKernelCreateTlspl accepted partitions up to 9 before falling through to the permission check. Hardware draws the line at 6 - threads/tls/create records 7, 8, 9 and 10 all returning ILLEGAL_ARGUMENT - so 8 and 9 were coming back ILLEGAL_PERM. Note this is genuinely different from sceKernelCreateVpl right above it, which does let 8 and 9 through to ILLEGAL_PERM; the two had been sharing a check that was only ever right for Vpl. Risk worth naming: the MT19937 change alters the numbers any game gets from these calls. That's the point - they were wrong - but a savestate taken mid-sequence will resume with a generator that behaves differently from the one that made it. --- Common/Data/Random/Rng.h | 29 ++++--- Core/HLE/sceKernelMemory.cpp | 5 +- Core/HLE/sceMd5.cpp | 146 ++++++++++++++++++++++++----------- pspautotests | 2 +- test.py | 6 +- 5 files changed, 130 insertions(+), 58 deletions(-) diff --git a/Common/Data/Random/Rng.h b/Common/Data/Random/Rng.h index 082053735f..b72c073124 100644 --- a/Common/Data/Random/Rng.h +++ b/Common/Data/Random/Rng.h @@ -36,24 +36,32 @@ public: }; // Data must consist only of the index and the twister array. This matches the PSP -// MT context exactly. +// MT context exactly - see pspautotests hash/mt19937ctx, which records the whole 2500 byte +// context off a real PSP: the counter sits at offset 0 and the 624 word array at offset 4, +// already twisted once by the time Init returns. class MersenneTwister { public: MersenneTwister(uint32_t seed) : index_(0) { mt_[0] = seed; for (uint32_t i = 1; i < MT_SIZE; i++) mt_[i] = (1812433253UL * (mt_[i - 1] ^ (mt_[i - 1] >> 30)) + i); + // The PSP twists during Init rather than lazily on the first draw, so a context that + // has been seeded but not yet used already holds the twisted array. + for (uint32_t i = 0; i < MT_SIZE; i++) + twist(i); } uint32_t R32() { - if (index_ == 0) - gen(); + // Read first, then regenerate that word in place - done in index order this is the + // same as twisting the whole array in one go, and it matches what hardware leaves + // behind in the context after each draw. uint32_t y = mt_[index_]; + twist(index_); + index_ = (index_ + 1) % MT_SIZE; y ^= y >> 11; y ^= (y << 7) & 2636928640UL; y ^= (y << 15) & 4022730752UL; y ^= y >> 18; - index_ = (index_ + 1) % MT_SIZE; return y; } @@ -65,12 +73,11 @@ private: u32_le index_; u32_le mt_[MT_SIZE]; - void gen() { - for(uint32_t i = 0; i < MT_SIZE; i++){ - uint32_t y = (mt_[i] & 0x80000000) + (mt_[(i + 1) % MT_SIZE] & 0x80000000); - mt_[i] = mt_[(i + 397) % MT_SIZE] ^ (y >> 1); - if (y % 2) mt_[i] ^= 2567483615UL; - } - return; + void twist(uint32_t i) { + // The low half has to come through the LOWER mask - masking both halves with + // 0x80000000 makes this something other than MT19937, which is what it used to do. + uint32_t y = (mt_[i] & 0x80000000) | (mt_[(i + 1) % MT_SIZE] & 0x7FFFFFFF); + mt_[i] = mt_[(i + 397) % MT_SIZE] ^ (y >> 1); + if (y & 1) mt_[i] = mt_[i] ^ 2567483615UL; } }; diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index c89fe80214..f70d47aee3 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -1863,7 +1863,10 @@ SceUID sceKernelCreateTlspl(const char *name, u32 partition, u32 attr, u32 block return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_NO_MEMORY, "invalid name"); if ((attr & ~PSP_TLSPL_ATTR_KNOWN) >= 0x100) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ATTR, "invalid attr parameter: %08x", attr); - if (partition < 1 || partition > 9 || partition == 7) + // Tlspl draws the line at 6, unlike Vpl above: threads/tls/create records 7, 8, 9 and 10 all + // returning ILLEGAL_ARGUMENT on a real PSP, where threads/vpl/create has 8 and 9 falling + // through to ILLEGAL_PERM. Same-looking check, genuinely different range. + if (partition < 1 || partition > 6) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ARGUMENT, "invalid partition %d", partition); BlockAllocator *allocator = BlockAllocatorFromID(partition); diff --git a/Core/HLE/sceMd5.cpp b/Core/HLE/sceMd5.cpp index 6653ae9e92..6abae8b5ca 100644 --- a/Core/HLE/sceMd5.cpp +++ b/Core/HLE/sceMd5.cpp @@ -48,9 +48,103 @@ u32 sceKernelUtilsMt19937UInt(u32 ctx) { return mt->R32(); } -// TODO: This MD5 stuff needs tests! +// The MD5 context lives in the game's own memory, exactly as it does on hardware, so two digests +// can be in flight at once. Layout confirmed against a real PSP by pspautotests hash/md5ctx: +// 96 bytes, and the word at offset 16 is never written by the kernel. +// +// sceKernelUtilsSha1Block* below still keeps one global state, for the same reason this used to - +// there's no hardware recording of the SHA-1 context layout yet. +struct PSPMd5Context { + u32_le h[4]; + u32_le pad; // the kernel leaves this one alone + u16_le usRemains; // bytes currently held in buf + u16_le usComputed; // stays zero on hardware + u64_le ullTotalLen; // total bytes fed in so far + u8 buf[64]; +}; + +static void Md5ContextRead(const PSPPointer &ctx, md5_context *out) { + for (int i = 0; i < 4; i++) { + out->state[i] = ctx->h[i]; + } + u64 total = ctx->ullTotalLen; + out->total[0] = (u32)total; + out->total[1] = (u32)(total >> 32); + memcpy(out->buffer, ctx->buf, sizeof(out->buffer)); +} + +static void Md5ContextWrite(PSPPointer &ctx, const md5_context *in) { + for (int i = 0; i < 4; i++) { + ctx->h[i] = (u32)in->state[i]; + } + u64 total = (u64)(u32)in->total[0] | ((u64)(u32)in->total[1] << 32); + ctx->ullTotalLen = total; + ctx->usRemains = (u16)(total & 0x3F); + ctx->usComputed = 0; + ctx.NotifyWrite("Md5Context"); +} + +// Hardware streams every byte through buf on its way into the digest, so after a whole-block +// update buf holds that block - not just the leftover tail. Our md5 hashes full blocks straight +// out of the caller's buffer, so reproduce what the PSP would have left behind: the data laid +// into a 64 byte window at its absolute offset in the stream. +static void Md5ContextFillBuf(PSPPointer &ctx, u64 totalBefore, const u8 *data, u32 len) { + if (len == 0) { + return; + } + if (len >= 64) { + data += len - 64; + totalBefore += len - 64; + len = 64; + } + for (u32 i = 0; i < len; i++) { + ctx->buf[(u32)((totalBefore + i) % 64)] = data[i]; + } + ctx.NotifyWrite("Md5Context"); +} + +// Init touches only the state and the counters - buf and the pad word are left as they were. +static int Md5BlockInit(u32 ctxAddr) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid()) + return hleLogError(Log::HLE, -1, "bad context address"); + md5_context fresh; + ppsspp_md5_starts(&fresh); + for (int i = 0; i < 4; i++) { + ctx->h[i] = (u32)fresh.state[i]; + } + ctx->usRemains = 0; + ctx->usComputed = 0; + ctx->ullTotalLen = 0; + ctx.NotifyWrite("Md5Context"); + return hleLogDebug(Log::HLE, 0); +} + +static int Md5BlockUpdate(u32 ctxAddr, u32 dataPtr, u32 len) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(dataPtr, len)) + return hleLogError(Log::HLE, -1, "bad address"); + md5_context work; + Md5ContextRead(ctx, &work); + u64 totalBefore = ctx->ullTotalLen; + const u8 *data = Memory::GetPointerWriteUnchecked(dataPtr); + ppsspp_md5_update(&work, (unsigned char *)data, (int)len); + Md5ContextWrite(ctx, &work); + Md5ContextFillBuf(ctx, totalBefore, data, len); + return hleLogDebug(Log::HLE, 0); +} + +static int Md5BlockResult(u32 ctxAddr, u32 digestAddr) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(digestAddr, 16)) + return hleLogError(Log::HLE, -1, "bad address"); + md5_context work; + Md5ContextRead(ctx, &work); + ppsspp_md5_finish(&work, Memory::GetPointerWriteUnchecked(digestAddr)); + Md5ContextWrite(ctx, &work); + return hleLogDebug(Log::HLE, 0); +} -static md5_context md5_ctx; static int sceMd5Digest(u32 dataAddr, u32 len, u32 digestAddr) { DEBUG_LOG(Log::HLE, "sceMd5Digest(%08x, %d, %08x)", dataAddr, len, digestAddr); @@ -63,33 +157,15 @@ static int sceMd5Digest(u32 dataAddr, u32 len, u32 digestAddr) { } static int sceMd5BlockInit(u32 ctxAddr) { - DEBUG_LOG(Log::HLE, "sceMd5BlockInit(%08x)", ctxAddr); - if (!Memory::IsValidAddress(ctxAddr)) - return -1; - - // TODO: Until I know how large a context is, we just go all lazy and use a global context, - // which will work just fine unless games do several MD5 concurrently. - - ppsspp_md5_starts(&md5_ctx); - return 0; + return Md5BlockInit(ctxAddr); } static int sceMd5BlockUpdate(u32 ctxAddr, u32 dataPtr, u32 len) { - DEBUG_LOG(Log::HLE, "sceMd5BlockUpdate(%08x, %08x, %d)", ctxAddr, dataPtr, len); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(dataPtr)) - return -1; - - ppsspp_md5_update(&md5_ctx, Memory::GetPointerWriteUnchecked(dataPtr), (int)len); - return 0; + return Md5BlockUpdate(ctxAddr, dataPtr, len); } static int sceMd5BlockResult(u32 ctxAddr, u32 digestAddr) { - DEBUG_LOG(Log::HLE, "sceMd5BlockResult(%08x, %08x)", ctxAddr, digestAddr); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(digestAddr)) - return -1; - - ppsspp_md5_finish(&md5_ctx, Memory::GetPointerWriteUnchecked(digestAddr)); - return 0; + return Md5BlockResult(ctxAddr, digestAddr); } int sceKernelUtilsMd5Digest(u32 dataAddr, int len, u32 digestAddr) { @@ -103,33 +179,15 @@ int sceKernelUtilsMd5Digest(u32 dataAddr, int len, u32 digestAddr) { } int sceKernelUtilsMd5BlockInit(u32 ctxAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsMd5BlockInit(%08x)", ctxAddr); - if (!Memory::IsValidAddress(ctxAddr)) - return -1; - - // TODO: Until I know how large a context is, we just go all lazy and use a global context, - // which will work just fine unless games do several MD5 concurrently. - - ppsspp_md5_starts(&md5_ctx); - return 0; + return Md5BlockInit(ctxAddr); } int sceKernelUtilsMd5BlockUpdate(u32 ctxAddr, u32 dataPtr, int len) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsMd5BlockUpdate(%08x, %08x, %d)", ctxAddr, dataPtr, len); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(dataPtr)) - return -1; - - ppsspp_md5_update(&md5_ctx, Memory::GetPointerWriteUnchecked(dataPtr), (int)len); - return 0; + return Md5BlockUpdate(ctxAddr, dataPtr, (u32)len); } int sceKernelUtilsMd5BlockResult(u32 ctxAddr, u32 digestAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsMd5BlockResult(%08x, %08x)", ctxAddr, digestAddr); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(digestAddr)) - return -1; - - ppsspp_md5_finish(&md5_ctx, Memory::GetPointerWriteUnchecked(digestAddr)); - return 0; + return Md5BlockResult(ctxAddr, digestAddr); } diff --git a/pspautotests b/pspautotests index 40317c54ce..1c4a78d809 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 40317c54ce6709b0f52e7617e01fbaff03806a19 +Subproject commit 1c4a78d809888ee6b947e89d4fb14b92c09c5ade diff --git a/test.py b/test.py index 951a7195ca..c2fd568af2 100755 --- a/test.py +++ b/test.py @@ -210,6 +210,8 @@ tests_good = [ "gpu/vertices/morph", # "gpu/vertices/texcoords", # See issue #19093 "hash/hash", + "hash/md5ctx", + "hash/mt19937ctx", "hle/check_not_used_uids", "intr/intr", "intr/enablesub", @@ -336,6 +338,7 @@ tests_good = [ "threads/threads/threadmanidlist", "threads/threads/threadmanidtype", "threads/threads/threads", + "threads/tls/create", "threads/tls/delete", "threads/tls/get", "threads/tls/free", @@ -456,6 +459,7 @@ tests_next = [ "io/io/io", "io/iodrv/iodrv", "io/shortname/shortname", + "io/stat/stat", "io/open/tty0", "jpeg/csc", "jpeg/decode", @@ -481,7 +485,7 @@ tests_next = [ "threads/scheduling/scheduling", "threads/threads/create", "threads/threads/terminate", - "threads/tls/create", + "threads/tls/memory", "threads/vpl/create", "umd/io/umd_io", "umd/raw_access/raw_access", From 705ea7eb02bc81def11cfe23cf4256aae62a624e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 8 Sep 2026 14:38:46 -0600 Subject: [PATCH 2/5] Keep the SHA-1 context in game memory too, and scope the Tlspl partition range to user mode sceKernelUtilsSha1Block* had the same single global context that MD5 did, so it gets the same treatment: state, counters and block buffer now live at ctxAddr in the layout hash/sha1ctx records off hardware. Unlike MD5, SHA-1 does not stream whole blocks through buf, which happens to be what our sha1_update already does - so no fill-in step is needed there. The Tlspl partition range from the last commit was too broad a cut. Hardware says only 1-6 exist, but that recording is from user mode, and BlockAllocatorFromID deliberately maps 8 and 10 to the user partition for a kernel-mode caller - rejecting them outright would have taken that away. The tightened range now applies to user mode only and kernel mode keeps what it had. threads/tls/partition also shows the answer doesn't depend on the compiled SDK version, checked across 1.00 through 6.06, and that partition 5 is accepted - which no test had covered. --- Core/HLE/sceKernelMemory.cpp | 15 ++++-- Core/HLE/sceMd5.cpp | 93 ++++++++++++++++++++++++++---------- pspautotests | 2 +- test.py | 2 + 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index f70d47aee3..47e312a480 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -1863,10 +1863,17 @@ SceUID sceKernelCreateTlspl(const char *name, u32 partition, u32 attr, u32 block return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_NO_MEMORY, "invalid name"); if ((attr & ~PSP_TLSPL_ATTR_KNOWN) >= 0x100) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ATTR, "invalid attr parameter: %08x", attr); - // Tlspl draws the line at 6, unlike Vpl above: threads/tls/create records 7, 8, 9 and 10 all - // returning ILLEGAL_ARGUMENT on a real PSP, where threads/vpl/create has 8 and 9 falling - // through to ILLEGAL_PERM. Same-looking check, genuinely different range. - if (partition < 1 || partition > 6) + // From user mode only 1-6 exist: threads/tls/partition records 7 and up all returning + // ILLEGAL_ARGUMENT on a real PSP, where 1, 3 and 4 fall through to ILLEGAL_PERM below. Note + // this differs from sceKernelCreateVpl above, which does let 8 and 9 reach the permission + // check - the two used to share a range that was only ever right for Vpl. + // + // The kernel-mode range is left as it was, since 8 and 10 map to the user partition for a + // kernel caller and there's no hardware recording from kernel mode to check it against - a + // test PRX built on this suite's common code doesn't fit in the kernel partition alongside + // PSPLink. + const u32 highestPartition = hleIsKernelMode() ? 9 : 6; + if (partition < 1 || partition == 7 || partition > highestPartition) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ARGUMENT, "invalid partition %d", partition); BlockAllocator *allocator = BlockAllocatorFromID(partition); diff --git a/Core/HLE/sceMd5.cpp b/Core/HLE/sceMd5.cpp index 6abae8b5ca..7f014d864b 100644 --- a/Core/HLE/sceMd5.cpp +++ b/Core/HLE/sceMd5.cpp @@ -52,8 +52,8 @@ u32 sceKernelUtilsMt19937UInt(u32 ctx) { // can be in flight at once. Layout confirmed against a real PSP by pspautotests hash/md5ctx: // 96 bytes, and the word at offset 16 is never written by the kernel. // -// sceKernelUtilsSha1Block* below still keeps one global state, for the same reason this used to - -// there's no hardware recording of the SHA-1 context layout yet. +// SHA-1 gets the same treatment further down. Its context is the same size with the same +// bookkeeping, but note it does not stream whole blocks through buf the way MD5 does. struct PSPMd5Context { u32_le h[4]; u32_le pad; // the kernel leaves this one alone @@ -191,7 +191,55 @@ int sceKernelUtilsMd5BlockResult(u32 ctxAddr, u32 digestAddr) { } -static sha1_context sha1_ctx; +// SHA-1's context, confirmed against a real PSP by pspautotests hash/sha1ctx. Same 96 bytes and +// same bookkeeping as MD5, but no pad word - and unlike MD5, a whole-block update leaves buf +// alone rather than copying the block through it, which is what our sha1 does anyway. +struct PSPSha1Context { + u32_le h[5]; + u16_le usRemains; + u16_le usComputed; + u64_le ullTotalLen; + u8 buf[64]; +}; + +static void Sha1ContextRead(const PSPPointer &ctx, sha1_context *out) { + for (int i = 0; i < 5; i++) { + out->state[i] = ctx->h[i]; + } + u64 total = ctx->ullTotalLen; + out->total[0] = (u32)total; + out->total[1] = (u32)(total >> 32); + memcpy(out->buffer, ctx->buf, sizeof(out->buffer)); +} + +static void Sha1ContextWrite(PSPPointer &ctx, const sha1_context *in) { + for (int i = 0; i < 5; i++) { + ctx->h[i] = (u32)in->state[i]; + } + u64 total = (u64)(u32)in->total[0] | ((u64)(u32)in->total[1] << 32); + ctx->ullTotalLen = total; + ctx->usRemains = (u16)(total & 0x3F); + ctx->usComputed = 0; + memcpy(ctx->buf, in->buffer, sizeof(ctx->buf)); + ctx.NotifyWrite("Sha1Context"); +} + +static int Sha1BlockInit(u32 ctxAddr) { + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid()) + return hleLogError(Log::HLE, -1, "bad context address"); + sha1_context fresh; + sha1_starts(&fresh); + for (int i = 0; i < 5; i++) { + ctx->h[i] = (u32)fresh.state[i]; + } + ctx->usRemains = 0; + ctx->usComputed = 0; + ctx->ullTotalLen = 0; + ctx.NotifyWrite("Sha1Context"); + return hleLogDebug(Log::HLE, 0); +} + int sceKernelUtilsSha1Digest(u32 dataAddr, int len, u32 digestAddr) { DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1Digest(%08x, %d, %08x)", dataAddr, len, digestAddr); @@ -204,34 +252,29 @@ int sceKernelUtilsSha1Digest(u32 dataAddr, int len, u32 digestAddr) { } int sceKernelUtilsSha1BlockInit(u32 ctxAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1BlockInit(%08x)", ctxAddr); - if (!Memory::IsValidAddress(ctxAddr)) - return -1; - - // TODO: Until I know how large a context is, we just go all lazy and use a global context, - // which will work just fine unless games do several MD5 concurrently. - - sha1_starts(&sha1_ctx); - - return 0; + return Sha1BlockInit(ctxAddr); } int sceKernelUtilsSha1BlockUpdate(u32 ctxAddr, u32 dataAddr, int len) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1BlockUpdate(%08x, %08x, %d)", ctxAddr, dataAddr, len); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(dataAddr)) - return -1; - - sha1_update(&sha1_ctx, Memory::GetPointerWriteUnchecked(dataAddr), (int)len); - return 0; + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(dataAddr, len)) + return hleLogError(Log::HLE, -1, "bad address"); + sha1_context work; + Sha1ContextRead(ctx, &work); + sha1_update(&work, Memory::GetPointerWriteUnchecked(dataAddr), (int)len); + Sha1ContextWrite(ctx, &work); + return hleLogDebug(Log::HLE, 0); } int sceKernelUtilsSha1BlockResult(u32 ctxAddr, u32 digestAddr) { - DEBUG_LOG(Log::HLE, "sceKernelUtilsSha1BlockResult(%08x, %08x)", ctxAddr, digestAddr); - if (!Memory::IsValidAddress(ctxAddr) || !Memory::IsValidAddress(digestAddr)) - return -1; - - sha1_finish(&sha1_ctx, Memory::GetPointerWriteUnchecked(digestAddr)); - return 0; + auto ctx = PSPPointer::Create(ctxAddr); + if (!ctx.IsValid() || !Memory::IsValidRange(digestAddr, 20)) + return hleLogError(Log::HLE, -1, "bad address"); + sha1_context work; + Sha1ContextRead(ctx, &work); + sha1_finish(&work, Memory::GetPointerWriteUnchecked(digestAddr)); + Sha1ContextWrite(ctx, &work); + return hleLogDebug(Log::HLE, 0); } diff --git a/pspautotests b/pspautotests index 1c4a78d809..9ace5419a8 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 1c4a78d809888ee6b947e89d4fb14b92c09c5ade +Subproject commit 9ace5419a8cbe11b10bae3d3d3763d15d404f848 diff --git a/test.py b/test.py index c2fd568af2..9177f934b3 100755 --- a/test.py +++ b/test.py @@ -212,6 +212,7 @@ tests_good = [ "hash/hash", "hash/md5ctx", "hash/mt19937ctx", + "hash/sha1ctx", "hle/check_not_used_uids", "intr/intr", "intr/enablesub", @@ -339,6 +340,7 @@ tests_good = [ "threads/threads/threadmanidtype", "threads/threads/threads", "threads/tls/create", + "threads/tls/partition", "threads/tls/delete", "threads/tls/get", "threads/tls/free", From a676eecba7a946716e167fd66b0badc902a09699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 8 Sep 2026 15:07:15 -0600 Subject: [PATCH 3/5] Tlspl partitions: 1-6 in both privilege levels, and document kernel-mode tests threads/tls/kernel/partition now records the sweep from a kernel module, which settles the range question the user-mode recording couldn't: privilege changes the permission check, not the range. Partitions 1, 3 and 4 are ILLEGAL_PERM from user mode and fine from kernel mode, while 7 and up are ILLEGAL_ARGUMENT either way. So the check goes back to a plain 1-6 for both, and the kernel-mode carve-out from the last commit - which would have let 8 and 9 through - is gone. The hardware doc gains a section on kernel-mode tests: what COMMON_KERNEL does, why the stock crt0 makes a kernel PRX unloadable, which libraries can't be imported, and how much room there actually is in the kernel partition. --- Core/HLE/sceKernelMemory.cpp | 17 +++++-------- docs/pspautotests-hardware.md | 47 +++++++++++++++++++++++++++++++++++ pspautotests | 2 +- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index 47e312a480..b9c875fd71 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -1863,17 +1863,12 @@ SceUID sceKernelCreateTlspl(const char *name, u32 partition, u32 attr, u32 block return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_NO_MEMORY, "invalid name"); if ((attr & ~PSP_TLSPL_ATTR_KNOWN) >= 0x100) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ATTR, "invalid attr parameter: %08x", attr); - // From user mode only 1-6 exist: threads/tls/partition records 7 and up all returning - // ILLEGAL_ARGUMENT on a real PSP, where 1, 3 and 4 fall through to ILLEGAL_PERM below. Note - // this differs from sceKernelCreateVpl above, which does let 8 and 9 reach the permission - // check - the two used to share a range that was only ever right for Vpl. - // - // The kernel-mode range is left as it was, since 8 and 10 map to the user partition for a - // kernel caller and there's no hardware recording from kernel mode to check it against - a - // test PRX built on this suite's common code doesn't fit in the kernel partition alongside - // PSPLink. - const u32 highestPartition = hleIsKernelMode() ? 9 : 6; - if (partition < 1 || partition == 7 || partition > highestPartition) + // Only 1-6 exist, in either privilege level: threads/tls/partition and its kernel-mode twin + // both record 7 and up returning ILLEGAL_ARGUMENT on a real PSP. What the privilege changes + // is the permission check below - 1, 3 and 4 are ILLEGAL_PERM from user mode and fine from + // kernel mode. Note this differs from sceKernelCreateVpl above, which lets 8 and 9 reach the + // permission check; the two used to share a range that was only ever right for Vpl. + if (partition < 1 || partition > 6) return hleLogWarning(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ARGUMENT, "invalid partition %d", partition); BlockAllocator *allocator = BlockAllocatorFromID(partition); diff --git a/docs/pspautotests-hardware.md b/docs/pspautotests-hardware.md index 7597c2480b..453c9e306d 100644 --- a/docs/pspautotests-hardware.md +++ b/docs/pspautotests-hardware.md @@ -174,6 +174,53 @@ python3 test.py --graphics=software io/shortname/shortname - **The module must be named `TESTMODULE`** (`common.c` does this) or `gentest.py` prints the load line as an unexpected result. +## Kernel-mode tests + +Some behaviour differs by privilege - `sceKernelCreateTlspl` accepts partitions 1, 3 and 4 from a +kernel module and rejects them with `ILLEGAL_PERM` from user mode - so occasionally a test has to +run as a kernel module. `tests/threads/tls/kernel` is the working example. Set `COMMON_KERNEL = 1` +in the Makefile before including `common.mk` and you get a kernel build: + +```make +TARGETS = partition +EXTRA_OBJS = tlspl-imports.o +COMMON_KERNEL = 1 + +COMMON_DIR = ../../../../common +include $(COMMON_DIR)/common.mk +``` + +Three things make this work, and all three took a while to find, so don't undo them casually: + +- **`USE_KERNEL_LIBS`, set automatically by `common.mk`.** The stock `crt0_prx.o` references + `__libcglue_init`, which pulls in the whole of libcglue, which imports `sceNetInet`, + `sceUtility` and the `ForUser` IO libraries. A kernel module that imports any of those fails to + load with `8002013C` (library not found), and no amount of trimming `LIBS` helps because the + reference comes from the startup object itself. `USE_KERNEL_LIBS` selects a different startup - + and brings `-nostdlib`, so `common/kernelglue.c` supplies the newlib support hooks that go + missing: `_sbrk` over a fixed heap, the `_read`/`_write`/`_close` stubs, no-op retargetable + locks, and `module_start`. +- **Nothing may import a `ForUser` library.** `ThreadManForUser` is fine, but `SysMemUserForUser` + is not, which is why `common.c` compiles the `sceKernelSetCompiledSdkVersion` helpers out of + kernel builds - a function pointer table referencing them is enough to pull the stubs in. + Watch for this: `psp-strings yourtest.prx | grep For` lists what you're importing. Beware also + that `ThreadManForKernel` does not export the Tlspl calls - importing them from there links + cleanly and then returns `8002013A`, library not yet linked, from every call. +- **Size.** The kernel partition has about 285 KB free with PSPLink resident, 242 KB of it + contiguous, and loading over the cable needs room for the file *and* the loaded image at once. + A kernel build therefore gets a 4 KB `schedfBuffer` and a 4 KB heap instead of the user build's + 64 KB and 21 MB. `psp-size yourtest.elf` plus the `.prx` file size against `meminfo`'s MAXFREE + tells you whether it will fit; `800200D9` (memblock alloc failed) means it didn't. + +Output works normally - `printf`, `checkpoint` and `schedf` all behave - but a kernel build writes +to `host0:` with `sceIo` directly rather than through newlib's `FILE`, since that's what dragged +libcglue in. Formatting still goes through newlib's `vsnprintf`, so the usual format specifiers +are all available. + +**PPSSPP can't run these yet.** `PPSSPPHeadless` times out on a kernel PRX built this way, so a +kernel-mode test can't go in `test.py` - keep it as a hardware reference and diff it by hand +against its user-mode twin. + ## Worked example: FAT short names `tests/io/shortname` was written this way and is a decent template. It creates a scratch directory diff --git a/pspautotests b/pspautotests index 9ace5419a8..926360d556 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 9ace5419a8cbe11b10bae3d3d3763d15d404f848 +Subproject commit 926360d5567fbf94c1605cd5ff7e91ef86f9f4cf From 41144e3b35c62632be46433a0d2f4a5c6b683c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 8 Sep 2026 15:12:53 -0600 Subject: [PATCH 4/5] Give the memory partitions the caller's privilege, not the syscall's PPSSPP decided whether a caller was privileged with hleIsKernelMode(), which reports whether the syscall being executed is itself a kernel-only export. That's a different question from the one the hardware answers: on a PSP the privilege belongs to the calling module, and a kernel module reaches sceKernelCreateTlspl through the ordinary ThreadManForUser NID like anything else. So a kernel module asking for partition 1, 3 or 4 got ILLEGAL_PERM where a real PSP hands it over, which the new threads/tls/kernel/partition test shows directly. BlockAllocatorFromID now also accepts a caller whose thread belongs to a kernel module, via a new __KernelCurThreadIsKernelMode(). It checks the thread's own attribute first and then the owning module, because a kernel module's main thread isn't necessarily flagged kernel - the attribute comes from PSP_MAIN_THREAD_ATTR, which needn't set it. That mirrors how sceKernelCreateThread already works out allowKernel. This only ever widens access, and only for threads belonging to kernel modules, so games are unaffected - they run in user modules and see exactly what they saw before. --- Core/HLE/sceKernelMemory.cpp | 10 ++++++++-- Core/HLE/sceKernelThread.cpp | 11 +++++++++++ Core/HLE/sceKernelThread.h | 4 ++++ docs/pspautotests-hardware.md | 13 ++++++++++--- pspautotests | 2 +- test.py | 1 + 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index b9c875fd71..ac0ec377ff 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -390,11 +390,17 @@ void __KernelMemoryShutdown() } BlockAllocator *BlockAllocatorFromID(int id) { + // A kernel module gets the privileged partitions whichever entry point it came in through - + // threads/tls/kernel/partition records sceKernelCreateTlspl accepting 1, 3 and 4 from a + // kernel module and refusing them from user mode, and it reaches the kernel through the + // ordinary ThreadManForUser NID either way. hleIsKernelMode() alone only catches the case + // where the export itself is kernel-only. + const bool kernelMode = hleIsKernelMode() || __KernelCurThreadIsKernelMode(); switch (id) { case KERNEL_PARTITION_ID: case 3: case 4: - if (hleIsKernelMode()) + if (kernelMode) return &kernelMemory; return nullptr; @@ -404,7 +410,7 @@ BlockAllocator *BlockAllocatorFromID(int id) { case 8: case 10: - if (hleIsKernelMode()) + if (kernelMode) return &userMemory; return nullptr; diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index bd139eede5..260c349f61 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -2290,6 +2290,17 @@ SceUID __KernelGetCurThreadModuleId() { return 0; } +bool __KernelCurThreadIsKernelMode() { + PSPThread *thread = __GetCurrentThread(); + if (thread && (thread->nt.attr & PSP_THREAD_ATTR_KERNEL) != 0) { + return true; + } + // A kernel module's own main thread isn't necessarily flagged kernel - the attribute comes + // from PSP_MAIN_THREAD_ATTR in the module info, which needn't set it - so fall back to what + // module the thread belongs to, the same way sceKernelCreateThread decides allowKernel. + return KernelModuleIsKernelMode(__KernelGetCurThreadModuleId()); +} + u32 __KernelGetCurThreadStack() { PSPThread *t = __GetCurrentThread(); if (t) diff --git a/Core/HLE/sceKernelThread.h b/Core/HLE/sceKernelThread.h index 146c3bb5f5..2edcff09fe 100644 --- a/Core/HLE/sceKernelThread.h +++ b/Core/HLE/sceKernelThread.h @@ -341,6 +341,10 @@ KernelObject *__KernelCallbackObject(); SceUID __KernelGetCurThread(); int KernelCurThreadPriority(); bool KernelChangeThreadPriority(SceUID threadID, int priority); +// Whether the running thread belongs to a kernel module. Privilege on the PSP is a property of +// the caller, not of the syscall - hleIsKernelMode() only says the entry point itself is a +// kernel-only export, which is a different question. +bool __KernelCurThreadIsKernelMode(); u32 __KernelGetCurThreadStack(); u32 __KernelGetCurThreadStackStart(); const char *__KernelGetThreadName(SceUID threadID); diff --git a/docs/pspautotests-hardware.md b/docs/pspautotests-hardware.md index 453c9e306d..67329f07a4 100644 --- a/docs/pspautotests-hardware.md +++ b/docs/pspautotests-hardware.md @@ -217,9 +217,16 @@ to `host0:` with `sceIo` directly rather than through newlib's `FILE`, since tha libcglue in. Formatting still goes through newlib's `vsnprintf`, so the usual format specifiers are all available. -**PPSSPP can't run these yet.** `PPSSPPHeadless` times out on a kernel PRX built this way, so a -kernel-mode test can't go in `test.py` - keep it as a hardware reference and diff it by hand -against its user-mode twin. +Output reaches an emulator the same way it reaches the cable: a kernel build feeds +`sceIoDevctl("emulator:", SEND_OUTPUT)` as well as writing the file, and calls +`sceKernelExitGame` at the end so headless stops rather than spinning to its timeout. Both are +easy to forget when writing a new harness - without the first the test appears to produce nothing, +and without the second it always reports TIMEOUT even though it ran. + +One emulator-side note, since it took a while to pin down: privilege on the PSP belongs to the +*caller*, not to the syscall. PPSSPP's `hleIsKernelMode()` only reports whether the entry point +itself is a kernel-only export, so a kernel module calling an ordinary `ForUser` NID used to look +like user mode. `__KernelCurThreadIsKernelMode()` answers the question this test needs. ## Worked example: FAT short names diff --git a/pspautotests b/pspautotests index 926360d556..7c5d150d40 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit 926360d5567fbf94c1605cd5ff7e91ef86f9f4cf +Subproject commit 7c5d150d40ab585d8bc1b5f60f5673eb56620ac9 diff --git a/test.py b/test.py index 9177f934b3..5caaed33a2 100755 --- a/test.py +++ b/test.py @@ -341,6 +341,7 @@ tests_good = [ "threads/threads/threads", "threads/tls/create", "threads/tls/partition", + "threads/tls/kernel/partition", "threads/tls/delete", "threads/tls/get", "threads/tls/free", From 1e2eaa1690df1c9b3801285ca274a0dad963a3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 8 Sep 2026 15:26:26 -0600 Subject: [PATCH 5/5] MacOS: Add shortcut to load the VSH --- SDL/CocoaBarItems.mm | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/SDL/CocoaBarItems.mm b/SDL/CocoaBarItems.mm index aedfdcb271..0306d2b915 100644 --- a/SDL/CocoaBarItems.mm +++ b/SDL/CocoaBarItems.mm @@ -267,6 +267,13 @@ void OSXOpenURL(const char *url) { openMemstickFolderItem.target = self; [self.fileMenu addItem:openMemstickFolderItem]; + [self.fileMenu addItem:[NSMenuItem separatorItem]]; + + NSMenuItem *loadVSH = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Load VSH") action:@selector(loadVSH) keyEquivalent:@""]; + loadVSH.enabled = YES; + loadVSH.target = self; + [self.fileMenu addItem:loadVSH]; + return menu; } @@ -448,12 +455,12 @@ void OSXOpenURL(const char *url) { } -(void)pauseAction: (NSMenuItem *)item { - System_PostUIMessage(UIMessage::REQUEST_GAME_PAUSE); + System_PostUIMessage(UIMessage::REQUEST_GAME_PAUSE); } -(void)resetAction: (NSMenuItem *)item { System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); - Core_Resume(); + Core_Resume(); } -(void)chatAction: (NSMenuItem *)item { @@ -494,7 +501,7 @@ void OSXOpenURL(const char *url) { } -(void)saveFrameDump { - System_PostUIMessage(UIMessage::SAVE_FRAME_DUMP); + System_PostUIMessage(UIMessage::SAVE_FRAME_DUMP); } -(void)takeScreenshot { @@ -657,6 +664,10 @@ TOGGLE_METHOD(FullScreen, g_Config.bFullScreen, g_Config.bFullScreen = !g_Config [NSWorkspace.sharedWorkspace openURL:memstickURL]; } +-(void)loadVSH { + System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, (g_Config.nandRootDirectory / "flash0/vsh/module/vshmain.prx").ToString()); +} + - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; }