UI, texture packs, headless: Validate values from files

- textures.ini skipLastDXT1Blocks* is subtracted from the hashed size after
  the memory check, so a large value (or a small one with reduceHash)
  underflowed it and hashed ~4 GB. Clamp it to the blocks there are.
- SND0.AT3 loops with a negative or backwards range pointed playback before
  the data. Ignore those, and loops outside the data.
- impose.rsc sprite bounds were checked as x + w in 32 bits, which wraps.
- Store entries are also uninstalled by name, so require a plain filename
  (. or / was the whole GAME directory).
- Headless screenshot compare read (and converted in place) w x h pixels of a
  reference PNG that could be smaller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-05 13:21:52 +01:00
1 parent 128cf1b7c4
commit ebafb89215
5 files changed
+35 -8

No files matched your search

+6 -2
View File
@@ -367,6 +367,9 @@ bool TextureReplacer::LoadIniValues(IniFile &ini, VFSBackend *dir, bool isOverri
}
options->Get("skipLastDXT1Blocks128x64", &skipLastDXT1Blocks128x64_);
options->Get("skipLastDXT1Blocks128x128", &skipLastDXT1Blocks128x128_);
// These are subtracted from the hashed size, so can't be more than the blocks there are.
skipLastDXT1Blocks128x64_ = std::clamp(skipLastDXT1Blocks128x64_, 0, (128 / 4) * (64 / 4));
skipLastDXT1Blocks128x128_ = std::clamp(skipLastDXT1Blocks128x128_, 0, (128 / 4) * (128 / 4));
if (reduceHash_ && textureHash_ == ReplacedTextureHash::QUICK) {
reduceHash_ = false;
ERROR_LOG(Log::TexReplacement, "Texture Replacement: reduceHash option requires safer hash, use xxh32 or xxh64 instead.");
@@ -635,11 +638,12 @@ u32 TextureReplacer::ComputeHash(u32 addr, int bufw, int w, int h, bool swizzled
}
// Hack for Yu Gi Oh texture hashing problem. See issue #19714
if (skipLastDXT1Blocks128x64_ && fmt == GE_TFMT_DXT1 && w == 128 && h == 64) {
// (reduceHash can make sizeInRAM smaller than all the blocks, so check it can't underflow.)
if (skipLastDXT1Blocks128x64_ && fmt == GE_TFMT_DXT1 && w == 128 && h == 64 && (u32)(8 * skipLastDXT1Blocks128x64_) < (u32)sizeInRAM) {
// Skip the last few blocks as specified.
sizeInRAM -= 8 * skipLastDXT1Blocks128x64_;
}
if (skipLastDXT1Blocks128x128_ && fmt == GE_TFMT_DXT1 && w == 128 && h == 128) {
if (skipLastDXT1Blocks128x128_ && fmt == GE_TFMT_DXT1 && w == 128 && h == 128 && (u32)(8 * skipLastDXT1Blocks128x128_) < (u32)sizeInRAM) {
sizeInRAM -= 8 * skipLastDXT1Blocks128x128_;
}
+18 -4
View File
@@ -2,6 +2,7 @@
#include <string>
#include <mutex>
#include <algorithm>
#include <climits>
#include "Common/File/VFS/VFS.h"
#include "Common/UI/Root.h"
@@ -132,14 +133,19 @@ bool WavData::Read(RIFFReader &file_) {
int samplesPerFrame = codec == PSP_CODEC_AT3PLUS ? 2048 : 1024;
for (int i = 0; i < numLoops; ++i) {
// Only seen forward loops, so let's ignore others.
if (loops[i].type != 0)
// Only seen forward loops, so let's ignore others. Also ones that don't go forwards,
// which would point playback before the data.
if (loops[i].type != 0 || loops[i].startSample < 0 || loops[i].endSample <= loops[i].startSample)
continue;
// We ignore loop interpolation (fraction) and play count for now.
raw_offset_loop_start = (loops[i].startSample / samplesPerFrame) * raw_bytes_per_frame;
const s64 loopStart = (s64)(loops[i].startSample / samplesPerFrame) * raw_bytes_per_frame;
const s64 loopEnd = (s64)(loops[i].endSample / samplesPerFrame) * raw_bytes_per_frame;
if (loopEnd > INT_MAX)
continue;
raw_offset_loop_start = (int)loopStart;
loop_start_offset = loops[i].startSample % samplesPerFrame;
raw_offset_loop_end = (loops[i].endSample / samplesPerFrame) * raw_bytes_per_frame;
raw_offset_loop_end = (int)loopEnd;
loop_end_offset = loops[i].endSample % samplesPerFrame;
if (loops[i].playCount == 0) {
@@ -201,6 +207,14 @@ bool WavData::Read(RIFFReader &file_) {
ERROR_LOG(Log::Audio, "Could not descend into RIFF file.");
return false;
}
// The loop points came from the smpl chunk, so they have to land in the data. The sample
// offsets (within a frame) are always positive, since the loop samples were checked above.
if (raw_offset_loop_start < 0 || raw_offset_loop_end > raw_data_size || raw_offset_loop_start > raw_offset_loop_end) {
raw_offset_loop_start = 0;
raw_offset_loop_end = 0;
loop_start_offset = 0;
loop_end_offset = 0;
}
sample_rate = samplesPerSec;
return true;
}
+1 -1
View File
@@ -298,7 +298,7 @@ bool ImposeArt::Parse(const std::vector<uint8_t> &file, int pspLanguage) {
const uint32_t w = r.U32(rec + 16);
const uint32_t h = r.U32(rec + 20);
const uint32_t offset = r.U32(rec + 24);
if (!r.ok || palette >= NUM_PALETTES || w == 0 || h == 0 || x + w > PSP_W || y + h > PSP_H || (w * h + 1) / 2 != size) {
if (!r.ok || palette >= NUM_PALETTES || w == 0 || h == 0 || w > PSP_W || x > PSP_W - w || h > PSP_H || y > PSP_H - h || (w * h + 1) / 2 != size) {
return false;
}
const size_t pixels = block + TEXT_RECORDS + (size_t)offset;
+3 -1
View File
@@ -448,7 +448,9 @@ void StoreScreen::ParseListing(const std::string &json) {
// (Path::operator/ does no ".." normalization); on platforms/builds where
// the store index isn't fetched over HTTPS, a MITM'd or compromised
// response could otherwise point "launch" at an arbitrary host path.
if (HasParentDirComponent(file))
// It's also uninstalled (deleted recursively) by name, so "." or "/" would be the
// whole GAME directory.
if (HasParentDirComponent(file) || !IsSafePathComponent(file))
continue;
e.file = file;
entries_.push_back(e);
+7
View File
@@ -403,6 +403,13 @@ double ScreenshotComparer::Compare(const Path &screenshotFilename) {
return -1.0f;
}
// Everything below reads w_ x h_ pixels from it.
if (width < (int)w_ || height < (int)h_) {
error_ = StringFromFormat("Screenshot is too small (%dx%d, expected %dx%d): %s", width, height, (int)w_, (int)h_, screenshotFilename.ToVisualString().c_str());
free(reference_);
reference_ = nullptr;
return -1.0f;
}
referenceStride_ = width;
}
} else {