BlockAllocator backs sceKernelAllocPartitionMemory and the various partition
allocators, and had no coverage at all. It's pure address bookkeeping - no real
memory - so it can be checked hard: after any sequence of operations the blocks
must still tile the range exactly, with no gaps, overlaps or strays, and
GetTotalFreeBytes/GetLargestFreeBlockSize must agree with what's really in the
list. ValidateAllocator() rebuilds the list through the public accessors and
asserts all of that, and it runs after every step of the randomised sections.
Covers bottom-up and top-down allocation, grain rounding, rejection of zero and
oversized requests, splitting and re-merging on free, double free and free of an
address that was never allocated, AllocAt including unaligned positions and
collisions, AllocAligned with alignments coarser than the allocator's grain,
filling the range completely and draining it, a range whose size isn't a
multiple of the grain, and two randomised churn loops - one plain alloc/free,
one mixing in aligned allocations and AllocAt to reach block layouts the simple
loop never produces. Fixed seeds, so a failure reproduces.
No bugs found - the allocator holds up. Verified the tests aren't vacuous by
injecting two plausible bugs: dropping the forward merge in MergeFreeBlocks, and
an off-by-one in the bottom-up fit check. Both are caught, the second pinpointed
to a specific churn iteration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
TouchInput::id was used directly to index the global primaryButton[]
array (MultiTouchButton::Touch) and to shift pointer bitmasks
(PSPDpad/PSPStick/PSPCustomStick/GestureGamepad::Touch), guarded only
by a debug-only assert in one of the five call sites - a no-op in
release builds. input.id isn't always a small sequential slot in
[0, TOUCH_MAX_POINTERS): SDL assigns SDL_FingerID values directly,
Android pointer IDs can go up to 31, and UWP's TouchMapper allocates
one more slot (11) than TOUCH_MAX_POINTERS (10) and can also return -1
when it runs out of slots - all reachable through ordinary multi-touch
use, no malicious input required.
Also apply bounds check to the PER_GAME gesture config ints
(iDoubleTapGesture/iSwipeUp/Down/Left/Right) before indexing
GestureKey::keyList[] with them.
Additionally, minor cleanup on Android and moves the TouchMapper helper
out from UWP to InputState.h.
LoadIfNeeded() indexed lines[0] unconditionally after splitting the
bundled CSV into lines, crashing if the file were empty. It also used
GetColumnIndex()'s result directly as an index into each row's fields
without checking for its (size_t)-1 "not found" sentinel, which would
have produced a huge out-of-bounds index on every row if any of the
expected column headers were missing. Both require a corrupted or
replaced redump.csv asset to trigger, but are simple, cheap checks to
add.
GetBlockTag() dereferenced GetBlockFromAddress()'s result without a
null check, unlike every other accessor in this file. Callers
(NetAdhocCommon.cpp, sceNet.cpp, sceNetAdhocMatching.cpp) pass the
result straight into strcmp() while recovering from a stale address
left over from an old/corrupted savestate - precisely the situation
where the address may no longer resolve to a block. Return "" instead
of dereferencing a null block, so strcmp() simply reports a mismatch
(correctly triggering those callers' recovery path) instead of
crashing.
DoState() read the saved block count directly from the savestate with
no validation before looping that many times allocating Blocks - a
corrupt/malicious savestate claiming an enormous count would drive an
effectively unbounded allocation loop. Clamp it to how many block
records could plausibly still fit in the remaining stream data.
zip_get_name() returns NULL on a corrupted central directory entry;
several call sites passed its result straight into endsWith(),
std::string construction, or the fileAllowed() lambda (which calls
HasParentDirComponent()/strchr()/strrchr() on it) without checking -
undefined behavior on a malformed/malicious zip (game/homebrew/texture
pack installs, or a downloaded ISO zip).
zip_stat_index() can likewise fail, leaving its output zip_stat
uninitialized; ZipReadFileByIndex, DetectTexturePackDest, and
ExtractFile all used the result (zstat.size, to size a buffer or check
a texture-pack size limit) without checking the call succeeded first,
using an initialized zip_stat via zip_stat_init() so the failure case
reads a known-zero size instead of stack garbage that could drive a
huge buffer.resize() or bypass the texture-pack size limit check.
ZipReadFileByIndex also didn't check zip_fopen_index()'s return before
passing it to zip_fread().
These parsers run on fully game-controlled buffers (reachable via the
various sceAtracSetData*/sceAtracSetHalfwayBuffer* HLE calls), so a
malicious/malformed game can supply arbitrary bytes here:
- AnalyzeAtracTrack's RIFF chunk-walking loop computed `offset +=
chunk + (chunk & 1)` (all in 32-bit) and only bounds-checked
afterwards. A crafted chunk size could wrap `offset` (and the
`offset + 12` check itself) around, bypassing the bounds check
entirely - Read32(), whose offset parameter is a plain `int`, would
then read from a wild pointer far outside the buffer. Do the
validation in 64-bit before mutating offset, mirroring the pattern
already used by the newer ParseWaveAT3 parser.
- AnalyzeAA3Track validated `size >= tagSize + 36` but then read up to
relative index 35 after rebasing by 10+tagSize - i.e. absolute index
tagSize+45, 10 bytes past what was actually checked.
- The SMPL chunk's loop count (checkNumLoops) was only checked for
being negative, not bounded against the chunk's actual size, so a
crafted value near INT_MAX would drive an unbounded (up to ~2
billion entry) vector::resize() - an easy crash/OOM. The same
unclamped value also let the fill loop below run past the end of
the chunk, since its bound compares the loop counter to chunkSize
rather than the byte offset actually being advanced (24 bytes/loop).
Clamping checkNumLoops to what the chunk can actually hold fixes
both.
- ParseAA3Headers checked for at least 9 bytes but the "ea3"/"id3"
branch it guards reads up through byte index 9, needing 10.
ExtractZipContents wrote every entry's declared size with no ceiling, so
a small high-ratio zip could decompress to fill the storage device.
- Add a maxTotalSize parameter to ExtractZipContents (default 4GB) and
ExtractFile.
- Bail out in the size-summation pass when the total declared size
exceeds the limit, and again per write chunk in case declared sizes
are inaccurate.
AnalyzeAtracTrack used max(fileSize, size) as the chunk-parse bound with
fileSize taken from the file's RIFF header, so a crafted inflated RIFF
size could push reads past the end of the buffer. Keep the real-library
behavior of tolerating a too-low size, but clamp the parse bound to the
actual mapped guest memory at the buffer.
Also guard ParseWaveAT3's RIFF scan against a blockSize < 4 underflow
that could make the offset negative and bypass the loop bounds check, and
clamp readSize to the mapped region in Atrac2::SetData before parsing.
pmf_init reported stream dimensions without a cap, and PMFView::Draw
allocated width * height * 4 with 32-bit int arithmetic, so a crafted
ICON1.PMF could overflow the allocation to a small buffer while
sws_scale wrote the full frame.
- Reject videos with dimensions outside 1..720x480 in pmf_init (PMFs on
the PSP never exceed 720x480).
- Use size_t arithmetic for the frame buffer allocation.
A crafted zip with a parent-directory ("..") entry name could escape the
destination directory during extraction, writing arbitrary files on the
host (e.g. into startup/autostart folders). ExtractZipContents built the
output path by concatenating the raw zip entry name onto the destination
with no traversal check.
Changes:
- Add HasParentDirComponent() utility in Core/Util/PathUtil and use it in
GameManager::ExtractZipContents to reject entries with a ".." component.
Guard both the directory-creation and file-writing passes.
- Expose ExtractZipContents as public for testing.
- Add unittest/TestZipSlip which crafts a zip with a "../evil.txt" entry
and verifies it is not written outside the destination directory.
Serializing the kernel memory block lists (userMemory, kernelMemory,
volatileMemory) wrote a full Section header per block and re-zeroed
each block's tag padding with strlen+memset on every save. Games keep
on the order of a thousand blocks alive, so the per-block overhead is
both measurable save time and wasted payload bytes.
Zero-pad tags once at write time instead (Block constructor and
SetAllocated), so the v2 form can store blocks raw: start, size, taken,
tag - no per-block section machinery, no per-save tag scrubbing.
Uninitialized padding still never reaches the stream, since every path
that writes a tag now clears it first.
v1 states still load through the old per-block-Section form, which is
kept unchanged.