diff --git a/GPU/Common/ReplacedTexture.cpp b/GPU/Common/ReplacedTexture.cpp index ad306ecbae..3590d79bd2 100644 --- a/GPU/Common/ReplacedTexture.cpp +++ b/GPU/Common/ReplacedTexture.cpp @@ -321,6 +321,17 @@ ReplacedTexture::LoadLevelResult ReplacedTexture::LoadLevelData(VFSFileReference std::string magic; ReplacedImageType imageType = Identify(vfs_, openFile, &magic); + // Disallow mixing image formats across mip levels: a KTX2/DDS container + // manages its own mip chain, so mixing one in at a higher mip level + // would corrupt the shared level data layout. + if (mipLevel == 0) { + firstImageType_ = imageType; + } else if (imageType != firstImageType_) { + WARN_LOG(Log::TexReplacement, "Replacement mipmap %d uses image format %d, but mip 0 uses %d. Stopping.", mipLevel, (int)imageType, (int)firstImageType_); + vfs_->CloseFile(openFile); + return LoadLevelResult::DONE; + } + bool ddsDX10 = false; int numMips = 1; @@ -531,7 +542,13 @@ ReplacedTexture::LoadLevelResult ReplacedTexture::LoadLevelData(VFSFileReference WARN_LOG(Log::TexReplacement, "Block compressed replacement texture '%s' not divisible by 4x4 (%dx%d). In D3D11 (only!) we will have to expand (potentially causing glitches).", filename.c_str(), level.w, level.h); } - data_.resize(numMips); + // Cap the mip count (attacker-controlled header field) and make sure + // data_ is large enough for mipLevel + numMips; otherwise the loop + // below indexes past the end of data_. + numMips = std::max(1, std::min(numMips, MAX_REPLACEMENT_MIP_LEVELS - mipLevel)); + if ((size_t)(mipLevel + numMips) > data_.size()) { + data_.resize(mipLevel + numMips); + } basist::ktx2_transcoder_state transcodeState; // Each thread needs one of these. @@ -553,7 +570,7 @@ ReplacedTexture::LoadLevelResult ReplacedTexture::LoadLevelData(VFSFileReference outputSize = levelInfo.m_orig_width * levelInfo.m_orig_height; outputPitch = levelInfo.m_orig_width; } - data_[i].resize(dataSizeBytes); + out.resize(dataSizeBytes); transcodeState.clear(); transcoder.transcode_image_level(i, 0, 0, &out[0], (uint32_t)outputSize, transcoderFormat, 0, (uint32_t)outputPitch, level.h, -1, -1, &transcodeState); @@ -585,7 +602,13 @@ ReplacedTexture::LoadLevelResult ReplacedTexture::LoadLevelData(VFSFileReference WARN_LOG(Log::TexReplacement, "Block compressed replacement texture '%s' not divisible by 4x4 (%dx%d). In D3D11 (only!) we will have to expand (potentially causing glitches).", filename.c_str(), level.w, level.h); } - data_.resize(numMips); + // Cap the mip count (attacker-controlled header field) and make sure + // data_ is large enough for mipLevel + numMips; otherwise the loop + // below indexes past the end of data_. + numMips = std::max(1, std::min(numMips, MAX_REPLACEMENT_MIP_LEVELS - mipLevel)); + if ((size_t)(mipLevel + numMips) > data_.size()) { + data_.resize(mipLevel + numMips); + } // A DDS File can contain multiple mipmaps. levels_.reserve(numMips); diff --git a/GPU/Common/ReplacedTexture.h b/GPU/Common/ReplacedTexture.h index 540c3574d5..c849611104 100644 --- a/GPU/Common/ReplacedTexture.h +++ b/GPU/Common/ReplacedTexture.h @@ -220,6 +220,9 @@ private: std::vector> data_; std::vector levels_; + // Image format of mip level 0; mixing formats across levels is not + // allowed (container formats like KTX2/DDS manage their own mip chain). + ReplacedImageType firstImageType_ = ReplacedImageType::INVALID; double lastUsed_ = 0.0; LimitedWaitable *threadWaitable_ = nullptr; diff --git a/GPU/Common/TextureCacheCommon.cpp b/GPU/Common/TextureCacheCommon.cpp index 25ece3e25b..f6c7df252e 100644 --- a/GPU/Common/TextureCacheCommon.cpp +++ b/GPU/Common/TextureCacheCommon.cpp @@ -1957,6 +1957,21 @@ TextureAlpha TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETex const u8 *texptr = Memory::GetPointer(texaddr); const uint32_t byteSize = (textureBitsPerPixel[format] * bufw * h) / 8; + // Validate the texture data fits in mapped RAM, like the DXT path does. + // texaddr/bufw/w/h are all guest-controlled via the GE display list. + const int bpp = textureBitsPerPixel[format]; + const uint32_t bytesPerRow = (bpp * bufw) / 8; + // Swizzled textures are read in 8-row blocks, rounding the height up. + const uint32_t rows = swizzled ? ((h + 7) & ~7) : h; + const uint32_t neededBytes = bytesPerRow * rows; + if (bytesPerRow > 0 && !Memory::IsValidRange(texaddr, neededBytes)) { + ERROR_LOG_REPORT(Log::G3D, "Texture extends beyond valid RAM: %08x + %d x %d", texaddr, bufw, h); + uint32_t limited = Memory::ClampValidSizeAt(texaddr, neededBytes); + h = limited / bytesPerRow; + if (swizzled) + h &= ~7; + } + char buf[128]; size_t len = snprintf(buf, sizeof(buf), "Tex_%08x_%dx%d_%s", texaddr, w, h, GeTextureFormatToString(format, clutformat)); NotifyMemInfo(MemBlockFlags::TEXTURE, texaddr, byteSize, buf, len); @@ -2065,13 +2080,13 @@ TextureAlpha TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETex // We can't know anything about alpha. return TextureAlpha::Any; } - return ReadIndexedTex(out, outPitch, level, texptr, 1, bufw, reverseColors, expandTo32bit); + return ReadIndexedTex(out, outPitch, w, h, level, texptr, 1, bufw, reverseColors, expandTo32bit); case GE_TFMT_CLUT16: - return ReadIndexedTex(out, outPitch, level, texptr, 2, bufw, reverseColors, expandTo32bit); + return ReadIndexedTex(out, outPitch, w, h, level, texptr, 2, bufw, reverseColors, expandTo32bit); case GE_TFMT_CLUT32: - return ReadIndexedTex(out, outPitch, level, texptr, 4, bufw, reverseColors, expandTo32bit); + return ReadIndexedTex(out, outPitch, w, h, level, texptr, 4, bufw, reverseColors, expandTo32bit); case GE_TFMT_4444: case GE_TFMT_5551: @@ -2189,10 +2204,7 @@ TextureAlpha TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETex return AlphaSumIsFull(alphaSum, fullAlphaMask) ? TextureAlpha::Solid : TextureAlpha::Any; } -TextureAlpha TextureCacheCommon::ReadIndexedTex(u8 *out, int outPitch, int level, const u8 *texptr, int bytesPerIndex, int bufw, bool reverseColors, bool expandTo32Bit) { - int w = gstate.getTextureWidth(level); - int h = gstate.getTextureHeight(level); - +TextureAlpha TextureCacheCommon::ReadIndexedTex(u8 *out, int outPitch, int w, int h, int level, const u8 *texptr, int bytesPerIndex, int bufw, bool reverseColors, bool expandTo32Bit) { if (gstate.isTextureSwizzled()) { tmpTexBuf32_.resize(bufw * ((h + 7) & ~7)); UnswizzleFromMem(tmpTexBuf32_.data(), bufw * bytesPerIndex, texptr, bufw, h, bytesPerIndex); diff --git a/GPU/Common/TextureCacheCommon.h b/GPU/Common/TextureCacheCommon.h index f2f3b49523..1908002ad7 100644 --- a/GPU/Common/TextureCacheCommon.h +++ b/GPU/Common/TextureCacheCommon.h @@ -425,7 +425,7 @@ protected: TextureAlpha DecodeTextureLevel(u8 *out, int outPitch, GETextureFormat format, GEPaletteFormat clutformat, uint32_t texaddr, int level, int bufw, TexDecodeFlags flags); static void UnswizzleFromMem(u32 *dest, u32 destPitch, const u8 *texptr, u32 bufw, u32 height, u32 bytesPerPixel); - TextureAlpha ReadIndexedTex(u8 *out, int outPitch, int level, const u8 *texptr, int bytesPerIndex, int bufw, bool reverseColors, bool expandTo32Bit); + TextureAlpha ReadIndexedTex(u8 *out, int outPitch, int w, int h, int level, const u8 *texptr, int bytesPerIndex, int bufw, bool reverseColors, bool expandTo32Bit); ReplacedTexture *FindReplacement(TexCacheEntry *entry, int *w, int *h, int *d); void PollReplacement(TexCacheEntry *entry, int *w, int *h, int *d);