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 <[email protected]>
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.
The s > 2 branch in PSPSaveDialog::DoState has been dead code since it
was written - the section version was never raised past 2, so
ioThreadStatus was reset to SAVEIO_NONE on every savestate load.
Loading a state that was saved while a savedata operation was in
flight then repeated the operation (or left the dialog waiting on a
completion that had already happened), instead of resuming from the
recorded status.
Restoring the value is safe: DoState joins the IO thread before
serializing, so the stored status is only ever SAVEIO_NONE or
SAVEIO_DONE, and the operation's effects are already part of the
serialized emulated RAM. Old (v2) states still load through the reset
path.
The headless build's own --debugger=PORT never actually worked on
Windows: headless/Headless.cpp never called net::Init() (WSAStartup),
so socket binding silently failed ("Unable to listen on any port
(debugger - webserver)"). Fixed by calling net::Init()/net::Shutdown()
there, gated on --debugger being passed since headless has no other use
for networking.
With that confirmed working, move parsing into the shared
Core/CmdLine.cpp auto-param table as a single --debugger=PORT option
(0 = pick automatically) available on both CmdLineMode::Application and
CmdLineMode::Headless, replacing the previous app-only boolean
--debugger flag and headless's separate ad hoc argv scan for it.
Behavior differs deliberately by build, same as before:
- App: just starts the debugger, game boots and runs normally.
- Headless: also forces coreParameter.startBreak = true (break before
anything runs), as it always has. Headless re-applies iRemoteISOPort
after its own g_Config.RestoreDefaults() call, which runs after
ApplyToConfig() and would otherwise wipe the requested port.
Verified live: headless now binds the requested port and responds to
cpu.status/game.status; the app build binds the requested port and
runs the game normally (not paused).
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
Reverse-engineering workflows need to (1) find where an unknown value lives
in memory and (2) label what's found, neither of which the debugger API
could do before:
- memory.search (MemorySubscriber.cpp): Cheat-Engine-style scan of a memory
range for a u8/u16/u32/float value, or a byte pattern with an optional
wildcard mask.
- hle.data.list/add/remove/rename (HLESubscriber.cpp): manage ST_DATA
symbols (structs, tables, buffers), mirroring the existing hle.func.*
commands for functions. Needed a new SymbolMap::RemoveData, since only
RemoveFunction existed - added following the same pattern.
Verified live against a running PPSSPP instance (game.status, cpu.stepping,
memory.search in u32/bytes/masked-bytes modes, and the full
add/list/rename/remove data-symbol lifecycle) via Tools/wsdbg.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
Application-mode only (CmdLineMode::Application) - the headless build has
its own separate --debugger=PORT mechanism in headless/Headless.cpp, which
doesn't currently work reliably, so this new flag intentionally does not
apply there. Sets bRemoteDebuggerOnStartup for the session without
persisting it to the config file.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6