Ioctl's ISO9660 path table read re-read sector `block` (the first
sector, already consumed by the preceding ReadBlocks) for the trailing
partial sector instead of `block + blocks`, returning duplicated data
from the start of the table instead of its actual tail.
ReadDirectory() advanced by the raw on-disk dir.size without checking
it's at least as large as the record's own header+identifier. A
crafted directory sector could set dir.size = 1 repeatedly, making the
loop reinterpret the same overlapping bytes as many separate entries -
allocating far more TreeEntry objects than the sector's actual size
should allow.
rc_bittree()/rc_number() can index bm_dist_bits up to column 50 and
bm_dist up to row 43 (when decoding a "long distance" match, i.e.
match_len > 2), but the arrays were only sized for 39 and 18
respectively. A crafted LZRC-compressed block (reachable via
NPDRMDemoBlockDevice::ReadBlock) could drive these indices out of
range, corrupting adjacent probability tables within the same
LZRC_DECODE struct via rc_bit()'s read-modify-write. Size the arrays
for the indices the algorithm can actually produce, and initialize
them via sizeof() so the memset in rc_init stays correct.
fileName is taken verbatim from the index file (only a leading slash
is stripped) and then used essentially unsanitized to build a path
under basePath (GetLocalPath() is a plain string join, unlike
MetaFileSystem::RealPath which does collapse ".." for normal game file
access). A crafted index file - these virtual-disc folders are commonly
shared/downloaded as homebrew - could use a ".." component to make
PPSSPP probe or open arbitrary host files/directories outside the
intended folder just by loading it. Reuse the existing
HasParentDirComponent() helper (already used for the same purpose in
GameManager's zip extraction) to reject such entries.
numFrames and numBlocks are derived from the same attacker-controlled
64-bit total_bytes field, but independently truncated to 32 bits using
different divisors (frameSize vs. the fixed 2048-byte block size).
With extreme total_bytes/block_size combinations the two truncations
can disagree so that numBlocks (which gates ReadBlock's bounds check)
describes more blocks than numFrames actually covers - ReadBlock then
indexes the numFrames+1-sized `index` array one or more elements past
its end. Reject any header where this could happen before allocating
anything.
The block table read from an NPDRM PBP's PSAR blob is only reversibly
XOR-scrambled, not otherwise validated, so a crafted file fully
controls table_[block].size/offset and the header's LBA/block-size
fields. Several of these were used without checks:
- table_[block].size could exceed blockSize_, causing ReadAt and the
KIRK cipher update to write past the end of blockBuf_/tempBuf_ (both
allocated as exactly blockSize_ bytes) - a heap buffer overflow.
- The block index derived from blockNumber (which can come from an
attacker-influenced /sce_lbn.../_size... raw sector open) was never
bounds-checked against numBlocks_ before indexing table_[].
- blockLBAs_ could be 0, dividing by zero both when computing
numBlocks_ and when computing the block index in ReadBlock.
- lbaSize_ could underflow if lbaEnd < lbaStart, and tableSize_
(numBlocks_ * sizeof(table_info)) was computed in 32-bit, so a large
numBlocks_ could wrap it to a small value - passing the "did we read
the whole table" check while only actually reading (and
descrambling) a small prefix, leaving the rest of the table_ array
as untouched, uninitialized heap memory that ReadBlock() would later
trust.
Reject all of these instead.
SeekFile stored a signed s32 position into the unsigned size_t seekPos,
so a negative position (e.g. from a truncating s32 cast of a large
lseek offset) wrapped seekPos to near 2^64. ReadFile's clamp arithmetic
then also wrapped, driving a memcpy from a wild pointer.
- Clamp the computed seek position to 0 in SeekFile.
- Clamp the read size against the remaining data in ReadFile, returning
0 when seekPos is at or past the end.
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 .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.
Some preserved PSP prototype builds were distributed as DVD-R images that contain the actual UMD data inside USER_L0.IMG and, for dual-layer titles, USER_L1.IMG. These images previously required manual extraction, renaming, or concatenation before PPSSPP could load them.
Add support for recognizing these DVD-R wrapper layouts and exposing the embedded UMD image through the normal disc loading path. This includes both ISO-based wrappers and UDF-based wrappers, so preserved prototype dumps can be opened directly without conversion.
This makes PPSSPP compatible with a wider range of preserved developer disc images without relying on title-specific handling.
The primary importance of this patch is to encourage preserving 1:1, perfect disc image dumps in the manner of Redump.org.
This patch has been successfully tested on the following DVD-R ISOs:
https://hiddenpalace.org/Rock_Band_Unplugged_(Dec_10,_2008_prototype)https://hiddenpalace.org/WipEout_Pulse_(May_4,_2007_prototype)https://hiddenpalace.org/Lara_Croft_Tomb_Raider:_Anniversary_(May_19,_2007_prototype)https://hiddenpalace.org/Heatseeker_(Jan_15,_2007_prototype)Fixes#15547.