Commit Graph
15339 Commits
Author SHA1 Message Date
HalexxanderandClaude Opus 5 e5bd8e8e58 Add optional controller rumble on button press
New "Vibrate controller on button press" setting under Controls. When enabled,
pressing a gamepad button rumbles that pad for as long as it's held - something
the PSP hardware never had, but which some people want from a modern pad.

Implemented per platform behind System_ControllerRumbleStart/Stop: SDL uses
SDL_RumbleGamepad, Windows feeds it into XinputDevice's existing vibration path
so it doesn't fight with the vibration games request through sceCtrl, and
Android goes through InputDevice.getVibrator() in Java. Stubbed elsewhere.

The D-pad is excluded, since it gets tapped constantly while navigating.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-05 12:08:09 -06:00
Henrik Rydgård 6d4bc5f261 Merge pull request #22207 from acts-1631/security/fix-pgf-bpe-validation
Validate PGF bits-per-entry fields
2026-09-05 09:48:59 -06:00
Henrik Rydgård 8b69031197 Merge pull request #22215 from hrydgard/shutdown-crash-fixes
Misc shutdown fixes on Windows
2026-09-05 09:47:39 -06:00
Henrik Rydgård 5d608e39a3 Merge pull request #22223 from hrydgard/iso-metadata-hardening
Clamp ISO metadata and file sizes to the max possible extent
2026-09-05 09:28:29 -06:00
Henrik Rydgård 64cdcbfca2 Merge pull request #22225 from hrydgard/misc-fixes
Assortment of minor fixes
2026-09-05 09:28:10 -06:00
Henrik Rydgård f929a74780 Merge pull request #22221 from 4RH1T3CT0R7/fix/debugger-breakpoint-list-refresh
Win32 debugger: refresh the breakpoint list after memcheck changes
2026-09-04 18:17:31 -06:00
Henrik RydgårdandClaude Opus 5 2389b8ce96 headless: add --save-state, the counterpart to --state
--state could load a savestate but nothing could produce one without a
GUI, so savestate bugs couldn't be reproduced or regression-tested from
a script. This saves one partway through the run, once the game is
actually up.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:14:41 -06:00
Henrik RydgårdandClaude Opus 5 5a84f287c4 sceIoRename: refuse wildcards, an existing destination, and don't wait on XDEV
Three ways our rename differed from the PSP's:

- A wildcard in either path was passed through to the host, so
  renaming "test*.txt" could quietly rename a real file. The PSP
  doesn't expand them here, it rejects them outright.
- Renaming onto a file that already exists succeeded, because the host
  rename() replaces the destination. The PSP refuses, and renaming a
  file onto itself counts as that too.
- Crossing devices returned the right error, but after the same wait
  as everything else. The hardware fails that one immediately.

Fixes io/file/rename, moved to tests_good.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:13:10 -06:00
Henrik RydgårdandClaude Opus 5 fd72f308af sceAudio: fix a crash when reserving a channel with none free
GetFreeChannel counted down with an unsigned loop variable, so i >= 0
was always true. With every channel already reserved it wrapped past
zero and kept indexing g_audioChans until it walked off the end -
sceAudioChReserve(-1, ...) segfaulted the emulator instead of returning
"no channels available". Reproduces on audio/sceaudio/reserve, which
crashed before printing anything.

Also gives sceVaudioChReserve the parameter checks it never had. It
took any sample count, channel count and frequency; the hardware allows
256, 1024 or 2048 samples, stereo only, and the same sample rates the
SRC channel accepts. Every value in the test now matches - what's left
there is only reschedule markers.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:12:31 -06:00
Henrik RydgårdandClaude Opus 5 d675b467c1 sceUtility: fix the system param string size check and the adhoc channel error
Two things utility/systemparam caught:

A negative size passed to sceUtilityGetSystemParamString went through
Memory::IsValidRange, where it became an enormous range and came back
as a generic -1. The PSP just reports that the string doesn't fit, same
as any other size too small to hold it.

