Files
ppsspp/GPU/Common/ReplacedTexture.h
Henrik Rydgård 0049b19fbf Fix OOB in texture replacer mip mixing and non-DXT texture decode
Vuln 17: ReplacedTexture::LoadLevelData let a KTX2/DDS file at a higher
mip level resize the shared data_ vector to its own (attacker-controlled)
mip count, so data_[mipLevel + i] indexed out of bounds and the KTX2
branch resized a different element than it wrote to. Disallow mixing
image formats across mip levels, cap the container mip count, and resize
the same element that is used as the transcode destination.

Vuln 18: DecodeTextureLevel only validated the start address for
non-DXT textures, so guest-controlled w/h/bufw could drive reads past
mapped RAM. Validate the needed range like the DXT path does and clamp
the height; ReadIndexedTex now takes the clamped w/h.
2026-08-01 13:16:22 +02:00

242 lines
5.8 KiB
C++

// Copyright (c) 2016- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#pragma once
#include <mutex>
#include <string>
#include "Common/File/VFS/VFS.h"
#include "Common/GPU/thin3d.h"
#include "Core/ConfigValues.h"
#include "GPU/Common/ImageCommon.h"
class TextureReplacer;
class LimitedWaitable;
// For forward compatibility, we specify the hash.
enum class ReplacedTextureHash {
QUICK,
XXH32,
XXH64,
};
enum class ReplacedImageType {
PNG,
ZIM,
DDS,
BASIS, // TODO: Might not even do this, KTX2 is a better container.
KTX2,
INVALID,
};
static const int MAX_REPLACEMENT_MIP_LEVELS = 12; // 12 should be plenty, 8 is the max mip levels supported by the PSP.
enum class ReplacementState : uint32_t {
UNLOADED,
PENDING,
NOT_FOUND, // Also used on error loading the images.
ACTIVE,
CANCEL_INIT,
COUNT, // Not a valid state
};
const char *StateString(ReplacementState state);
struct GPUFormatSupport {
bool bc123;
bool astc;
bool bc7;
bool etc2;
};
struct ReplacementCacheKey {
u64 cachekey; // Split into two u32?
u32 hash;
ReplacementCacheKey() : cachekey(0), hash(0) {}
ReplacementCacheKey(u64 ckey, u32 h) : cachekey(ckey), hash(h) {}
bool operator ==(const ReplacementCacheKey &k) const {
return k.cachekey == cachekey && k.hash == hash;
}
bool operator <(const ReplacementCacheKey &k) const {
if (k.cachekey == cachekey) {
return k.hash < hash;
}
return k.cachekey < cachekey;
}
// Access the parts.
u64 CacheKey() const { return cachekey; }
u32 ClutHash() const { return (u32)(cachekey & 0xFFFFFFFFULL); }
u32 Address() const { return (u32)(cachekey >> 32); }
u32 ContentsHash() const { return hash; }
void ZeroAddress() {
cachekey &= 0xFFFFFFFFULL;
}
void ZeroClutHash() {
cachekey &= 0xFFFFFFFF00000000ULL;
}
void ZeroContentsHash() {
hash = 0;
}
};
struct ReplacementDesc {
int newW;
int newH;
ReplacementCacheKey cacheKey;
int w;
int h;
TextureFiltering forceFiltering;
std::string hashfiles;
Path basePath;
std::vector<std::string> filenames;
std::string logId;
GPUFormatSupport formatSupport;
};
class ReplacedTexture;
// These aren't actually all replaced, they can also represent a placeholder for a not-found
// replacement (texture == nullptr).
struct ReplacedTextureRef {
ReplacedTexture *texture; // shortcut
std::string hashfiles; // key into the cache
};
// Metadata about a given texture level.
struct ReplacedTextureLevel {
// Data dimensions
int w = 0;
int h = 0;
// PSP texture dimensions
int fullW = 0;
int fullH = 0;
int fullDataSize = 0;
// To be able to reload, we need to be able to reopen, unfortunate we can't use zip_file_t.
// TODO: This really belongs on the level in the cache, not in the individual ReplacedTextureLevel objects.
VFSFileReference *fileRef = nullptr;
};
class ReplacedTexture {
public:
ReplacedTexture(VFSBackend *vfs, const ReplacementDesc &desc);
~ReplacedTexture();
inline ReplacementState State() const {
return state_;
}
void SetState(ReplacementState state) {
_dbg_assert_(state != state_);
state_ = state;
}
void GetSize(int level, int *w, int *h) const {
_dbg_assert_(State() == ReplacementState::ACTIVE);
_dbg_assert_((size_t)level < levels_.size());
*w = levels_[level].fullW;
*h = levels_[level].fullH;
}
int GetLevelDataSizeAfterCopy(int level) const {
// Includes padding etc.
return levels_[level].fullDataSize;
}
size_t GetTotalDataSize() const {
if (State() != ReplacementState::ACTIVE) {
return 0;
}
size_t sz = 0;
for (auto &data : data_) {
sz += data.size();
}
return sz;
}
bool ForceFiltering(TextureFiltering *forceFiltering) const {
if (desc_.forceFiltering != (TextureFiltering)0) {
*forceFiltering = desc_.forceFiltering;
return true;
} else {
return false;
}
}
int NumLevels() const {
_dbg_assert_(State() == ReplacementState::ACTIVE);
return (int)levels_.size();
}
Draw::DataFormat Format() const {
_dbg_assert_(State() == ReplacementState::ACTIVE);
return fmt;
}
const ReplacementDesc &Desc() const {
return desc_;
}
TextureAlpha AlphaStatus() const {
return alphaStatus_;
}
bool Poll(double budget);
bool CopyLevelTo(int level, uint8_t *out, size_t outDataSize, int rowPitch);
std::string logId_;
private:
enum class LoadLevelResult {
LOAD_ERROR = 0,
CONTINUE = 1,
DONE = 2,
};
void Prepare(VFSBackend *vfs);
LoadLevelResult LoadLevelData(VFSFileReference *fileRef, const std::string &filename, int level, Draw::DataFormat *pixelFormat);
void PurgeIfNotUsedSinceTime(double t);
std::vector<std::vector<uint8_t>> data_;
std::vector<ReplacedTextureLevel> 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;
std::mutex lock_;
Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED;
TextureAlpha alphaStatus_ = TextureAlpha::Any;
double lastUsed = 0.0;
std::atomic<ReplacementState> state_ = ReplacementState::UNLOADED;
VFSBackend *vfs_ = nullptr;
ReplacementDesc desc_;
friend class TextureReplacer;
friend class ReplacedTextureTask;
};