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.
EP_MAP_STRIDE * EPMapEntriesNum was computed in 32-bit and could wrap,
passing the range check while the loop read the unwrapped count; and the
check was skipped entirely when headerOffset == 0 (the player tempbuf
path).
- Use 64-bit math for the EP map size.
- Keep the guest-RAM range check when headerOffset != 0.
- Cap the entry count for the headerOffset == 0 path so the reads stay
within the player's 64KB tempbuf.
ReadCharGlyph read glyph metadata at an attacker-controlled bit offset
with no validation, and ReadShadowGlyph only checked after delegating to
it. Validate charPtr (with margin for the header reads) at the top of
ReadCharGlyph so it is safe regardless of caller, and drop the now
redundant checks at the ReadPtr call sites.
PGF::ReadPtr walked four length-prefixed tables and computed table sizes
before any bounds check, and used signed 32-bit size math that could
overflow, allowing a crafted font to read past the input buffer.
- Validate the total size of all tables up front using 64-bit math.
- Check the rev3 extra header fits before reading it.
- Cap charPointerLength/charMapLength/shadowMapLength to avoid absurd
allocations.
- Bounds-check glyph data offsets before reading each glyph.
Also throw in a warning fix
A malicious or MITM'd server could send a Content-Range header matching
the requested range but a larger entity body, overflowing the caller's
fixed-size buffer via output.Take. Clamp the copied size to the requested
range.
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.
The import debug reporter used IsValidAddress (start-address only) before
formatting module names with %s, so a crafted unterminated name could be
read past guest RAM. Use IsValidNullTerminatedString instead.
- InitContextFromTrackInfo now rejects files where sampleSize (from
blockAlign) exceeds the buffer size, instead of only clamping later in
DecodeForSas. Keep the DecodeForSas check as defense-in-depth since a
large buffer could still allow a crafted packet to overflow the fixed
assembly buffer.
- CChunkFileReader::Verify now returns ERROR_BROKEN_STATE if bounds
checking fails, so modified savestates are rejected here too.
PointerWrap tracked no end-of-buffer, so DoState() implementations could
read past the end of a crafted or truncated savestate via DoVoid's
unchecked memcpy, and DoVector could resize to an attacker-controlled
size before reading.
- PointerWrap now tracks a read end; DoVoid/ExpectVoid fail (MODE_NOOP)
before reading out of bounds.
- String reads are bounds-checked for the whole string including NUL.
- DoVector rejects sizes that can't fit in the remaining buffer.
- LoadPtr takes the buffer size and sets the read end.
- Capping the decompression buffer allocation in LoadFile.
The loop-with-trailer streaming path copied 'secondBufferByte % sampleSize'
bytes into the main buffer with no clamp. sampleSize is file-derived, so a
crafted value could overflow the destination. Clamp the copy length to
info.bufferByte.
DISC_ID/DISC_VERSION/homebrewName are attacker-controlled and were used
unsanitized to build migration rename paths. Skip the migration step if
any of them contain a '/' or '\' so the destination can't escape the
savestate directory.
ReadSFO dereferenced index table entries without checking the table fit
within the buffer, and GetDataOffset had no bounds checks at all (reading
attacker-controlled offsets and strcmp'ing without a terminator guard).
- Validate the index table fits entirely within the buffer in ReadSFO.
- Add a size parameter to GetDataOffset and validate the index table,
key/data table positions, and key string termination before use.
The LZRC decompressor's only bounds check for output (and input) was a
debug-only _dbg_assert_msg_, which is a no-op in release builds. The
NPDRM demo block device also passed a hardcoded 1 MiB output length
while the real destination buffer (blockBuf_) could be as small as 2048
bytes, allowing a crafted NPDRM image to trigger an unbounded heap
overflow during game load.
Changes:
- rc_putbyte/rc_getbyte now enforce real bounds and set an error flag
instead of relying on debug asserts; decompression aborts with -1 on
overflow or truncated input.
- normalize() reads via rc_getbyte so it stays in bounds.
- Plain-text path clamps the copy size to both the output buffer and the
remaining input (and no longer interprets the size as signed).
- NPDRMDemoBlockDevice::ReadBlock passes blockSize_ (the real buffer
size) instead of 0x00100000 to lzrc_decompress.
- Add unittest/TestLzrc (synthetic input, no test data files): checks the
plain-text clamp, truncated input, and output overflow all fail safely.
- AGENTS.md: note to reuse existing format handlers/decompressors before
writing new ones.
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.
A crafted .cso compressed ISO could trigger a heap buffer overflow on any
sector read, reachable via ordinary gameplay.
Two root causes:
1. Unvalidated frame index deltas. ReadBlock/ReadBlocks computed a
compressed read range from two adjacent frame-index-table entries.
With non-monotonic entries, compressedReadEnd - compressedReadPos
underflows as u64, producing a huge read size that fileLoader->ReadAt()
wrote into the fixed-size readBuffer.
2. hdr.align (indexShift, 0-255) used as '1 << indexShift' was undefined
behavior at >= 32, and frameSize + (1 << indexShift) could wrap,
undersizing the buffer while inflate() was configured with the full
frameSize.
Fixes:
- Validate the index table is monotonically non-decreasing in the
constructor.
- Reject files with indexShift > 20.
- Use unsigned shift for buffer size math and store readBufferSize.
- Clamp compressed read sizes to readBufferSize in both ReadBlock and
ReadBlocks.
Added KernelModuleAddressDescription() (Core/HLE/sceKernelModule.cpp),
which looks up which currently loaded module (and text/data/bss/segment
section within it) an address falls in, e.g. "EBOOT.BIN.text+1234".
Wired it into:
- Core_MemoryException/Core_ExecException/Core_BreakException
(Core/Core.cpp), appended next to every address/pc/ra shown in their
log lines.
- FormatStackTrace (Core/MemFault.cpp), appended per-frame next to the
existing symbol description.
This makes crash/exception logs actionable even when there's no symbol
at the faulting address - you at least get which module and section
it's in, useful for reverse engineering unfamiliar code.
Verified live via headless: injected a MIPS break instruction at the
current PC (through Tools/wsdbg) and confirmed the log line changed from
"break instruction hit at 088040ac" to "break instruction hit at 088040ac
[sceDisplayWaitVblank Test.text+ac]".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
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.
FixedSizeQueue::DoState serializes the entire fixed backing store. For
the sceAudio channel queues that is 512KB per channel (32768*8 s16
samples), or ~4.6MB of mostly dead bytes in every savestate across the
nine channels - the live sample count at any moment is normally a few
KB. This addresses the existing TODO in DoState.
Add DoStateCompact(), which stores only the live [head, head+count)
region and restores it linearized at the front of storage. A wrapped
live region is written as its two pieces in pop order; since the POD
DoArray path writes raw bytes with no per-element or per-call header,
the single linear read on load consumes them identically. The count is
validated on load and a bad value fails the load cleanly via
p.SetError.
AudioChannel bumps its section to v3 to use the compact form; old
states still load through the unchanged full-storage path. This shrinks
every savestate by several MB uncompressed and cuts the copy/compress
cost of each save, including the rewind feature's periodic states.
On savestate load, two sites delete a pre-load host object that owns an
HLEHelperThread without calling Forget() first, so ~HLEHelperThread runs
__KernelDeleteThread and kernelMemory.Free() with thread ids and block
addresses from before the load, against the freshly restored kernel
state:
- sceUtility: Do(p, accessThread) deletes the stale accessThread inside
DoClass before recreating it from the stream.
- scePsmf: Do(p, psmfPlayerMap) deletes every existing PsmfPlayer, and
~PsmfPlayer -> AbortFinish() deletes its finishThread raw.
When the stale id or block happens to be absent from the restored state
this only logs errors ("... does not exist" / "BlockAllocator: invalid
free"). When it has been recycled, a live thread is terminated or a
live allocation is freed, silently corrupting the loaded state. Easiest
to hit by loading a state while a savedata operation or PSMF player is
active, into a session where those ids were reused.
__IoDoState and __PsmfShutdown already Forget() before deleting; do the
same at these two sites. Worst case behavior change is a leaked
kernel-side thread record where one was previously (incorrectly)
freed.