sceUtilityGetSystemParamInt returned 0x800ADF4 for an automatic adhoc
channel unconditionally. The FIXME there wondered whether the hardware
only does that once adhocctl is initialized - it does. Before any adhoc
module is up, which is the state nearly every game asks this in, the
hardware returns 0 and writes the channel out.

Fixes utility/systemparam/systemparam, moved to tests_good.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:12:24 -06:00
Artem Lytkin 20e0185707 Win32 debugger: refresh the breakpoint list after memcheck changes
The list reloads on SystemNotification::DISASSEMBLY, which every CPU
breakpoint mutation in BreakpointManager has posted since 8d0d601b5, but
none of the MemCheck ones do, so a memory breakpoint added from the list
(or the disasm view's dialog, the ImDebugger, the WebSocket API) only
showed up after the next Break. The enable checkbox then toggled against
the list's stale copy of the memcheck, so re-enabling one it had just
disabled disabled it again. Post the notification from the memcheck
mutations too, and from the condition setters of both kinds, since the
list's cached copy also feeds the edit dialog.
2026-09-05 00:54:50 +03:00
Henrik RydgårdandClaude Opus 5 fbdb54300e Put a ceiling on the CSO frame size
Follow-up to #22208, which bounded the index table but left the frame
size itself unbounded - readBuffer and zlibBuffer are sized straight
from it, so a 96-byte header could still ask for a couple of gigabytes.
Harmless enough on 64-bit, where the pages never get touched, but a
32-bit build would just fail the allocation.

Real images use 2KB to 64KB frames, so 16MB leaves plenty of headroom.
All 19 CSOs I have on hand still load.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 a506d0be6f Lowercase the extension reported by ZipFileLoader
Identify_File and friends compare against lowercase extensions, the way
Path::GetFileExtension returns them, but ZipFileLoader passed the name
from the zip through unchanged. So a file stored as e.g. "DUMP.PPDMP"
inside a zip failed to load, while "dump.ppdmp" worked.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 fbdf611fc4 Chat: keep more lines of history, and add timestamps
The log was trimmed to 50 lines, which isn't enough to scroll back
through a conversation - now 250. Chat entries also carry the time they
arrived, shown as a dimmed HH:MM in front of the name, behind a new
"Show timestamps in chat" setting.

The timestamp is kept next to the text rather than baked into it, since
the chat view finds the sender by splitting the line at the first colon.

Fixes #15444

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 eb1bc34123 Map the right analog stick by default on Android pads
The generic Android pad map and the Retroid map were the only pad
defaults without it, so the right stick did nothing until mapped by
hand. Uses the same axes and directions as the desktop pad default.

Fixes #21591

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 b5bafa43b8 Prefer upper case PSP/SHADERS and PSP/THEMES
These two were lower case for legacy reasons, unlike every other
directory, which actually matters on case sensitive file systems like
the one iOS uses. An existing lower case directory is still used if
there's no upper case one, so nobody has to move their files.

Fixes #20527

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:53 -06:00
Henrik RydgårdandClaude Opus 5 6bb91bf2f0 Remove the "Up to 2" buffered frames option
Three frames in flight showed no performance benefit over two, so drop
the option and clamp existing configs (and the default) down to 2.
The now-unused "Up to 2" translation keys are left in place.

Fixes #21419

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:08:21 -06:00
Henrik RydgårdandClaude Opus 5 1c7f393e2b Misc shutdown fixes
* Destroy the ImGui debugger explicitly, not at static destruction time

* Windows: destroy the debugger windows before NativeShutdown()

* Clear the disassembly cache where it goes stale, not in view destructors
  (behind a no-op stub for libretro, which doesn't build DisassemblyManager.cpp)

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-04 13:06:24 -06:00
Henrik Rydgård 2994145f86 ISOFileSystem: Don't copy out uninitialized stack on a failed block read
ReadFile and the read-path-table ioctl both read a sector into a stack buffer
and memcpy it to the destination without checking whether the read succeeded.
FileBlockDevice::ReadBlock returns false on a short read and leaves the buffer
untouched, so a read past the end of a truncated or crafted image copies 2KB of
uninitialized host stack into guest-visible memory.

Zero the buffer on failure, and bail out of the ioctl if the volume descriptor
can't be read instead of using a garbage path table length. The constructor
already checked that same read.

ReadBlocks writes straight into the caller's buffer, so a partial read there
leaves stale data rather than host memory - left alone deliberately, since
zeroing it would throw away the valid prefix on a truncated image.
2026-09-04 13:02:57 -06:00
Henrik Rydgård 9c1f55ed76 ISOFileSystem: Clamp file sizes to what the image actually contains
The size in an ISO directory record is untrusted, and callers allocate host
buffers from it - GetISOGameID and ReadFileToString did so directly until the
previous commit, and ReadFile clamps reads to the claimed size rather than to
the image.

We've warned about out-of-range extents since c766536914, but deliberately kept
the file, and rounded down so the warning wouldn't fire on borderline images.
Keep that behavior and just clamp the recorded size to the bytes that really
exist. For a well-formed file the extent always fits within its sectors, so this
never triggers; for a truncated one the game keeps booting instead of losing the
file entirely.
2026-09-04 12:59:24 -06:00
Acts1631 497dd8c1e7 Reject implausibly large ISO metadata files
GameManager::GetISOGameID() and GameInfoCache::ReadFileToString() resized host
buffers directly from the size reported by the ISO directory record, which is
untrusted. A crafted image can advertise a huge PARAM.SFO or icon and cause an
excessive allocation just from inspecting the file in the game list.

Cap both at a size well above anything real, and skip the read instead. Follows
the pattern PSARUnpack::ReadWholeFile already uses.

Taken from #22209.
2026-09-04 12:58:27 -06:00
Henrik Rydgård e47a1aa24d Core: Handle CORE_REENTER_DISPATCH in Core_NextFrame
Also add the missing case to CoreStateToString.
2026-09-04 12:20:11 -06:00
Acts1631 095480bcb9 Reject overflowing CSO index sizes
Keep CSO frame and block counts wide until validating their 32-bit

representation, preventing numFrames + 1 from wrapping. Also reject

index tables that cannot fit in the source file or host size_t before

allocation and indexing.
2026-09-03 10:06:35 -04:00
Acts1631 9c2c19ab71 Validate PGF bits-per-entry fields
Reject negative, oversized, or zero-width PGF table fields before

converting them to unsigned size arithmetic or passing them to

getBits. Malformed fonts can otherwise trigger undefined shifts and

out-of-bounds reads during table parsing.
2026-09-03 10:05:58 -04:00
Henrik Rydgård 3a6e6e3e0b Merge pull request #22197 from hrydgard/arm64-irjit-sxtw
Fix Arm64 IRJIT sxtw bug with kernel addresses
2026-09-02 21:51:11 +02:00
Henrik Rydgård 329b7a15bd Don't call std::terminate on a malformed number in argv or compat.ini
CmdLine used std::stoi/std::stod and Compatibility used stoi/stof, all of which
throw on junk with nothing catching them. 'PPSSPPHeadless --timeout=abc' aborted
the process, and so did a bad value in a [PostShaderSetting]-style compat section -
at startup, with no diagnostic. Parse with sscanf and report it: CmdLine already
has the pattern for this in its Bool case, and Config.cpp's ini reads were fixed
the same way earlier. A bad compat.ini entry now warns and keeps the default.
2026-09-02 18:06:47 +02:00
Henrik Rydgård 2d5d2b98a7 Merge pull request #22188 from hrydgard/misc-crash-and-state-fixes
Claude fixing more assorted bugs
2026-09-02 18:03:30 +02:00
Henrik Rydgård 3ef6ad6285 arm64jit: Don't sign-extend constant addresses with the top bit set
The ARM64 IR JIT crashed on any load/store to a constant address with the
top bit set and an offset too large for an immediate - the kernel RAM mirror
at 0x88000000, for instance.

PrepareSrc1Address is careful about this: a constant address like 0x89100010
arrives sign-extended as a negative int64_t, and the (imm & 0xC0000000) ==
0x80000000 check turns it back into the positive value it should be. But when
the offset doesn't fit an immediate we fall back to loading it into a register
and using the register-offset addressing mode, which only takes the W half of
that register plus an extend - and we asked for SXTW, undoing the fix and
pointing the access ~2GB below the memory view.

Sign extension is still right when the offset is genuinely negative, which
happens when the base is a pointerified register and the displacement is a
negative one from the MIPS instruction. So extend based on the sign of imm.

Found by the Jit unit test, which stores to 0x89100000 and segfaults in the
JIT_IR phase - only reproducible on an actual ARM64 CPU, which is why it never
showed up on CI. The RISC-V and LoongArch backends get this case right already.
2026-09-02 17:53:12 +02:00
Henrik Rydgård 56bba5f6f5 Merge pull request #22189 from hrydgard/debug-input-rewind-overflows
Fix three more out-of-bounds writes (minor)
2026-08-31 17:53:10 +02:00
Henrik Rydgård 14111097da Claude found this code buggy, and it's not used, so delete it. 2026-08-31 16:37:55 +02:00
Henrik Rydgård 9cb50459d6 Merge pull request #22186 from hrydgard/medium-correctness-fixes
Medium-severity correctness fixes
2026-08-31 16:30:53 +02:00
Henrik Rydgård 496eddb7fb Merge pull request #22185 from hrydgard/framebuffer-and-null-deref-fixes
Framebuffer and null deref fixes
2026-08-31 13:29:40 +02:00
Henrik Rydgård e88a0acaa4 Core: fix parens in the unaligned-access reason check
Written as (2 || 4 || 8 || misaligned), so every 2/4/8-byte access got labeled
'(unaligned)' and a genuinely misaligned larger access never reached the struct
branch. Log text only.
2026-08-31 13:02:53 +02:00
Henrik Rydgård 5c329415a5 PSPLoaders: bound the UMD_DATA.BIN read
InitMemorySizeForGame read all of disc0:/UMD_DATA.BIN into a vector and then copied
it into a string, with no size limit, from an image we don't control - and the
DISC_ID that gets us here is equally forgeable, it just has to match one of the 16
g_HDRemasters entries. A real UMD_DATA.BIN is a few dozen bytes; anything larger is
a mistake or an attack, so check the size before reading.
2026-08-31 12:54:06 +02:00
Henrik Rydgård 85efe6ba7c Instance: lock the shared counter for real, and don't hand out ID 0
The POSIX path used mlock() as though it were a mutex. mlock only pins pages in
RAM - it provides no mutual exclusion at all, so the read-modify-write of the
cross-process instance counter was unsynchronized. Two instances launched at the
same moment could both come away with PPSSPP_ID == 1, at which point both pass
IsFirstInstance() and write ppsspp.ini over each other, and both compute the same
adhoc local IP. Take an advisory lock on the shm fd instead. (The Windows path was
already fine - it uses a named mutex.)

Also, next/total are uint8_t in a segment that outlives the processes using it, so
next climbs across runs and wraps. Landing on 0 is worse than it looks: it isn't a
valid instance id, IsFirstInstance() fails, and config saving is silently disabled
from then on. Skip past it on wrap.
2026-08-31 12:53:34 +02:00
Henrik Rydgård cf8ef68f6b Merge pull request #22182 from hrydgard/remote-iso-empty-share-dir
Remote ISO: don't serve the whole filesystem when no folder is set
2026-08-31 12:47:23 +02:00
Henrik Rydgård 9b896e0f7d Merge pull request #22173 from hrydgard/range-validation-fixes
Claude code review: Fix a batch of missing or wrong range validation
2026-08-31 12:32:26 +02:00
Henrik Rydgård e2ab84087e Fix five ways to end up stuck, crashed, or silently degraded
System.cpp stamped BootState::Complete unconditionally after InitGPU(), overwriting
the Failed that InitGPU sets when GPU_Init() fails - after it has already run
CPU_Shutdown(). PSP_InitUpdate then took the success path on a core that no longer
existed, down to a null Memory::base, and the first guest access dereferenced it.
InitGPU now reports failure and both callers honor it. (The libretro path had the
same problem from the other direction: it calls InitGPU after the Failed check.)

HandleAssert called g_assertCancelCallback directly on the IDCANCEL path, without
the null check its own BreakIntoPSPDebugger() helper does - and EmuScreen clears the
callback when a game is unloaded. So any assert after returning to the menu turned
"Cancel: skip and break into PPSSPP debugger" into a null jump, from the one button
whose entire purpose is surviving the assert.

__CheatDoState registered the cheat event type when the savestate had no CwCheat
section, but never scheduled it. CoreTiming::DoState has already swapped in the
state's event queue by then, which doesn't contain one either - so loading an old
savestate silently killed cheats, and the enable/disable polling with them, for the
rest of the session.

Achievements::ChangeUMD set g_isIdentifying and returned without clearing it when
hashing failed, leaving IsBlockingExecution() true forever - EmuScreen stops running
the CPU and the game is frozen until restart. Reachable from a disc swap on any ISO
whose PARAM.SFO or EBOOT.BIN can't be read.

x64Analyzer routed opcode 0x88 into the write path but had no case for it, so it hit
the default, logged from inside the crash handler, and failed. 0x88 is exactly what
the x64 JIT emits for a guest sb, so MemFault could never skip or ignore a bad byte
store the way it can a word one. Handle the 8-bit forms, and drop the 0x8a/0x8b cases
in the read path that the same 0xF0 mask made unreachable. Covered by a new
CheckAnalyze case, which fails without this change.

314 pspautotests pass, all unit tests pass.
2026-08-31 12:27:24 +02:00
Henrik Rydgård bcd4f7e653 LuaContext: don't run console commands without a Lua state
Init() only runs on game load and Shutdown() nulls lua_ back out, but ImDebugger::Frame
draws the Lua console outside its PSP_IsInited() block, and whether the console is open
is persisted config. So opening it, exiting to the menu, and typing anything other than
the built-in clear/help/history dereferenced null.
2026-08-31 12:22:28 +02:00
Henrik Rydgård 3bd9e23f91 Merge pull request #22174 from hrydgard/misc-correctness-fixes
Claude code review: Misc minor correctness fixes
2026-08-31 11:37:02 +02:00
Henrik Rydgård d828d0162c Merge pull request #22176 from hrydgard/metafilesystem-savestate-compat
Fix savestate breakage related to file system mounts
2026-08-31 11:35:22 +02:00
Henrik RydgårdandClaude Opus 5 375fa0fc11 Misc correctness fixes
GetStringErrorMsg had the strerror_r result test backwards. The XSI variant returns
0 on success, so every successful lookup returned "Unknown error"; and under glibc
with _GNU_SOURCE the GNU variant is selected instead, which returns the message by
pointer and typically leaves the buffer untouched, so it returned an empty string.
Either way GetLastErrorMsg() was useless on Linux, Android and macOS. Pick the right
handling by overload resolution rather than guessing which signature we got.

KeyMap's "no gamepad button mapped to cancel" fallback pushed into confirmKeys
instead of cancelKeys - and pushed the confirm button. So unmapping cancel left no
gamepad way out of menus, and duplicated an entry in the confirm list.

ControlMapper::AddListener mutated listeners_ without taking mutex_, while
RemoveListener takes it and the input thread iterates the vector under it. Opening a
screen while an axis is moving could reallocate it mid-iteration. The comment about
piggybacking on a screenmanager mutex was stale - there isn't one.

Config's two std::stof calls on PostShaderSetting values ran on user-editable ini
text with no try/catch, so a malformed entry called std::terminate during startup
config load. Use the same checked sscanf that LoadGameConfig already uses.
(CmdLine.cpp and Compatibility.cpp have the same pattern; not touched here.)

The screenshot downscale path leaked its final buffer on every downscaled shot,
which savestate thumbnails hit on every save at 3x and above.

HandleUploadPost is registered unconditionally, so closing the Upload screen left an
unauthenticated file-write endpoint live for as long as anything else kept the server
up. Check the flag in the handler.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-31 01:19:06 +02:00
Henrik Rydgård 949f773220 WebServer: bail out of an upload whose peer disconnected
Same root cause as the websocket spin this branch fixes, different symptom, so it
belongs with the InputSink EOF change rather than apart from it.

ReadBinaryUntilTerminator() returns 0 both when nothing has arrived yet and when
the peer is gone, and the multipart loop only exits on finding its terminator - so
a cancelled browser upload left a handler thread spinning forever. That also hangs
web server shutdown, since it joins its handler threads.

Now that InputSink reports EOF, ask it.

(Moved here from misc-correctness-fixes, which will be rebased on top of this.)
2026-08-31 01:02:46 +02:00
Henrik Rydgård 1295512866 MetaFileSystem: also warn against reordering existing mounts
The positional serialization means swapping two Mount() calls breaks every old
savestate just as thoroughly as adding one does, so say so. Also note that the
mount order spans more than MountFileSystems() - the UMD mounts are added first,
by MountGameISO, before we get there.
2026-08-31 00:23:53 +02:00
Henrik Rydgård 7b409af50f MetaFileSystem: spell out how to add a mount without breaking savestates
The positional serialization means adding a mount silently invalidates every
existing savestate unless its prefix is added to the skip list, which is exactly
how flash1: broke them. Write down the rule, and note that renaming or removing
a mount isn't something this scheme can handle at all.
2026-08-30 23:59:06 +02:00
Henrik RydgårdandClaude Opus 5 729653bf42 Fix loading savestates made before flash1: was mounted
78ef1eae82 added a flash1: mount, but MetaFileSystem::DoState serializes the
mounts positionally - one section each, with no length to skip by - so a state
written before that commit has one section fewer than we now have mounts.

The existing count check assumed a single missing entry could only be pfat0:,
which was the previous mount added this way. So it took the skipPfat0 path,
skipped pfat0's section while still only looping n times, and ended up making
n-1 DoState calls against n sections. Everything after that read shifted, and
the load died with "Failure at DirectoryFileSystem".

Make the "these were added later" set explicit and ordered instead, and iterate
over the mounts rather than over the saved count, so the number of DoState calls
matches the state regardless of which of them are missing.

Verified against Wipeout Pure (UCUS98612): both save slots report n=9 against 10
mounts and fail to load before this, and load after, in both the app and headless.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 23:52:09 +02:00
Henrik RydgårdandClaude Opus 5 016f976b1f Fix a batch of missing or wrong range validation
Memory::IsValidAddress and friends tested the extended-RAM range with
(address & 0x3F000000), i.e. at 16MB granularity, so they accepted the whole 16MB
block containing the end of RAM. That's harmless at 32MB and 64MB, but the Sora no
Kiseki SC/3rd HD remasters run with 0x04C00000, so addresses from 0x0CC00000 to
0x0CFFFFFF read as valid, and MaxSizeAtAddress then underflowed to ~4GB there -
which defeats ClampValidSizeAt and IsValidRange entirely for that window. Mask
with 0x3FFFFFFF instead, in all five helpers and the copies in MemMapFunctions.cpp.

IsValidTextureAddress's extended-RAM branch repeated the first branch's whole mask
rather than just its alignment bits, so it was dead code and extended RAM was never
accepted as a texture source.

ComputeTextureHash checked IsValidAddress(addr + sizeInRAM), i.e. only the end
address, which can land in a different valid region than the start - a VRAM texture
with a large enough computed size ends exactly at the base of RAM and "passes"
while reading far past the 8MB VRAM view. Use IsValidRange.

TextureReplacer::ComputeHash's strided path had no range check at all, unlike the
contiguous path right above it. Also clamp the pack-supplied reduce-hash factor to
1.0 - it's a reduction, and the ini parser only rejects exactly 0.

ZipExtractFileToMemory read an uninitialized zip_stat when zip_stat_index failed
(it ignored the return value) and sized a host allocation directly from the zip's
declared uncompressed size. Reached just by opening an archive.

Memory::Reinit ignored Init()'s return value, and DoState fed it a memory size
taken straight from the savestate. A bogus size made the map fail to allocate and
left base null, after which DoMemoryVoid wrote RAM through it. Validate the size,
propagate the failure, and roll back to the previous size if reinit fails.

314 pspautotests pass, all unit tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 23:05:04 +02:00
Henrik RydgårdandClaude Opus 5 9f4dcf359b Fix three more out-of-bounds writes
GetCurrentDrawAsDebugVertices (GE debugger vertex preview) sized its index scratch
buffer at a fixed 65536 and then ran both expanding steps into it: index generation
turns strips/fans into up to 3 indices per input index, and RunSoftwareTransform can
then expand points/lines/rects into 6 more each. A 30000-vertex triangle strip wrote
~90000 entries. Size the buffer from the count instead.

The Expand{Rectangles,Lines,Points} capacity checks were also off: they compared the
expansion against indsSize but write the expanded indices at inds + vertexCount, so
the input count has to be part of the sum.

ControlMapper::Axis wrote rawAxisValue_[axis.axisId] with no bounds check, one line
below an explicit check on axis.deviceId. axisId comes straight from the device -
Android reports AXIS_GENERIC_13..16 as 44..47, against a 44-entry array - so it wrote
into the neighbouring deviceTimestamps_. NativeAxis had the same unchecked write into
HLEPlugins::PluginDataAxis, where it goes out of the object entirely.

Rewind's LockedDecompress computed its copy-from-base block size as
base.size() - result.size() in size_t and truncated to int, so it went negative once
the output grew past the base, and insert() then ran with last < first. That happens
because a state can outlive the base it was compressed against: there are 20 states
but only 2 bases, rotated every 16 saves. Track a generation per base and refuse to
decode a state whose base is gone, and bound the block size against the base itself.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 22:45:50 +02:00
Henrik RydgårdandClaude Opus 5 b35f28e200 Remote ISO: don't serve the whole filesystem when no folder is set
In LOCAL_FOLDER share mode, LocalFromRemotePath ended with

    return Path(g_Config.sRemoteISOSharedDir) / decoded;

sRemoteISOSharedDir defaults to empty and nothing requires the user to pick a
folder before pressing "Share Games (Server)". Path::operator/ doesn't insert a
separator when the component already starts with one, so with an empty base it
returns the component verbatim - "GET /etc/passwd" resolved to Path("/etc/passwd"),
which is non-empty and went straight to DiscHandler. The backslash, "/.." and "//"
filters never fired, because no traversal is needed to get there. That is an
unauthenticated arbitrary file read for anything that can reach the port.

Refuse to resolve anything when no shared directory is configured, and check that
the joined path actually stays inside it. HandleListing needs the same guard: it
called GetFilesInDir on the empty path, which on Windows becomes
FindFirstFile("\*") - a listing of the root of the current drive.

Also log a warning when the server starts in this state, so "nothing is shared"
doesn't look like a mysterious failure.

The empty-base behavior of Path::operator/ is surprising enough to be worth
pinning down, so TestPath now asserts it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
2026-08-30 13:51:02 +02:00
Henrik Rydgård 76c7ddfb11 Merge pull request #22170 from hrydgard/upnp-portmanager-fixes
UPnP: fix the exit hang and the CPU spin, and only run the thread when enabled
2026-08-30 10:25:44 +02:00