6.8 KiB
sceKernel HLE review notes
Notes from a review pass over Core/HLE/sceKernel*.cpp (~16k lines across 14 files), done
2026-09-03. Kept mostly so the next person doesn't re-derive the same conclusions - the
"verified clean" section is as much the point as the findings.
Cross-referenced against JPCSP (../JPCSP) where the correct behaviour wasn't obvious from
the PSP docs.
Bug classes that turned up
Each of these appeared in more than one place, so they're worth grepping for when touching this code:
Timeout writeback without a clamp. CoreTiming::UnscheduleEvent returns the scheduled time
minus the current time, which is negative when the event is overdue but not yet processed - the
exact situation when a wait is satisfied around its own timeout. Nine of ten sites wrote
(u32)cyclesToUs(cyclesLeft) straight into the game's timeout variable. Now funnelled through
HLEKernel::WriteRemainingTimeout in KernelWaitHelpers.h; the two thread-end sites keep their own
copy because they unschedule even when the game passed no timeout pointer.
Addresses validated, extents not. Memory::IsValidAddress checks a single byte.
KernelImportModuleFuncs used it for nidData and varData and then indexed those arrays
numFuncs (u16) and numVars (u8) times; sysclib_strcpy used it for a destination it then wrote
strlen(src) + 1 bytes into. Use IsValidRange with the size you're actually going to touch.
Unbounded walks over game-supplied data. The module variable relocation list was scanned until
it happened to hit a zero word; the mbx packet list was walked until it came back around to its
head. Both follow only valid pointers, so nothing faults - they just run off the end of memory or
spin forever. Bound them by what's mapped (Memory::ClampValidSizeAt) or by the count the object
already tracks.
Overflow checks that don't match the arithmetic they guard. sceKernelCreateFpl and
sceKernelCreateTlspl validate blockSize * count at 4-byte alignment, but then allocate using an
alignment the caller chooses, so the aligned total can still wrap - allocating a small block while
the block count stays huge, and later handing out address + block * alignedSize outside it.
Functions that return a pointer, returning an error code. sceKernelAllocHeapMemory passed
BlockAllocator::Alloc's (u32)-1 straight out, and returned UID error codes for a bad heap id.
JPCSP documents the contract as "the address of the allocated memory block, or NULL on error", so
every failure has to be 0 or callers checking for null see success.
Verified clean - don't re-flag these
readyCallbacksCountbookkeeping. There's a self-suspecting"became negative"report in__KernelCheckCallbacks, but the invariant holds. The increment (only on the 0 -> nonzero transition in__KernelNotifyCallback) is matched by three decrements: running a callback (__KernelRunCallbackOnThreadzeroesnotifyCountfirst),sceKernelDeleteCallback, and thread deletion. The thread-deletion path looks like it should double-decrement with a latersceKernelDeleteCallback, butPSPThread::Cleanup()destroys the callbacks immediately after the decrement loop, so the second lookup fails.u32 error;uninitialized beforekernelObjects.Get, thenif (error) return;. Safe -GetwritesoutErroron both paths, 0 on success.- Kernel object name copies. Every
strncpy(x.name, name, KERNELOBJECT_MAX_NAME_LENGTH)writes into achar[KERNELOBJECT_MAX_NAME_LENGTH + 1]and follows with an explicit NUL. Correct. strncpy(moduleName, modinfo->name, ARRAY_SIZE(module->nm.name))insceKernelModule.cppmixes two buffers' sizes but is safe: 28 into a zero-initialised 29.- VPL and FPL allocation failure handling. Both check for
(u32)-1properly; the heap was the outlier. - The unchecked
ReadUnchecked_U32sites insceKernelThread.cpp(register restore, extended stack restore, the exit-callback parameter area) all haveIsValid4AlignedRangechecks a few lines above. sceKernelCreateHeap'salloc.Init(address + 128, size - 128)looks like it underflows for a heap smaller than 128 bytes, but can't:BlockAllocator::AllocAlignedrounds the caller's size variable up by reference and every partition allocator is constructed with grain 256.- VTimer scheduling arithmetic. The
base + schedule - currentu64->s64 conversion wraps negative on underflow, which thegoalUs < minGoalUsclamp catches.
Latent, worth knowing about
__KernelSendMsgPipe / __KernelReceiveMsgPipe have two shapes of transfer loop. The buffered one
explicitly breaks when a transfer would move zero bytes; the unbuffered ones (bufSize == 0) don't -
they only make progress inside if (bytesToSend > 0), so a queued waiting thread with a zero
freeSize would spin forever. That can't happen today because every AddSendWaitingThread /
AddReceiveWaitingThread call site is guarded by a size != 0 check, but the invariant lives two
call sites away from the loop that depends on it.
Open, deliberately not changed
__KernelStartThreaddoesn't boundargSize.sp -= (argSize + 0xf) & ~0xfwithargSizeup toINT_MAXpushes the stack pointer below the thread's stack. The copy itself goes through the validatingMemory::Memcpy, so this is guest-side corruption only, and real hardware probably doesn't validate it either - changing it risks breaking games that depend on that. TheargSize + 0xfsigned overflow is UB regardless and deserves a cast.- Three unchecked
__GetCurrentThread()dereferences:__KernelCurHasReadyCallbacks,__KernelWaitCallbacksCurThread,KernelRotateThreadReadyQueue. Of 40 uses in that file the rest check, assert, or use a ternary - and__KernelWaitCurThread, the direct sibling of the second one, has_assert_(thread != nullptr). No path was found where a syscall runs without a current thread, so these were left alone rather than papering over a real invariant.
Coverage
Read closely: Heap, Semaphore, Alarm in full; Module's import path; Interrupt's sysclib
block; Memory's pool creation and PartitionMemoryBlock; sceKernel.cpp's object pool and
savestate loop; Thread's start/refer paths and the callback machinery; Mbx's message list;
VTimer's scheduling.
Audited across all 14 files by pattern: unchecked memory access, timeout writeback, null checks
after kernelObjects.Get, name copies, allocator return values, uninitialised members.
Also checked: MsgPipe's transfer loops, and the interrupt dispatch in sceKernelInterrupt.cpp
(__TriggerInterrupt indexes intrHandlers without a bounds or null check, unlike
__RunOnePendingInterrupt, but every caller passes an internal constant).
Not read line by line: the EventFlag / Mutex wait-queue bodies, and thread scheduling proper.