# 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 - **`readyCallbacksCount` bookkeeping.** 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 (`__KernelRunCallbackOnThread` zeroes `notifyCount` first), `sceKernelDeleteCallback`, and thread deletion. The thread-deletion path looks like it should double-decrement with a later `sceKernelDeleteCallback`, but `PSPThread::Cleanup()` destroys the callbacks immediately after the decrement loop, so the second lookup fails. - **`u32 error;` uninitialized before `kernelObjects.Get`, then `if (error) return;`.** Safe - `Get` writes `outError` on both paths, 0 on success. - **Kernel object name copies.** Every `strncpy(x.name, name, KERNELOBJECT_MAX_NAME_LENGTH)` writes into a `char[KERNELOBJECT_MAX_NAME_LENGTH + 1]` and follows with an explicit NUL. Correct. - **`strncpy(moduleName, modinfo->name, ARRAY_SIZE(module->nm.name))`** in `sceKernelModule.cpp` mixes two buffers' sizes but is safe: 28 into a zero-initialised 29. - **VPL and FPL allocation failure handling.** Both check for `(u32)-1` properly; the heap was the outlier. - **The unchecked `ReadUnchecked_U32` sites** in `sceKernelThread.cpp` (register restore, extended stack restore, the exit-callback parameter area) all have `IsValid4AlignedRange` checks a few lines above. - **`sceKernelCreateHeap`'s `alloc.Init(address + 128, size - 128)`** looks like it underflows for a heap smaller than 128 bytes, but can't: `BlockAllocator::AllocAligned` rounds the caller's size variable up by reference and every partition allocator is constructed with grain 256. - **VTimer scheduling arithmetic.** The `base + schedule - current` u64->s64 conversion wraps negative on underflow, which the `goalUs < minGoalUs` clamp 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 - **`__KernelStartThread` doesn't bound `argSize`.** `sp -= (argSize + 0xf) & ~0xf` with `argSize` up to `INT_MAX` pushes the stack pointer below the thread's stack. The copy itself goes through the validating `Memory::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. The `argSize + 0xf` signed 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.