diff --git a/Common/Data/Encoding/Compression.cpp b/Common/Data/Encoding/Compression.cpp index c348ed9313..0ac584243d 100644 --- a/Common/Data/Encoding/Compression.cpp +++ b/Common/Data/Encoding/Compression.cpp @@ -54,7 +54,7 @@ bool compress_string(const std::string& str, std::string *dest, int compressionl } /** Decompress an STL string using zlib and return the original data. */ -bool decompress_string(const std::string& str, std::string *dest) { +bool decompress_string(const std::string& str, std::string *dest, size_t maxSize) { if (!str.size()) return false; @@ -85,7 +85,11 @@ bool decompress_string(const std::string& str, std::string *dest) { outstring.append(outbuffer, zs.total_out - outstring.size()); } - + if (outstring.size() > maxSize) { + inflateEnd(&zs); + ERROR_LOG(Log::IO, "Decompressed data is larger than %d bytes, giving up", (int)maxSize); + return false; + } } while (ret == Z_OK); inflateEnd(&zs); @@ -95,6 +99,6 @@ bool decompress_string(const std::string& str, std::string *dest) { return false; } - *dest = outstring; + *dest = std::move(outstring); return true; } diff --git a/Common/Data/Encoding/Compression.h b/Common/Data/Encoding/Compression.h index 5323b8f549..b66db3e16c 100644 --- a/Common/Data/Encoding/Compression.h +++ b/Common/Data/Encoding/Compression.h @@ -4,4 +4,5 @@ // inflate/deflate convenience wrapper. Uses zlib. bool compress_string(const std::string& str, std::string *dest, int compressionlevel = 9); -bool decompress_string(const std::string& str, std::string *dest); +// Fails if the output would be larger than maxSize (a small input can expand enormously). +bool decompress_string(const std::string& str, std::string *dest, size_t maxSize = 256 * 1024 * 1024); diff --git a/Common/Data/Encoding/Utf8.cpp b/Common/Data/Encoding/Utf8.cpp index faad2bba78..8ec109d423 100644 --- a/Common/Data/Encoding/Utf8.cpp +++ b/Common/Data/Encoding/Utf8.cpp @@ -110,6 +110,12 @@ int u8_charnum(const char *s, int offset) uint32_t u8_nextchar(const char *s, int *index, size_t size) { uint32_t ch = 0; _dbg_assert_(*index >= 0 && *index < 100000000); + // At (or past) the end, there's nothing to read. Leave the index at the end so loops waiting + // for end() finish. + if (*index < 0 || (size_t)*index >= size) { + *index = (int)size; + return 0; + } int sz = 0; int i = *index; do { @@ -138,6 +144,7 @@ void u8_dec(const char *s, int *i) { bool AnyEmojiInString(std::string_view str, size_t byteCount) { int i = 0; + byteCount = std::min(byteCount, str.size()); while (i < byteCount) { uint32_t c = u8_nextchar(str.data(), &i, str.size()); if (CodepointIsProbablyEmoji(c)) { diff --git a/Common/Data/Format/JSONReader.cpp b/Common/Data/Format/JSONReader.cpp index b5f11e57d9..83ca13a7e1 100644 --- a/Common/Data/Format/JSONReader.cpp +++ b/Common/Data/Format/JSONReader.cpp @@ -1,3 +1,5 @@ +#include + #include "Common/Data/Format/JSONReader.h" #include "Common/File/VFS/VFS.h" #include "Common/File/Path.h" @@ -121,20 +123,31 @@ double JsonGet::getFloat(const char *child_name, double default_value) const { return val->value.toNumber(); } +// Casting a double that's out of range (or NaN) to int is undefined. +static int ClampToInt(double d) { + if (!(d >= (double)INT_MIN)) { + return d != d ? 0 : INT_MIN; + } + if (d > (double)INT_MAX) { + return INT_MAX; + } + return (int)d; +} + int JsonGet::getInt(const char *child_name) const { const JsonNode *val = get(child_name, JSON_NUMBER); if (!val) { ERROR_LOG(Log::IO, "Number '%s' missing from node", child_name); return 0; } - return (int)val->value.toNumber(); + return ClampToInt(val->value.toNumber()); } int JsonGet::getInt(const char *child_name, int default_value) const { const JsonNode *val = get(child_name, JSON_NUMBER); if (!val) return default_value; - return (int)val->value.toNumber(); + return ClampToInt(val->value.toNumber()); } bool JsonGet::getBool(const char *child_name) const { diff --git a/Common/Data/Format/PNGLoad.cpp b/Common/Data/Format/PNGLoad.cpp index ac78ce838d..6138908100 100644 --- a/Common/Data/Format/PNGLoad.cpp +++ b/Common/Data/Format/PNGLoad.cpp @@ -23,13 +23,28 @@ int pngLoad(const char *file, int *pwidth, int *pheight, unsigned char **image_d *image_data_ptr = nullptr; return 0; } + if (png.width == 0 || png.height == 0 || png.width > 8192 || png.height > 8192) { + WARN_LOG(Log::IO, "pngLoad: Bad size %dx%d (%s)", (int)png.width, (int)png.height, file); + png_image_free(&png); + *image_data_ptr = nullptr; + return 0; + } *pwidth = png.width; *pheight = png.height; png.format = PNG_FORMAT_RGBA; int stride = PNG_IMAGE_ROW_STRIDE(png); *image_data_ptr = (unsigned char *)malloc(PNG_IMAGE_SIZE(png)); - png_image_finish_read(&png, NULL, *image_data_ptr, stride, NULL); + if (!*image_data_ptr) { + png_image_free(&png); + return 0; + } + if (!png_image_finish_read(&png, NULL, *image_data_ptr, stride, NULL)) { + WARN_LOG(Log::IO, "pngLoad: %s (%s)", png.message, file); + free(*image_data_ptr); + *image_data_ptr = nullptr; + return 0; + } return 1; } @@ -50,11 +65,13 @@ struct PngReadContext { int pngLoadPtr(const unsigned char *input_ptr, size_t input_len, int *pwidth, int *pheight, unsigned char **image_data_ptr, int maxWidth, int maxHeight) { - png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, pngErrorHandler, pngWarningHandler); - if (!png) { + // The error handler below frees whatever this points to. + *image_data_ptr = nullptr; + if (input_len == 0) { return 0; } - if (input_len == 0) { + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, pngErrorHandler, pngWarningHandler); + if (!png) { return 0; } diff --git a/Common/Data/Format/ZIMLoad.cpp b/Common/Data/Format/ZIMLoad.cpp index 0113a46d71..41dec3f004 100644 --- a/Common/Data/Format/ZIMLoad.cpp +++ b/Common/Data/Format/ZIMLoad.cpp @@ -63,7 +63,9 @@ int LoadZIMPtr(const uint8_t *zim, size_t datasize, int *width, int *height, int memcpy(height, zim + 8, 4); memcpy(flags, zim + 12, 4); - if (*width <= 0 || *height <= 0) { + // Nothing we use is anywhere near this, and the data is often compressed, so the + // size is otherwise only bounded by how much memory a decompression bomb can claim. + if (*width <= 0 || *height <= 0 || *width > 8192 || *height > 8192) { ERROR_LOG(Log::IO, "Invalid ZIM dimensions %i x %i", *width, *height); return 0; } @@ -147,9 +149,12 @@ int LoadZIMPtr(const uint8_t *zim, size_t datasize, int *width, int *height, int *image = 0; return 0; } - if (outlen != total_data_size) { - // Shouldn't happen if return value was Z_OK. + if (outlen != (long)total_data_size) { + // The stream ended early, leaving the rest of the image uninitialized. ERROR_LOG(Log::IO, "Wrong size data in ZIM: %i vs %i", (int)outlen, (int)total_data_size); + free(*image); + *image = 0; + return 0; } } else if (*flags & ZIM_ZSTD_COMPRESSED) { size_t outlen = ZSTD_decompress(*image, total_data_size, zim + 16, payload_size); diff --git a/Common/Data/Text/Parsers.cpp b/Common/Data/Text/Parsers.cpp index b5b5c0992c..c3afffe621 100644 --- a/Common/Data/Text/Parsers.cpp +++ b/Common/Data/Text/Parsers.cpp @@ -169,7 +169,10 @@ StringWriter &StringWriter::F(const char *format, ...) { va_list args; va_start(args, format); int wouldHaveBeenWritten = vsnprintf(p_, remainder, format, args); - p_ += std::min((int)remainder, wouldHaveBeenWritten); va_end(args); + // -1 on encoding errors, which mustn't move p_ backwards. + if (wouldHaveBeenWritten > 0) { + p_ += std::min((int)remainder, wouldHaveBeenWritten); + } return *this; } diff --git a/Common/File/VFS/DirectoryReader.cpp b/Common/File/VFS/DirectoryReader.cpp index 386568d70b..1b5b78a06d 100644 --- a/Common/File/VFS/DirectoryReader.cpp +++ b/Common/File/VFS/DirectoryReader.cpp @@ -24,13 +24,17 @@ static std::string ToLowerASCII(std::string_view str) { return lower; } -void DirectoryReader::IndexDirectory(const Path &dir, const std::string &prefix) { +void DirectoryReader::IndexDirectory(const Path &dir, const std::string &prefix, int depth) { + // A symlink loop would otherwise recurse until the stack runs out. + if (depth > 32) { + return; + } std::vector files; File::GetFilesInDir(dir, &files); for (const File::FileInfo &file : files) { const std::string relative = prefix + file.name; if (file.isDirectory) { - IndexDirectory(file.fullName, relative + "/"); + IndexDirectory(file.fullName, relative + "/", depth + 1); } else { lowerIndex_[ToLowerASCII(relative)] = relative; } @@ -44,7 +48,7 @@ bool DirectoryReader::FindIgnoringCase(std::string_view path, std::string *actua std::lock_guard guard(indexLock_); if (!indexBuilt_) { // Only done on the first miss, so a directory where every name matches never pays for it. - IndexDirectory(path_, ""); + IndexDirectory(path_, "", 0); indexBuilt_ = true; INFO_LOG(Log::IO, "Indexed %d files in %s to find names that differ in case", (int)lowerIndex_.size(), path_.c_str()); } @@ -134,13 +138,22 @@ void DirectoryReader::ReleaseFile(VFSFileReference *vfsReference) { VFSOpenFile *DirectoryReader::OpenFileForRead(VFSFileReference *vfsReference, size_t *size) { DirectoryReaderFileReference *reference = (DirectoryReaderFileReference *)vfsReference; + // On some platforms, directories can be opened as files. Their size is meaningless. + if (File::IsDirectory(reference->path)) { + return nullptr; + } FILE *file = File::OpenCFile(reference->path, "rb"); if (!file) { return nullptr; } File::Fseek(file, 0, SEEK_END); - *size = File::Ftell(file); + const int64_t fileSize = File::Ftell(file); File::Fseek(file, 0, SEEK_SET); + if (fileSize < 0) { + fclose(file); + return nullptr; + } + *size = (size_t)fileSize; DirectoryReaderOpenFile *openFile = new DirectoryReaderOpenFile(); openFile->file = file; return openFile; diff --git a/Common/File/VFS/DirectoryReader.h b/Common/File/VFS/DirectoryReader.h index b291e7610b..8e3e81c1ea 100644 --- a/Common/File/VFS/DirectoryReader.h +++ b/Common/File/VFS/DirectoryReader.h @@ -36,7 +36,7 @@ public: private: // Returns the path (relative to path_) of a file whose name matches path except for case. bool FindIgnoringCase(std::string_view path, std::string *actualPath); - void IndexDirectory(const Path &dir, const std::string &prefix); + void IndexDirectory(const Path &dir, const std::string &prefix, int depth); Path path_; bool ignoreCase_ = false; diff --git a/Common/File/VFS/ZipFileReader.cpp b/Common/File/VFS/ZipFileReader.cpp index 595083f0f3..7deade30d5 100644 --- a/Common/File/VFS/ZipFileReader.cpp +++ b/Common/File/VFS/ZipFileReader.cpp @@ -16,8 +16,10 @@ #include "Common/StringUtils.h" // Sanity cap on a single zip entry's declared uncompressed size, so a corrupt or -// malicious zip can't drive an implausible (or, near UINT64_MAX, wrapping) allocation. -static constexpr uint64_t MAX_ZIP_ENTRY_SIZE = 1ULL << 32; // 4GB, generous for any real asset. +// malicious zip can't drive an implausible allocation (which also has to fit in a +// 32-bit size_t). Entries are read whole into memory; the largest real asset, an +// uncompressed 8192x8192 replacement texture with mips, is under 512MB. +static constexpr uint64_t MAX_ZIP_ENTRY_SIZE = 512ULL * 1024 * 1024; ZipContainer::ZipContainer() noexcept : sourceData_(nullptr), zip_(nullptr) {} @@ -186,8 +188,13 @@ uint8_t *ZipFileReader::ReadFile(std::string_view path, size_t *size) { return 0; } uint8_t *contents = new uint8_t[zstat.size + 1]; - zip_fread(file, contents, zstat.size); + const zip_int64_t bytesRead = zip_fread(file, contents, zstat.size); zip_fclose(file); + if (bytesRead < 0 || (uint64_t)bytesRead != zstat.size) { + ERROR_LOG(Log::IO, "Error reading %s from ZIP (%lld of %llu bytes)", temp_path.c_str(), (long long)bytesRead, (unsigned long long)zstat.size); + delete[] contents; + return 0; + } contents[zstat.size] = 0; *size = zstat.size; @@ -402,6 +409,13 @@ VFSOpenFile *ZipFileReader::OpenFileForRead(VFSFileReference *vfsReference, size delete openFile; return nullptr; } + // Callers allocate the returned size. + if (zstat.size > MAX_ZIP_ENTRY_SIZE) { + ERROR_LOG(Log::IO, "Zip entry %d claims an implausible size (%llu), refusing to open", reference->zi, (unsigned long long)zstat.size); + lock_.unlock(); + delete openFile; + return nullptr; + } openFile->zf = zip_fopen_index(zip_file_, reference->zi, 0); if (!openFile->zf) { @@ -429,15 +443,20 @@ void ZipFileReader::Rewind(VFSOpenFile *vfsOpenFile) { size_t ZipFileReader::Read(VFSOpenFile *vfsOpenFile, void *buffer, size_t length) { ZipFileReaderOpenFile *file = (ZipFileReaderOpenFile *)vfsOpenFile; _assert_(file); - _dbg_assert_(file->zf != nullptr); - return zip_fread(file->zf, buffer, length); + if (!file->zf) { + return 0; + } + // -1 on errors, like a bad CRC or a corrupt stream. + const zip_int64_t bytesRead = zip_fread(file->zf, buffer, length); + return bytesRead < 0 ? 0 : (size_t)bytesRead; } void ZipFileReader::CloseFile(VFSOpenFile *vfsOpenFile) { ZipFileReaderOpenFile *file = (ZipFileReaderOpenFile *)vfsOpenFile; _assert_(file); - _dbg_assert_(file->zf != nullptr); - zip_fclose(file->zf); + if (file->zf) { + zip_fclose(file->zf); + } file->zf = nullptr; vfsOpenFile = nullptr; lock_.unlock(); diff --git a/Common/Render/ManagedTexture.cpp b/Common/Render/ManagedTexture.cpp index 59469c119f..55600a6237 100644 --- a/Common/Render/ManagedTexture.cpp +++ b/Common/Render/ManagedTexture.cpp @@ -94,6 +94,18 @@ bool TempImage::LoadTextureLevelsFromFileData(const uint8_t *data, size_t size, switch (typeSuggestion) { case ImageFileType::ZIM: numLevels = LoadZIMPtr((const uint8_t *)data, size, width, height, &zimFlags, levels); + if (numLevels <= 0) { + numLevels = 0; + return false; + } + // Everything below treats the data as four bytes per pixel. + if ((zimFlags & ZIM_FORMAT_MASK) != ZIM_RGBA8888 || width[0] > maxWidth || height[0] > maxHeight) { + ERROR_LOG(Log::IO, "Unsupported ZIM: format %d, %dx%d", zimFlags & ZIM_FORMAT_MASK, width[0], height[0]); + free(levels[0]); + levels[0] = nullptr; + numLevels = 0; + return false; + } fmt = ZimToT3DFormat(zimFlags & ZIM_FORMAT_MASK); ConvertRGBA8888ToPremulAlpha((u32 *)levels[0], (const u32 *)levels[0], width[0] * height[0]); break; @@ -117,6 +129,22 @@ bool TempImage::LoadTextureLevelsFromFileData(const uint8_t *data, size_t size, case ImageFileType::JPEG: { int actual_components = 0; + if (size > INT_MAX) { + return false; + } + // Check the size before decoding. + { + jpgd::jpeg_decoder_mem_stream stream(data, (jpgd::uint)size); + jpgd::jpeg_decoder decoder(&stream); + if (decoder.get_error_code() != jpgd::JPGD_SUCCESS) { + ERROR_LOG(Log::IO, "Bad JPEG header"); + return false; + } + if (decoder.get_width() <= 0 || decoder.get_height() <= 0 || decoder.get_width() > maxWidth || decoder.get_height() > maxHeight) { + ERROR_LOG(Log::IO, "JPEG too large: %dx%d (max %dx%d)", decoder.get_width(), decoder.get_height(), maxWidth, maxHeight); + return false; + } + } unsigned char *jpegBuf = jpgd::decompress_jpeg_image_from_memory(data, (int)size, &width[0], &height[0], &actual_components, 4); if (jpegBuf) { numLevels = 1; diff --git a/Common/Render/Text/draw_text.cpp b/Common/Render/Text/draw_text.cpp index 8d2eb1a749..b4dcff9d33 100644 --- a/Common/Render/Text/draw_text.cpp +++ b/Common/Render/Text/draw_text.cpp @@ -117,7 +117,7 @@ void TextDrawer::DrawString(DrawBuffer &target, const FontStyle &style, float sc } else { DataFormat texFormat; // Pick between the supported formats, of which at least one is supported on each platform. Prefer R8 (but only if swizzle is supported) - bool emoji = SupportsColorEmoji() && AnyEmojiInString(str.data(), str.length()); + bool emoji = SupportsColorEmoji() && AnyEmojiInString(str, str.length()); if (emoji) { texFormat = Draw::DataFormat::R8G8B8A8_UNORM; } else if ((draw_->GetDataFormatSupport(Draw::DataFormat::R8_UNORM) & Draw::FMT_TEXTURE) != 0 && draw_->GetDeviceCaps().textureSwizzleSupported) { diff --git a/Common/Serialize/SerializeDeque.h b/Common/Serialize/SerializeDeque.h index 6c0aaad8d8..a41a634189 100644 --- a/Common/Serialize/SerializeDeque.h +++ b/Common/Serialize/SerializeDeque.h @@ -32,6 +32,15 @@ void DoDeque(PointerWrap &p, std::deque &x, T &default_val) { return; } } + // See DoVector. + if (p.mode == PointerWrap::MODE_READ && sizeof(T) > SerializeMinElemSize()) { + x.clear(); + for (u32 i = 0; i < deq_size && !p.Failed(); i++) { + x.push_back(default_val); + Do(p, x.back()); + } + return; + } x.resize(deq_size, default_val); u32 i; for (i = 0; i < deq_size; i++) diff --git a/Common/Serialize/SerializeFuncs.h b/Common/Serialize/SerializeFuncs.h index 07277b46a5..a8920951e0 100644 --- a/Common/Serialize/SerializeFuncs.h +++ b/Common/Serialize/SerializeFuncs.h @@ -18,6 +18,7 @@ #pragma once // Templates for save state serialization. See Serializer.h. +#include #include #include #include "Common/Serialize/Serializer.h" @@ -134,6 +135,18 @@ void DoVector(PointerWrap &p, std::vector &x, T &default_val) { return; } } + // Elements that aren't stored raw can serialize to a few bytes but be much bigger in memory, so + // the check above still lets a corrupt state ask for gigabytes. Grow as the data arrives instead. + if (p.mode == PointerWrap::MODE_READ && sizeof(T) > SerializeMinElemSize()) { + x.clear(); + for (u32 done = 0; done < vec_size && !p.Failed(); ) { + const u32 n = std::min(vec_size - done, (u32)1024); + x.resize(done + n, default_val); + DoArray(p, &x[done], n); + done += n; + } + return; + } if (vec_size != x.size()) x.resize(vec_size, default_val); if (vec_size > 0) diff --git a/Common/Serialize/SerializeList.h b/Common/Serialize/SerializeList.h index 3a37aaf891..48c02940d0 100644 --- a/Common/Serialize/SerializeList.h +++ b/Common/Serialize/SerializeList.h @@ -32,6 +32,15 @@ void DoList(PointerWrap &p, std::list &x, T &default_val) { return; } } + // See DoVector. + if (p.mode == PointerWrap::MODE_READ && sizeof(T) > SerializeMinElemSize()) { + x.clear(); + for (u32 i = 0; i < list_size && !p.Failed(); i++) { + x.push_back(default_val); + Do(p, x.back()); + } + return; + } x.resize(list_size, default_val); for (T &elem : x) diff --git a/Common/Serialize/Serializer.cpp b/Common/Serialize/Serializer.cpp index 065c05d71a..e17a3742af 100644 --- a/Common/Serialize/Serializer.cpp +++ b/Common/Serialize/Serializer.cpp @@ -503,18 +503,22 @@ CChunkFileReader::Error CChunkFileReader::LoadFile(const Path &filename, std::st if (header.Compress) { // Sanity cap on the decompressed size to avoid a giant allocation from - // an attacker-controlled header field. Real savestates are well under this. - if (header.UncompressedSize > 0x40000000) { + // an attacker-controlled header field. Real savestates (even with 64MB of + // RAM) are well under this. + if (header.UncompressedSize > 0x10000000) { ERROR_LOG(Log::SaveState, "ChunkReader: UncompressedSize too large: %u", header.UncompressedSize); return ERROR_BAD_FILE; } } if (SerializeCompressType(header.Compress) == SerializeCompressType::ZSTD) { - u8 *uncomp_buffer = new u8[header.UncompressedSize]; + u8 *uncomp_buffer = (u8 *)malloc(header.UncompressedSize); + if (!uncomp_buffer) { + return ERROR_BAD_FILE; + } if (!DecompressZstdFromFile(pFile, header.ExpectedSize, uncomp_buffer, header.UncompressedSize)) { ERROR_LOG(Log::SaveState, "ChunkReader: Failed to decompress file"); - delete [] uncomp_buffer; + free(uncomp_buffer); return ERROR_BAD_FILE; } _buffer = uncomp_buffer; @@ -525,16 +529,20 @@ CChunkFileReader::Error CChunkFileReader::LoadFile(const Path &filename, std::st // Uncompressed, or snappy (older states). sz = header.ExpectedSize; - u8 *buffer = new u8[sz]; - if (!pFile.ReadBytes(buffer, sz)) + u8 *buffer = (u8 *)malloc(sz); + if (!buffer || !pFile.ReadBytes(buffer, sz)) { ERROR_LOG(Log::SaveState, "ChunkReader: Error reading file"); - delete [] buffer; + free(buffer); return ERROR_BAD_FILE; } if (header.Compress) { - u8 *uncomp_buffer = new u8[header.UncompressedSize]; + u8 *uncomp_buffer = (u8 *)malloc(header.UncompressedSize); + if (!uncomp_buffer) { + free(buffer); + return ERROR_BAD_FILE; + } size_t uncomp_size = header.UncompressedSize; bool success = false; if (SerializeCompressType(header.Compress) == SerializeCompressType::SNAPPY) { @@ -545,19 +553,19 @@ CChunkFileReader::Error CChunkFileReader::LoadFile(const Path &filename, std::st } if (!success) { ERROR_LOG(Log::SaveState, "ChunkReader: Failed to decompress file"); - delete [] uncomp_buffer; - delete [] buffer; + free(uncomp_buffer); + free(buffer); return ERROR_BAD_FILE; } if ((u32)uncomp_size != header.UncompressedSize) { ERROR_LOG(Log::SaveState, "Size mismatch: file: %u calc: %u", header.UncompressedSize, (u32)uncomp_size); - delete [] uncomp_buffer; - delete [] buffer; + free(uncomp_buffer); + free(buffer); return ERROR_BAD_FILE; } _buffer = uncomp_buffer; sz = uncomp_size; - delete [] buffer; + free(buffer); } else { _buffer = buffer; } diff --git a/Common/Serialize/Serializer.h b/Common/Serialize/Serializer.h index 8a27297dc5..c132c62028 100644 --- a/Common/Serialize/Serializer.h +++ b/Common/Serialize/Serializer.h @@ -303,7 +303,7 @@ public: if (error == ERROR_NONE) { failureReason->clear(); error = LoadPtr(ptr, sz, _class, failureReason); - delete [] ptr; + free(ptr); INFO_LOG(Log::SaveState, "ChunkReader: Done loading '%s'", filename.c_str()); } else { WARN_LOG(Log::SaveState, "ChunkReader: Error found during load of '%s'", filename.c_str()); @@ -359,6 +359,7 @@ public: // The file half of Load() and Save(), for doing it away from the state itself. LoadFile hands back // a new[]'d buffer; SaveFile takes ownership of a malloc'd one (as from MeasureAndSavePtr). + // The buffer is allocated with malloc (so the allocation can fail cleanly), free it with free(). static Error LoadFile(const Path &filename, std::string *gitVersion, u8 *&buffer, size_t &sz); // Reads only the first size bytes of the state, decompressing no more than that. Not for snappy // compressed (old) states. diff --git a/Core/CwCheat.cpp b/Core/CwCheat.cpp index 96bb8fa6d9..0831a83f9f 100644 --- a/Core/CwCheat.cpp +++ b/Core/CwCheat.cpp @@ -1469,8 +1469,13 @@ void CWCheatEngine::Run() { // InterpretNextOp and ExecuteOp move i. for (size_t i = 0; i < cheat.lines.size(); ) { + const size_t start = i; CheatOperation op = InterpretNextCwCheat(cheat, i); ExecuteOp(op, cheat, i); + // A skip count near 2^32 wraps i on 32-bit hosts, which would run the same code forever. + if (i <= start) { + break; + } } } } diff --git a/Core/Debugger/LineInfo.cpp b/Core/Debugger/LineInfo.cpp index 412ff99bec..d55c4f438e 100644 --- a/Core/Debugger/LineInfo.cpp +++ b/Core/Debugger/LineInfo.cpp @@ -16,6 +16,7 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include #include #include @@ -83,19 +84,20 @@ public: return Fail(); } int64_t SLEB() { - int64_t result = 0; + // Unsigned, so shifting into (and past) the sign bit is defined. + uint64_t result = 0; int shift = 0; for (int i = 0; i < 10; i++) { const uint8_t b = U8(); if (!ok_) return 0; if (shift < 64) - result |= (int64_t)(b & 0x7f) << shift; + result |= (uint64_t)(b & 0x7f) << shift; shift += 7; if (!(b & 0x80)) { if (shift < 64 && (b & 0x40)) - result -= (int64_t)1 << shift; - return result; + result |= ~(uint64_t)0 << shift; + return (int64_t)result; } } return Fail(); @@ -147,6 +149,13 @@ enum { // One .debug_line unit: header, then a bytecode program that walks a virtual machine whose output // rows are (address, file, line). See the DWARF spec, "Line Number Information". +// Line 0 marks the end of a sequence, so real rows are at least 1. +static u32 ClampLine(int64_t line) { + return line > 0 ? (u32)std::min(line, UINT32_MAX) : 1; +} + +static constexpr size_t MAX_FILES = 1 << 16; + static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries, std::vector *files, std::map *fileIndex, u32 moduleStart, u32 moduleSize, u32 addressDelta) { const uint16_t version = r.U16(); @@ -158,6 +167,8 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries } const uint32_t headerLength = r.U32(); + if ((u64)r.pos() + headerLength > unitEnd) + return false; const size_t programStart = r.pos() + headerLength; const uint8_t minInstLength = r.U8(); @@ -192,6 +203,11 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries // Deduped across units so a header included by 500 files is stored once. auto it = fileIndex->find(name); if (it == fileIndex->end()) { + // Each one costs far more memory than its bytes in the file. + if (files->size() >= MAX_FILES) { + unitFiles.push_back(0); + continue; + } it = fileIndex->insert({ name, (u32)files->size() }).first; files->push_back(name); } @@ -204,8 +220,12 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries u32 address = 0; u32 file = 1; - int line = 1; + // 64 bits, so the file can't overflow it with line advances. + int64_t line = 1; bool sawAddress = false; + // Every one-byte special opcode can emit a row. After deduplication there's at most one per + // address, so more than a couple per instruction is a file trying to exhaust memory. + const size_t maxEntries = (size_t)moduleSize / 2 + 65536; auto emit = [&](u32 lineNo) { // Rows before a DW_LNE_set_address belong to no real code, and anything that doesn't land @@ -213,6 +233,8 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries const u32 finalAddress = addressDelta + address; if (!sawAddress || finalAddress < moduleStart || finalAddress - moduleStart >= moduleSize) return; + if (entries->size() >= maxEntries) + return; LineEntry e; e.address = finalAddress; e.line = lineNo; @@ -227,10 +249,14 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries const uint8_t adjusted = op - opcodeBase; address += (adjusted / lineRange) * minInstLength; line += lineBase + (adjusted % lineRange); - emit(line > 0 ? (u32)line : 1); + emit(ClampLine(line)); } else if (op == 0) { // Extended opcode, length-prefixed so unknown ones can be skipped. const uint64_t length = r.ULEB(); + // The length covers at least the sub-opcode. Check it before adding: a huge one wraps + // back to this opcode, which then loops forever. + if (!r.ok() || length == 0 || r.pos() > unitEnd || length > unitEnd - r.pos()) + return false; const size_t next = r.pos() + (size_t)length; const uint8_t sub = r.U8(); if (sub == DW_LNE_end_sequence) { @@ -248,13 +274,13 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries } else { switch (op) { case DW_LNS_copy: - emit(line > 0 ? (u32)line : 1); + emit(ClampLine(line)); break; case DW_LNS_advance_pc: address += (u32)r.ULEB() * minInstLength; break; case DW_LNS_advance_line: - line += (int)r.SLEB(); + line = std::clamp(line + std::clamp(r.SLEB(), INT32_MIN, INT32_MAX), INT32_MIN, UINT32_MAX); break; case DW_LNS_set_file: file = (u32)r.ULEB(); @@ -293,36 +319,46 @@ int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 module if (data[0] != ELFMAG0 || data[1] != ELFMAG1 || data[2] != ELFMAG2 || data[3] != ELFMAG3) return 0; - const Elf32_Ehdr *header = (const Elf32_Ehdr *)data; + // Copied out, since the offsets in the file needn't be aligned. Sums in 64 bits, so they can't + // wrap a 32-bit size_t. + Elf32_Ehdr headerCopy; + memcpy(&headerCopy, data, sizeof(headerCopy)); + const Elf32_Ehdr *header = &headerCopy; if (header->e_shoff == 0 || header->e_shnum == 0 || header->e_shentsize < sizeof(Elf32_Shdr)) return 0; - if ((size_t)header->e_shoff + (size_t)header->e_shnum * header->e_shentsize > size) + if ((u64)header->e_shoff + (u64)header->e_shnum * header->e_shentsize > size) return 0; if (header->e_shstrndx >= header->e_shnum) return 0; auto section = [&](int i) { - return (const Elf32_Shdr *)(data + header->e_shoff + (size_t)i * header->e_shentsize); + Elf32_Shdr s; + memcpy(&s, data + header->e_shoff + (size_t)i * header->e_shentsize, sizeof(s)); + return s; }; - const Elf32_Shdr *shstr = section(header->e_shstrndx); - if ((size_t)shstr->sh_offset + shstr->sh_size > size) + const Elf32_Shdr shstr = section(header->e_shstrndx); + if ((u64)shstr.sh_offset + shstr.sh_size > size) return 0; - const Elf32_Shdr *debugLine = nullptr; + static const char debugLineName[] = ".debug_line"; + Elf32_Shdr debugLine{}; + bool foundDebugLine = false; for (int i = 0; i < header->e_shnum; i++) { - const Elf32_Shdr *s = section(i); - if (s->sh_name >= shstr->sh_size) + const Elf32_Shdr s = section(i); + // The name, with its terminator, has to fit in the string table. + if (s.sh_name >= shstr.sh_size || shstr.sh_size - s.sh_name < sizeof(debugLineName)) continue; - const char *name = (const char *)data + shstr->sh_offset + s->sh_name; - if (!strcmp(name, ".debug_line")) { + const char *name = (const char *)data + shstr.sh_offset + s.sh_name; + if (!memcmp(name, debugLineName, sizeof(debugLineName))) { debugLine = s; + foundDebugLine = true; break; } } - if (!debugLine || debugLine->sh_size == 0) + if (!foundDebugLine || debugLine.sh_size == 0) return 0; - if ((size_t)debugLine->sh_offset + debugLine->sh_size > size) + if ((u64)debugLine.sh_offset + debugLine.sh_size > size) return 0; ModuleLines mod; @@ -331,9 +367,9 @@ int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 module mod.files.push_back(""); std::map fileIndex; - Reader r(data + debugLine->sh_offset, debugLine->sh_size); + Reader r(data + debugLine.sh_offset, debugLine.sh_size); int skippedUnits = 0; - while (r.ok() && !r.AtEnd(debugLine->sh_size)) { + while (r.ok() && !r.AtEnd(debugLine.sh_size)) { const size_t unitStart = r.pos(); const uint32_t unitLength = r.U32(); if (!r.ok() || unitLength == 0) @@ -342,9 +378,9 @@ int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 module // produces, and misreading it would walk off into nonsense. if (unitLength >= 0xfffffff0) break; - const size_t unitEnd = unitStart + 4 + unitLength; - if (unitEnd > debugLine->sh_size) + if ((u64)unitStart + 4 + unitLength > debugLine.sh_size) break; + const size_t unitEnd = unitStart + 4 + unitLength; if (!ParseUnit(r, unitEnd, &mod.entries, &mod.files, &fileIndex, moduleStart, moduleSize, addressDelta)) skippedUnits++; @@ -363,9 +399,16 @@ int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 module std::stable_sort(mod.entries.begin(), mod.entries.end(), [](const LineEntry &a, const LineEntry &b) { return a.address < b.address; }); - mod.entries.erase(std::unique(mod.entries.begin(), mod.entries.end(), [](const LineEntry &a, const LineEntry &b) { - return a.address == b.address; - }), mod.entries.end()); + // (std::unique would keep the first.) + size_t out = 0; + for (size_t i = 0; i < mod.entries.size(); i++) { + if (out > 0 && mod.entries[out - 1].address == mod.entries[i].address) { + mod.entries[out - 1] = mod.entries[i]; + } else { + mod.entries[out++] = mod.entries[i]; + } + } + mod.entries.resize(out); const int count = (int)mod.entries.size(); INFO_LOG(Log::Loader, "Line info: %d rows across %d files for module at %08x", diff --git a/Core/Debugger/SymbolMap.cpp b/Core/Debugger/SymbolMap.cpp index a82a1f1ccf..b76f951169 100644 --- a/Core/Debugger/SymbolMap.cpp +++ b/Core/Debugger/SymbolMap.cpp @@ -108,11 +108,10 @@ bool SymbolMap::LoadSymbolMap(const Path &filename) { if (p == NULL) break; - // Chop any newlines off. - for (size_t i = strlen(line) - 1; i > 0; i--) { - if (line[i] == '\r' || line[i] == '\n') { - line[i] = '\0'; - } + // Chop any newlines off. The line can be empty, if it starts with a NUL. + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n')) { + line[--len] = '\0'; } if (strlen(line) < 4 || sscanf(line, "%255s", temp) != 1) diff --git a/Core/Dialog/PSPGamedataInstallDialog.cpp b/Core/Dialog/PSPGamedataInstallDialog.cpp index b05414f500..117d76d105 100644 --- a/Core/Dialog/PSPGamedataInstallDialog.cpp +++ b/Core/Dialog/PSPGamedataInstallDialog.cpp @@ -213,7 +213,7 @@ void PSPGamedataInstallDialog::WriteSfoFile() { ParamSFOData sfoFile; std::string sfopath = GetGameDataInstallFileName(&request, SFO_FILENAME); std::vector sfoFileData; - if (pspFileSystem.ReadEntireFile(sfopath, sfoFileData) >= 0) { + if (pspFileSystem.ReadEntireFile(sfopath, sfoFileData, false, 1024 * 1024) >= 0) { sfoFile.ReadSFO(sfoFileData); } diff --git a/Core/ELF/ElfReader.cpp b/Core/ELF/ElfReader.cpp index 8f2f5b9afc..9edd90302e 100644 --- a/Core/ELF/ElfReader.cpp +++ b/Core/ELF/ElfReader.cpp @@ -15,6 +15,9 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. +#include +#include + #include "Common/StringUtils.h" #include "Common/Data/Text/Demangle.h" #include "Common/File/DirListing.h" @@ -42,11 +45,15 @@ const char *ElfReader::GetSectionName(int section) const { return nullptr; } const char *ptr = (const char *)GetSectionDataPtr(header->e_shstrndx); - - if (ptr) - return ptr + nameOffset; - else + if (!ptr) return nullptr; + // The name has to end before the file does. + const size_t maxLen = size_ - ((size_t)nameOffset + stringsOffset); + if (strnlen(ptr + nameOffset, maxLen) == maxLen) { + ERROR_LOG(Log::Loader, "ELF: Unterminated name in section %d", section); + return nullptr; + } + return ptr + nameOffset; } void addrToHiLo(u32 addr, u16 &hi, s16 &lo) @@ -63,13 +70,51 @@ void addrToHiLo(u32 addr, u16 &hi, s16 &lo) } } +bool ElfReader::ConsumeRelocBudget(u64 bytes) { + relocBytesProcessed_ += bytes; + if (relocBytesProcessed_ > size_) { + ERROR_LOG_REPORT(Log::Loader, "ELF: More relocation data than the file holds, ignoring the rest"); + return false; + } + return true; +} + bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) { + if (numRelocs <= 0) + return true; + if (!ConsumeRelocBudget((u64)numRelocs * sizeof(Elf32_Rel))) + return false; + std::vector relocOps; relocOps.resize(numRelocs); DEBUG_LOG(Log::Loader, "Loading %i relocations...", numRelocs); int numErrors = 0; + // A HI16 pairs with the next relocation that can serve as its LO16, skipping other HI16s. + // Whether one can doesn't depend on the HI16, so find them all in one backward pass, rather + // than scanning forward from every HI16 (quadratic, with a table of only HI16s). + auto isLoCandidate = [&](int t) { + int t_type = rels[t].r_info & 0xF; + // In MotorStorm: Arctic Edge (US), these are sometimes R_MIPS_16 (instead of LO16.) + // It appears the PSP takes any relocation that is not a HI16, but let's play it safe and + // skip the others, we've only seen R_MIPS_16. + if (t_type != R_MIPS_LO16 && t_type != R_MIPS_16) + return false; + // The candidate LO16 declares its own segment - use that rather than the HI16's, + // which is what the mismatch warning further down is there to detect. + int t_readwrite = (rels[t].r_info >> 8) & 0xff; + if (!SegmentIsLoaded(t_readwrite)) + return false; + return Memory::IsValidAddress(rels[t].r_offset + segmentVAddr[t_readwrite]); + }; + std::vector nextLo(numRelocs + 1, -1); + for (int t = numRelocs - 1; t >= 0; t--) { + nextLo[t] = isLoCandidate(t) ? t : nextLo[t + 1]; + } + // Relocations up to here have already been logged as skipped by an earlier HI16. + int loggedSkipsUpTo = 0; + { for (int r = 0; r < numRelocs; r++) { u32 info = rels[r].r_info; @@ -97,8 +142,8 @@ bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) { addr += segmentVAddr[readwrite]; - // It appears that misaligned relocations are allowed. - if (((addr & 3) && type != R_MIPS_32) || !Memory::IsValidAddress(addr)) { + // It appears that misaligned relocations are allowed. We read and write 4 bytes either way. + if (((addr & 3) && type != R_MIPS_32) || !Memory::IsValidRange(addr, 4)) { if (numErrors < 10) { WARN_LOG_REPORT(Log::Loader, "Suspicious address %08x, skipping reloc, type = %d", addr, type); } else if (numErrors == 10) { @@ -128,7 +173,7 @@ bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) { } addr += segmentVAddr[readwrite]; - if (((addr & 3) && type != R_MIPS_32) || !Memory::IsValidAddress(addr)) { + if (((addr & 3) && type != R_MIPS_32) || !Memory::IsValidRange(addr, 4)) { continue; } @@ -164,31 +209,29 @@ bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) { u32 cur = (op & 0xFFFF) << 16; u16 hi = 0; bool found = false; - for (int t = r + 1; t < numRelocs; t++) { - int t_type = rels[t].r_info & 0xF; - if (t_type == R_MIPS_HI16) + const int t = nextLo[r + 1]; + const int skipEnd = t < 0 ? numRelocs : t; + for (int s = std::max(r + 1, loggedSkipsUpTo); s < skipEnd; s++) { + int s_type = rels[s].r_info & 0xF; + int s_readwrite = (rels[s].r_info >> 8) & 0xff; + if (s_type == R_MIPS_HI16 || !SegmentIsLoaded(s_readwrite)) continue; - - // The candidate LO16 declares its own segment - use that rather than the HI16's, - // which is what the mismatch warning further down is there to detect. + u32 skippedAddr = rels[s].r_offset + segmentVAddr[s_readwrite]; + if (s_type != R_MIPS_LO16 && s_type != R_MIPS_16) { + // These exists in some popular games like Assassin's Creed: Bloodlines and GTA: VCS: (https://report.ppsspp.org/logs/kind/1187) + ERROR_LOG_REPORT(Log::Loader, "ELF relocation HI16/%d pair (instead of LO16) at %08x / %08x", s_type, addr, skippedAddr); + } else { + ERROR_LOG(Log::Loader, "Bad corrLoAddr %08x", skippedAddr); + } + } + loggedSkipsUpTo = std::max(loggedSkipsUpTo, skipEnd); + if (t >= 0) { + int t_type = rels[t].r_info & 0xF; int t_readwrite = (rels[t].r_info >> 8) & 0xff; - if (!SegmentIsLoaded(t_readwrite)) - continue; u32 corrLoAddr = rels[t].r_offset + segmentVAddr[t_readwrite]; - - // In MotorStorm: Arctic Edge (US), these are sometimes R_MIPS_16 (instead of LO16.) - // It appears the PSP takes any relocation that is not a HI16. - if (t_type != R_MIPS_LO16) { - if (t_type != R_MIPS_16) { - // Let's play it safe for now and skip. We've only seen this type. - // These exists in some popular games like Assassin's Creed: Bloodlines and GTA: VCS: (https://report.ppsspp.org/logs/kind/1187) - ERROR_LOG_REPORT(Log::Loader, "ELF relocation HI16/%d pair (instead of LO16) at %08x / %08x", t_type, addr, corrLoAddr); - continue; - } else { - WARN_LOG_REPORT(Log::Loader, "ELF relocation HI16/%d(16) pair (instead of LO16) at %08x / %08x", t_type, addr, corrLoAddr); - } + if (t_type == R_MIPS_16) { + WARN_LOG_REPORT(Log::Loader, "ELF relocation HI16/%d(16) pair (instead of LO16) at %08x / %08x", t_type, addr, corrLoAddr); } - // Should have matching index and segment info, according to llvm, which makes sense. if ((rels[t].r_info >> 8) != (rels[r].r_info >> 8)) { WARN_LOG_REPORT(Log::Loader, "ELF relocation HI16/LO16 with mismatching r_info lo=%08x, hi=%08x", rels[t].r_info, rels[r].r_info); @@ -196,16 +239,11 @@ bool ElfReader::LoadRelocations(const Elf32_Rel *rels, int numRelocs) { if (log) { DEBUG_LOG(Log::Loader, "Corresponding lo found at %08x", corrLoAddr); } - if (Memory::IsValidAddress(corrLoAddr)) { - s16 lo = (s16)relocOps[t]; - cur += lo; - cur += relocateTo; - addrToHiLo(cur, hi, lo); - found = true; - break; - } else { - ERROR_LOG(Log::Loader, "Bad corrLoAddr %08x", corrLoAddr); - } + s16 lo = (s16)relocOps[t]; + cur += lo; + cur += relocateTo; + addrToHiLo(cur, hi, lo); + found = true; } if (found) { op = (op & 0xFFFF0000) | hi; @@ -289,6 +327,8 @@ void ElfReader::LoadRelocations2(int rel_seg) ERROR_LOG_REPORT(Log::Loader, "Rel2 segment extends past the end of the file"); return; } + if (!ConsumeRelocBudget(ph->p_filesz)) + return; end = buf+ph->p_filesz; // Everything below reads forward from buf, so check there's something there each time. All of @@ -311,6 +351,12 @@ void ElfReader::LoadRelocations2(int rel_seg) while((1< 16) { + ERROR_LOG_REPORT(Log::Loader, "Rel2: bad field sizes %d/%d/%d", flag_bits, seg_bits, type_bits); + return; + } + buf += 4; // Both tables are prefixed by their own size, and are indexed by bitfields out of the command @@ -522,13 +568,12 @@ void ElfReader::LoadRelocations2(int rel_seg) int ElfReader::LoadInto(u32 loadAddress, bool fromTop) { - DEBUG_LOG(Log::Loader,"String section: %i", header->e_shstrndx); - if (size_ < sizeof(Elf32_Ehdr)) { ERROR_LOG(Log::Loader, "Truncated ELF header, %d bytes", (int)size_); // Probably not the right error code. return SCE_KERNEL_ERROR_MEMBLOCK_ALLOC_FAILED; } + DEBUG_LOG(Log::Loader,"String section: %i", header->e_shstrndx); if (header->e_ident[0] != ELFMAG0 || header->e_ident[1] != ELFMAG1 || header->e_ident[2] != ELFMAG2 || header->e_ident[3] != ELFMAG3) @@ -546,14 +591,14 @@ int ElfReader::LoadInto(u32 loadAddress, bool fromTop) { if (header->e_ident[EI_DATA] != ELFDATA2LSB) return SCE_KERNEL_ERROR_MEMBLOCK_ALLOC_FAILED; - const size_t phdrEnd = header->e_phoff + sizeof(Elf32_Phdr) * GetNumSegments(); - const size_t shdrEnd = header->e_shoff + sizeof(Elf32_Shdr) * GetNumSections(); + const u64 phdrEnd = (u64)header->e_phoff + sizeof(Elf32_Phdr) * GetNumSegments(); + const u64 shdrEnd = (u64)header->e_shoff + sizeof(Elf32_Shdr) * GetNumSections(); if (size_ < phdrEnd || size_ < shdrEnd) { // Say which table runs off the end and by how much. "Truncated ELF" on its own sends you // looking at the executable, when the usual cause is that we were handed fewer bytes than // the file really has - a short read, or a size clamped somewhere upstream. const char *which = size_ < phdrEnd ? "program header table" : "section header table"; - const size_t needed = size_ < phdrEnd ? phdrEnd : shdrEnd; + const u64 needed = size_ < phdrEnd ? phdrEnd : shdrEnd; loadError_ = StringFromFormat( "Truncated ELF: %s ends at %d but only %d bytes are available (%d short), %d sections, %d segments", which, (int)needed, (int)size_, (int)(needed - size_), GetNumSections(), GetNumSegments()); @@ -793,7 +838,7 @@ int ElfReader::LoadInto(u32 loadAddress, bool fromTop) { int numRelocs = s->sh_size / sizeof(Elf32_Rel); Elf32_Rel *rels = (Elf32_Rel *)GetSectionDataPtr(i); - if (GetSectionDataOffset(i) + sizeof(Elf32_Rel) * numRelocs > size_) + if ((u64)GetSectionDataOffset(i) + sizeof(Elf32_Rel) * (u64)numRelocs > size_) rels = nullptr; DEBUG_LOG(Log::Loader,"%s: Performing %i relocations on %s : offset = %08x", name, numRelocs, GetSectionName(sectionToModify), sections[i].sh_offset); @@ -845,7 +890,7 @@ int ElfReader::LoadInto(u32 loadAddress, bool fromTop) { int numRelocs = p->p_filesz / sizeof(Elf32_Rel); Elf32_Rel *rels = (Elf32_Rel *)GetSegmentPtr(i); - if (p->p_offset + p->p_filesz > size_) + if ((u64)p->p_offset + p->p_filesz > size_) rels = nullptr; if (!rels || !LoadRelocations(rels, numRelocs)) { ERROR_LOG(Log::Loader, "LoadInto: Relocs failed, trying anyway (2)"); diff --git a/Core/ELF/ElfReader.h b/Core/ELF/ElfReader.h index da369c86c0..dcd8ae8984 100644 --- a/Core/ELF/ElfReader.h +++ b/Core/ELF/ElfReader.h @@ -202,6 +202,10 @@ private: // array that callers are trusted to stay inside. std::vector segmentVAddr; size_t size_ = 0; + // Relocation data processed so far. Several relocation sections or segments can point at the + // same table, so this is capped at the file size to keep the total work linear. + u64 relocBytesProcessed_ = 0; + bool ConsumeRelocBudget(u64 bytes); u32 firstSegAlign = 0; std::string loadError_; }; diff --git a/Core/ELF/ParamSFO.cpp b/Core/ELF/ParamSFO.cpp index 3a7745af14..d0b6e717e8 100644 --- a/Core/ELF/ParamSFO.cpp +++ b/Core/ELF/ParamSFO.cpp @@ -146,7 +146,7 @@ bool ParamSFOData::ReadSFO(const u8 *paramsfo, size_t size) { // The index table itself must fit entirely within the buffer before we // can dereference entries; otherwise indexTables[i] reads out of bounds. - if (sizeof(Header) + (size_t)header->index_table_entries * sizeof(IndexTable) > size) { + if (sizeof(Header) + (u64)header->index_table_entries * sizeof(IndexTable) > size) { return false; } @@ -172,15 +172,17 @@ bool ParamSFOData::ReadSFO(const u8 *paramsfo, size_t size) { for (u32 i = 0; i < header->index_table_entries; i++) { - size_t key_offset = header->key_table_start + indexTables[i].key_table_offset; - if (key_offset >= size) { + const u64 key_offset64 = (u64)header->key_table_start + indexTables[i].key_table_offset; + if (key_offset64 >= size) { return false; } + const size_t key_offset = (size_t)key_offset64; - size_t data_offset = header->data_table_start + indexTables[i].data_table_offset; - if (data_offset >= size) { + const u64 data_offset64 = (u64)header->data_table_start + indexTables[i].data_table_offset; + if (data_offset64 >= size) { return false; } + const size_t data_offset = (size_t)data_offset64; std::string key = readStringCapped(key_offset, 0); if (key.empty()) @@ -200,7 +202,7 @@ bool ParamSFOData::ReadSFO(const u8 *paramsfo, size_t size) { case 0x0004: // Special format UTF-8 { - if (data_offset + indexTables[i].param_len > size) + if ((u64)data_offset + indexTables[i].param_len > size) continue; const u8 *utfdata = (const u8 *)(paramsfo + data_offset); VERBOSE_LOG(Log::Loader, "%s %s", key.c_str(), utfdata); @@ -242,7 +244,7 @@ int ParamSFOData::GetDataOffset(const u8 *paramsfo, size_t size, const char *dat WARN_LOG(Log::Loader, "Unexpected SFO header version: %08x", header->version); // Both the index table and the key/data tables must fit in the buffer. - if (sizeof(Header) + (size_t)header->index_table_entries * sizeof(IndexTable) > size) + if (sizeof(Header) + (u64)header->index_table_entries * sizeof(IndexTable) > size) return -1; if (header->key_table_start > size || header->data_table_start > size) return -1; @@ -254,19 +256,18 @@ int ParamSFOData::GetDataOffset(const u8 *paramsfo, size_t size, const char *dat for (u32 i = 0; i < header->index_table_entries; i++) { - // In size_t throughout - these are u32s from the file, and mixing them with int meant the - // bounds check below was done in whatever type the promotion landed on. ReadSFO does the - // same arithmetic this way. - size_t key_offset = header->key_table_start + indexTables[i].key_table_offset; + // In u64 - these are u32s from the file, and the sums can wrap a 32-bit size_t. + // ReadSFO does the same arithmetic this way. + const u64 key_offset = (u64)header->key_table_start + indexTables[i].key_table_offset; if (key_offset >= size) continue; - size_t data_offset = data_start + indexTables[i].data_table_offset; + const u64 data_offset = (u64)data_start + indexTables[i].data_table_offset; if (data_offset >= size) continue; const char *key = (const char *)(key_start + indexTables[i].key_table_offset); // Ensure the key string is NUL-terminated within the buffer before strcmp. - if (strnlen(key, size - key_offset) == size - key_offset) + if (strnlen(key, (size_t)(size - key_offset)) == size - key_offset) continue; if (!strcmp(key, dataName)) { diff --git a/Core/ELF/PrxDecrypter.cpp b/Core/ELF/PrxDecrypter.cpp index 6fb0c69668..debe02566a 100644 --- a/Core/ELF/PrxDecrypter.cpp +++ b/Core/ELF/PrxDecrypter.cpp @@ -972,7 +972,7 @@ static int pspDecryptType0(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz memcpy(reinterpret_cast(header)+sizeof(KIRK_CMD1_HEADER), type0.prxHeader, sizeof(type0.prxHeader)); decryptKirkHeaderType0(reinterpret_cast(header), type0.kirkBlock, xorbuf, pti->code); - if (!KirkOutputFits(header, size)) + if (!KirkOutputFits(header, size - offset)) { return -5; } @@ -1031,7 +1031,7 @@ static int pspDecryptType1(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz memcpy(reinterpret_cast(header)+sizeof(KIRK_CMD1_HEADER), type1.prxHeader, sizeof(type1.prxHeader)); decryptKirkHeaderType0(reinterpret_cast(header), type1.kirkBlock, xorbuf, pti->code); - if (!KirkOutputFits(header, size)) + if (!KirkOutputFits(header, size - offset)) { return -5; } @@ -1099,7 +1099,7 @@ static int pspDecryptType2(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz decryptKirkHeader(reinterpret_cast(header), type2.kirkHeader, xorbuf.cbegin()+0x10, pti->code); header->mode = 1; - if (!KirkOutputFits(header, size)) + if (!KirkOutputFits(header, size - offset)) { return -5; } @@ -1150,7 +1150,7 @@ static int pspFinishType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size decryptKirkHeader(reinterpret_cast(header), type5.kirkHeader, xorbuf.cbegin()+0x10, code); header->mode = 1; - if (!KirkOutputFits(header, size)) + if (!KirkOutputFits(header, size - offset)) { return -5; } @@ -1275,7 +1275,7 @@ static int pspDecryptType6(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz header->mode = 1; header->ecdsa_hash = 1; - if (!KirkOutputFits(header, size)) + if (!KirkOutputFits(header, size - offset)) { return -5; } @@ -1352,7 +1352,7 @@ static int pspDecryptType9(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz // branch only writes the mode word and zeroes the rest of that region. header->ecdsa_hash = 0; - if (!KirkOutputFits(header, size)) + if (!KirkOutputFits(header, size - offset)) { return -5; } @@ -1365,7 +1365,7 @@ static int pspDecryptType9(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz return decryptSize; } -int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed, const u32 *fuseId) +static int pspDecryptPRXAnyType(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed, const u32 *fuseId) { // Every type below reads the tag at 0xD0, the size at 0xB0 and key data as far as 0x150, and // writes a KIRK header into outbuf at a fixed offset derived from sizeof(PSP_Header). Without @@ -1423,6 +1423,17 @@ int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed, const u return pspDecryptType9(&kirk, inbuf, outbuf, size); } +int pspDecryptPRX(const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed, const u32 *fuseId) +{ + const int res = pspDecryptPRXAnyType(inbuf, outbuf, size, seed, fuseId); + // The decrypted size comes from the header, and callers size their reads of outbuf by it. + if (res > 0 && (u32)res > size) { + ERROR_LOG(Log::Loader, "PRX claims a decrypted size of %d, larger than its %d bytes", res, (int)size); + return -1; + } + return res; +} + bool pspUnwrapDNASPGD(std::vector &data, const u8 *secureId, const u32 *fuseId) { if (data.size() < 0x90 || memcmp(data.data(), "\0PGD", 4) != 0) { diff --git a/Core/FileLoaders/DiskCachingFileLoader.cpp b/Core/FileLoaders/DiskCachingFileLoader.cpp index 2e81f191f6..c40910de9e 100644 --- a/Core/FileLoaders/DiskCachingFileLoader.cpp +++ b/Core/FileLoaders/DiskCachingFileLoader.cpp @@ -87,11 +87,15 @@ size_t DiskCachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Prepare(); size_t readSize; - if (absolutePos >= filesize_) { + if (absolutePos < 0 || absolutePos >= filesize_) { bytes = 0; } else if (absolutePos + (s64)bytes >= filesize_) { bytes = (size_t)(filesize_ - absolutePos); } + // The cache computes the last block as pos + bytes - 1. + if (bytes == 0) { + return 0; + } if (cache_ && cache_->IsValid() && (flags & Flags::HINT_UNCACHED) == 0) { readSize = cache_->ReadFromCache(absolutePos, bytes, data); @@ -357,6 +361,7 @@ bool DiskCachingFileLoaderCache::MakeCacheSpaceFor(size_t blocks) { while (cacheSize_ > goal) { u16 minGeneration = generation_; + bool evicted = false; // We increment the iterator inside because we delete things inside. for (size_t i = 0; i < blockIndexLookup_.size(); ++i) { @@ -377,6 +382,7 @@ bool DiskCachingFileLoaderCache::MakeCacheSpaceFor(size_t blocks) { info.generation = 0; info.hits = 0; --cacheSize_; + evicted = true; // TODO: Doing this in chunks might be a lot better. WriteIndexData(blockIndexLookup_[i], info); @@ -389,6 +395,12 @@ bool DiskCachingFileLoaderCache::MakeCacheSpaceFor(size_t blocks) { } } + // If we found nothing to evict and the next pass would look for the same generation, + // it can't make progress either. + if (!evicted && minGeneration == oldestGeneration_) { + ERROR_LOG(Log::Loader, "Disk cache: Nothing left to evict, %d blocks counted", (int)cacheSize_); + break; + } // If we didn't find any, update to the lowest we did find. oldestGeneration_ = minGeneration; } @@ -548,6 +560,9 @@ bool DiskCachingFileLoaderCache::LoadCacheFile(const Path &path) { } else if (header.maxBlocks < MAX_BLOCKS_LOWER_BOUND || header.maxBlocks > MAX_BLOCKS_UPPER_BOUND) { // This means it's not in our safety bounds, reject. valid = false; + } else if (header.blockSize != DEFAULT_BLOCK_SIZE) { + // We only ever create these, and everything below divides by and allocates the block size. + valid = false; } // If it's valid, retain the file pointer. @@ -600,6 +615,12 @@ void DiskCachingFileLoaderCache::LoadCacheIndex() { if (index_[i].block == INVALID_BLOCK) { continue; } + // Two entries can't share a data block. Keeping both would serve one of them the wrong + // data, and count a block that eviction can't find (which then never finishes). + if (blockIndexLookup_[index_[i].block] != INVALID_INDEX) { + index_[i].block = INVALID_BLOCK; + continue; + } if (index_[i].generation < oldestGeneration_) { oldestGeneration_ = index_[i].generation; diff --git a/Core/FileLoaders/HTTPFileLoader.cpp b/Core/FileLoaders/HTTPFileLoader.cpp index 8830994ee6..c41a9c80b6 100644 --- a/Core/FileLoaders/HTTPFileLoader.cpp +++ b/Core/FileLoaders/HTTPFileLoader.cpp @@ -89,6 +89,11 @@ void HTTPFileLoader::Prepare() { } if (size_pos != header.npos) { filesize_ = atoll(&header[size_pos]); + // The caches size their indexes from this. Nothing we'd load is this big. + if (filesize_ < 0 || filesize_ > (64LL << 30)) { + ERROR_LOG(Log::Loader, "HTTP: Implausible Content-Length: %s", header.c_str()); + filesize_ = 0; + } } } if (startsWithNoCase(header, "Accept-Ranges:")) { diff --git a/Core/FileLoaders/PS1EbootFileLoader.cpp b/Core/FileLoaders/PS1EbootFileLoader.cpp index 98c768bc4b..dfa94532f1 100644 --- a/Core/FileLoaders/PS1EbootFileLoader.cpp +++ b/Core/FileLoaders/PS1EbootFileLoader.cpp @@ -428,13 +428,13 @@ bool PS1EbootFileLoader::ParseMds(const Path &mdsPath, const std::string &data) const u8 *d = (const u8 *)data.data(); const size_t size = data.size(); auto read32 = [&](size_t offset) -> u32 { - return offset + 4 <= size ? (d[offset] | (d[offset + 1] << 8) | (d[offset + 2] << 16) | ((u32)d[offset + 3] << 24)) : 0; + return offset <= size && size - offset >= 4 ? (d[offset] | (d[offset + 1] << 8) | (d[offset + 2] << 16) | ((u32)d[offset + 3] << 24)) : 0; }; if (size < 0x58 || memcmp(d, "MEDIA DESCRIPTOR", 16) != 0) { return false; } const u32 session = read32(0x50); - if (session + 0x18 > size) { + if ((size_t)session + 0x18 > size) { return false; } const u32 numBlocks = d[session + 0x0A]; @@ -466,6 +466,9 @@ bool PS1EbootFileLoader::ParseMds(const Path &mdsPath, const std::string &data) if (file < 0) { // "*.mdf" is the .mds's own name. const u32 nameBlock = read32(b + 0x34); + if ((size_t)nameBlock + 5 > size) { + return false; + } const u32 nameOffset = read32(nameBlock); std::string name; for (u32 p = nameOffset; p < size && d[p] != 0; p += d[nameBlock + 4] ? 2 : 1) { diff --git a/Core/FileLoaders/RamCachingFileLoader.cpp b/Core/FileLoaders/RamCachingFileLoader.cpp index 2e402ff1d4..c50255e4c5 100644 --- a/Core/FileLoaders/RamCachingFileLoader.cpp +++ b/Core/FileLoaders/RamCachingFileLoader.cpp @@ -88,6 +88,11 @@ size_t RamCachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, F void RamCachingFileLoader::InitCache() { std::lock_guard guard(blocksMutex_); + // The whole file has to fit in memory (and a size_t, which may be 32 bits). + if (filesize_ <= 0 || (u64)filesize_ > (u64)(SIZE_MAX - BLOCK_SIZE) || (u64)filesize_ > (8ULL << 30)) { + ERROR_LOG(Log::IO, "Can't cache a file of %lld bytes in RAM, will fall back to regular reads.", (long long)filesize_); + return; + } u32 blockCount = (u32)((filesize_ + BLOCK_SIZE - 1) >> BLOCK_SHIFT); // Overallocate for the last block. cache_ = (u8 *)malloc((size_t)blockCount << BLOCK_SHIFT); diff --git a/Core/FileLoaders/ZipFileLoader.cpp b/Core/FileLoaders/ZipFileLoader.cpp index 57e12bcf77..8c6bf1899f 100644 --- a/Core/FileLoaders/ZipFileLoader.cpp +++ b/Core/FileLoaders/ZipFileLoader.cpp @@ -74,6 +74,12 @@ bool ZipFileLoader::Initialize(int fileIndex) { } _dbg_assert_(zstat.index == fileIndex); + // The whole file is decompressed into memory. No disc image is anywhere near 4GB, and the + // size has to fit in a (possibly 32-bit) size_t too. + if (!(zstat.valid & ZIP_STAT_SIZE) || zstat.size > (4ULL << 30) || zstat.size > (u64)SIZE_MAX) { + ERROR_LOG(Log::IO, "File index %d in ZIP has an implausible size (%llu)", fileIndex, (unsigned long long)zstat.size); + return false; + } dataFileSize_ = zstat.size; dataFile_ = zip_fopen_index(zipArchive_, zstat.index, ZIP_FL_UNCHANGED); if (!dataFile_) { diff --git a/Core/FileSystems/BlobFileSystem.cpp b/Core/FileSystems/BlobFileSystem.cpp index 7139c2fcad..586fa87982 100644 --- a/Core/FileSystems/BlobFileSystem.cpp +++ b/Core/FileSystems/BlobFileSystem.cpp @@ -92,6 +92,10 @@ size_t BlobFileSystem::SeekFile(u32 handle, s32 position, FileMove type) { entry->second = fileLoader_->FileSize() + position; break; } + // Reads take the position as a file offset. + if (entry->second < 0) { + entry->second = 0; + } return (size_t)entry->second; } return 0; diff --git a/Core/FileSystems/BlockDevices.cpp b/Core/FileSystems/BlockDevices.cpp index e87adbaf31..aa454b95da 100644 --- a/Core/FileSystems/BlockDevices.cpp +++ b/Core/FileSystems/BlockDevices.cpp @@ -99,7 +99,7 @@ static bool ParseUDFFileEntryExtent(FileLoader *fileLoader, u32 sector, UDFShort u32 extAttrLen = ReadLE32(block.data() + 0xA8); u32 allocDescLen = ReadLE32(block.data() + 0xAC); - u32 allocDescOffset = 0xB0 + extAttrLen; + const u64 allocDescOffset = 0xB0 + (u64)extAttrLen; if (allocDescLen < 8 || allocDescOffset + 8 > block.size()) return false; @@ -110,11 +110,14 @@ static bool ParseUDFRootDirectory(FileLoader *fileLoader, u32 sector, u32 partit UDFShortAd extent{}; if (!ParseUDFFileEntryExtent(fileLoader, sector, &extent)) return false; - if (extent.length == 0) + // A root directory is a few KB, so this cap is very generous. + if (extent.length == 0 || extent.length > 1024 * 1024) return false; + const u64 offset = ((u64)partitionStart + extent.position) * 2048; + if (offset + extent.length > fileLoader->FileSize()) + return false; dirData->resize(extent.length); - const u64 offset = (u64)(partitionStart + extent.position) * 2048; return fileLoader->ReadAt(offset, 1, dirData->size(), dirData->data()) == dirData->size(); } @@ -191,7 +194,8 @@ static bool FindUDFLayerFileEntrySectors(FileLoader *fileLoader, u32 rootSector, if (entryLen == 0 || offset + entryLen > dirData.size()) break; - if (tag == 0x0101 && crcLen >= 20) { + // The fixed part of a File Identifier Descriptor is 38 bytes. + if (tag == 0x0101 && crcLen >= 22) { u8 fileIdLen = dirData[offset + 19]; u16 implUseLen = ReadLE16(&dirData[offset + 36]); size_t nameOffset = offset + 38 + implUseLen; @@ -567,6 +571,10 @@ CISOFileBlockDevice::CISOFileBlockDevice(FileLoader *fileLoader) return; } const u64 totalSize = hdr.total_bytes; + if (totalSize > MAX_CSO_IMAGE_SIZE) { + errorString_ = "Invalid CSO header (image is too large)"; + return; + } // Compute the counts without overflowing the 64-bit total size, then // validate them before truncating to the 32-bit fields used by the CSO // block reader. In particular, numFrames + 1 must remain representable. @@ -739,6 +747,9 @@ bool CISOFileBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) } bool CISOFileBlockDevice::ReadBlocks(u32 minBlock, int count, u8 *outPtr) { + if (count <= 0) { + return true; + } if (count == 1) { return ReadBlock(minBlock, outPtr); } @@ -801,7 +812,18 @@ bool CISOFileBlockDevice::ReadBlocks(u32 minBlock, int count, u8 *outPtr) { } if (plain) { - memcpy(outPtr, rawBuffer + frameBlockOffset * GetBlockSize(), frameBlocks * GetBlockSize()); + // The frame's data may be shorter than a full frame (or the read came up short), + // so only copy what's actually in the buffer. + const u64 available = std::min(frameReadEnd, readBufferEnd) - frameReadPos; + const u64 copyStart = (u64)frameBlockOffset * GetBlockSize(); + const u64 copyBytes = (u64)frameBlocks * GetBlockSize(); + const u64 copied = available > copyStart ? std::min(available - copyStart, copyBytes) : 0; + if (copied) { + memcpy(outPtr, rawBuffer + copyStart, (size_t)copied); + } + if (copied < copyBytes) { + memset(outPtr + copied, 0, (size_t)(copyBytes - copied)); + } } else { z.avail_in = frameReadSize; z.next_out = frameBlocks == blocksPerFrame ? outPtr : zlibBuffer; @@ -980,14 +1002,15 @@ bool NPDRMDemoBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) } int lba = blockNumber - currentBlock_; - if (lba >= 0 && lba < blockLBAs_){ + if (currentBlock_ >= 0 && lba >= 0 && lba < blockLBAs_){ memcpy(outPtr, blockBuf_ + lba*2048, 2048); return true; } int block = blockNumber / blockLBAs_; lba = blockNumber % blockLBAs_; - currentBlock_ = block * blockLBAs_; + // Only marked valid once blockBuf_ has been filled successfully. + currentBlock_ = -1; // blockNumber comes from the caller (ultimately from guest-controlled reads, e.g. // via /sce_lbn.../_size... on a raw sector open) and isn't otherwise guaranteed to be @@ -997,6 +1020,7 @@ bool NPDRMDemoBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) } if (table_[block].unk_1c != 0) { + memset(outPtr, 0, 2048); if((u32)block == (numBlocks_ - 1)) return true; // demos make by fake_np else @@ -1019,6 +1043,7 @@ bool NPDRMDemoBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) size_t readSize = fileLoader_->ReadAt(psarOffset+table_[block].offset, 1, table_[block].size, readBuf, flags); if (readSize != (size_t)table_[block].size){ + memset(outPtr, 0, 2048); if((u32)block==(numBlocks_-1)) return true; else @@ -1047,6 +1072,7 @@ bool NPDRMDemoBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) } } + currentBlock_ = block * blockLBAs_; memcpy(outPtr, blockBuf_+lba*2048, 2048); return true; } @@ -1091,10 +1117,13 @@ chd_file *OpenCHDThroughLoader(FileLoader *fileLoader, std::string *error) { coreFile->core.fread = [](void *out_data, size_t size, size_t count, core_file *file) { ExtendedCoreFile *coreFile = (ExtendedCoreFile *)file; FileLoader *loader = (FileLoader *)file->argp; + if (size == 0) { + return (size_t)0; + } uint64_t totalSize = size * count; - loader->ReadAt(coreFile->seekPos, totalSize, out_data); - coreFile->seekPos += totalSize; - return size * count; + size_t bytesRead = loader->ReadAt(coreFile->seekPos, totalSize, out_data); + coreFile->seekPos += bytesRead; + return bytesRead / size; }; coreFile->core.fclose = [](core_file *file) { ExtendedCoreFile *coreFile = (ExtendedCoreFile *)file; @@ -1173,6 +1202,15 @@ CHDFileBlockDevice::CHDFileBlockDevice(FileLoader *fileLoader) impl_->chd = file; impl_->header = chd_get_header(impl_->chd); + // We copy 2048 bytes out of each unit, and need at least one unit per hunk + // (otherwise the block math below divides by zero or reads outside the hunk). + if (impl_->header->unitbytes < 2048 || impl_->header->hunkbytes < impl_->header->unitbytes) { + errorString_ = StringFromFormat("CHD has unsupported unit size %d (hunk size %d)", impl_->header->unitbytes, impl_->header->hunkbytes); + chd_close(file); + impl_->chd = nullptr; + return; + } + readBuffer = new u8[impl_->header->hunkbytes]; currentHunk = -1; blocksPerHunk = impl_->header->hunkbytes / impl_->header->unitbytes; @@ -1205,6 +1243,9 @@ bool CHDFileBlockDevice::ReadBlock(int blockNumber, u8 *outPtr, bool uncached) { if (err != CHDERR_NONE) { ERROR_LOG(Log::Loader, "CHD read failed: %d %d %s", blockNumber, hunk, chd_error_string(err)); NotifyReadError(); + currentHunk = -1; + memset(outPtr, 0, GetBlockSize()); + return false; } currentHunk = hunk; } diff --git a/Core/FileSystems/BlockDevices.h b/Core/FileSystems/BlockDevices.h index ce5bb9aba4..d059eb50c9 100644 --- a/Core/FileSystems/BlockDevices.h +++ b/Core/FileSystems/BlockDevices.h @@ -65,6 +65,10 @@ protected: std::string errorString_; }; +// A dual-layer UMD holds 1.8 GB, so this is generous. A CSO header can claim any size, +// independent of how big the file is. +constexpr u64 MAX_CSO_IMAGE_SIZE = 8ULL * 1024 * 1024 * 1024; + class CISOFileBlockDevice : public BlockDevice { public: CISOFileBlockDevice(FileLoader *fileLoader); diff --git a/Core/FileSystems/CSOCompress.cpp b/Core/FileSystems/CSOCompress.cpp index 1337efd5b2..a4e9cffcd0 100644 --- a/Core/FileSystems/CSOCompress.cpp +++ b/Core/FileSystems/CSOCompress.cpp @@ -187,6 +187,13 @@ bool Compress(const Path &input, const Path &output, Preset preset, const Progre return false; } + // No PSP disc is anywhere near this. The size can come from a CSO header, which can claim + // far more than the file holds, and the index below is sized from it. + if (src.size > MAX_CSO_IMAGE_SIZE) { + *error = "Input image is too large"; + return false; + } + const PresetSettings settings = SettingsFor(preset); const uint32_t blockSize = settings.blockSize; const uint32_t sectorsPerBlock = blockSize / SECTOR_SIZE; diff --git a/Core/FileSystems/ISOFileSystem.cpp b/Core/FileSystems/ISOFileSystem.cpp index b0812b1f11..56c9412eb1 100644 --- a/Core/FileSystems/ISOFileSystem.cpp +++ b/Core/FileSystems/ISOFileSystem.cpp @@ -233,7 +233,17 @@ std::string ISOFileSystem::TreeEntry::BuildPath() { } void ISOFileSystem::ReadDirectory(TreeEntry *root) const { - for (u32 secnum = root->startsector, endsector = root->startsector + (root->dirsize + 2047) / 2048; secnum < endsector; ++secnum) { + // Real directories are a few KB. The size comes from the image, so cap it to keep a corrupt + // one from making us parse (and allocate entries for) gigabytes of sectors. + const u32 MAX_DIRECTORY_SIZE = 4 * 1024 * 1024; + u32 dirsize = root->dirsize; + if (dirsize > MAX_DIRECTORY_SIZE) { + blockDevice->NotifyReadError(); + ERROR_LOG(Log::FileSystem, "Directory '%s' claims an implausible size (%u), truncating", root->name.c_str(), dirsize); + dirsize = MAX_DIRECTORY_SIZE; + } + const u32 endsector = (u32)std::min((u64)root->startsector + (dirsize + 2047) / 2048, blockDevice->GetNumBlocks()); + for (u32 secnum = root->startsector; secnum < endsector; ++secnum) { u8 theSector[2048]; if (!blockDevice->ReadBlock(secnum, theSector)) { blockDevice->NotifyReadError(); @@ -252,9 +262,10 @@ void ISOFileSystem::ReadDirectory(TreeEntry *root) const { break; const int IDENTIFIER_OFFSET = 33; - if (offset + IDENTIFIER_OFFSET + dir.identifierLength > 2048) { + if (offset + IDENTIFIER_OFFSET > 2048 || offset + IDENTIFIER_OFFSET + dir.identifierLength > 2048) { blockDevice->NotifyReadError(); ERROR_LOG(Log::FileSystem, "Directory entry crosses sectors, corrupt iso?"); + root->valid = true; // Prevents re-reading (and duplicating the children so far) return; } // dir.size (the record length actually consumed) must cover at least its @@ -265,6 +276,7 @@ void ISOFileSystem::ReadDirectory(TreeEntry *root) const { if (dir.size < IDENTIFIER_OFFSET + dir.identifierLength) { blockDevice->NotifyReadError(); ERROR_LOG(Log::FileSystem, "Directory entry size too small, corrupt iso?"); + root->valid = true; return; } @@ -522,7 +534,9 @@ int ISOFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outd u8 *out = Memory::GetPointerWriteRangeOrException(outdataPtr, size); int blocks = size / blockDevice->GetBlockSize(); - blockDevice->ReadBlocks(block, blocks, out); + if (blocks > 0) { + blockDevice->ReadBlocks(block, blocks, out); + } size -= blocks * blockDevice->GetBlockSize(); out += blocks * blockDevice->GetBlockSize(); diff --git a/Core/FileSystems/MetaFileSystem.cpp b/Core/FileSystems/MetaFileSystem.cpp index 999d21de2d..37bc651953 100644 --- a/Core/FileSystems/MetaFileSystem.cpp +++ b/Core/FileSystems/MetaFileSystem.cpp @@ -616,7 +616,7 @@ size_t MetaFileSystem::SeekFile(u32 handle, s32 position, FileMove type) return 0; } -int MetaFileSystem::ReadEntireFile(const std::string &filename, std::vector &data, bool quiet) { +int MetaFileSystem::ReadEntireFile(const std::string &filename, std::vector &data, bool quiet, size_t maxSize) { FileAccess access = FILEACCESS_READ; if (quiet) { access = (FileAccess)(access | FILEACCESS_PPSSPP_QUIET); @@ -626,8 +626,13 @@ int MetaFileSystem::ReadEntireFile(const std::string &filename, std::vector return handle; SeekFile(handle, 0, FILEMOVE_END); - size_t dataSize = GetSeekPos(handle); + const s64 fileSize = GetSeekPos(handle); SeekFile(handle, 0, FILEMOVE_BEGIN); + if (fileSize < 0 || (u64)fileSize > maxSize) { + CloseFile(handle); + return SCE_KERNEL_ERROR_ERROR; + } + const size_t dataSize = (size_t)fileSize; data.resize(dataSize); size_t result = ReadFile(handle, data.data(), dataSize); diff --git a/Core/FileSystems/MetaFileSystem.h b/Core/FileSystems/MetaFileSystem.h index be79268f0b..c4ecd6590d 100644 --- a/Core/FileSystems/MetaFileSystem.h +++ b/Core/FileSystems/MetaFileSystem.h @@ -151,7 +151,8 @@ public: u64 FreeDiskSpace(const std::string &path) override; // Convenience helper - returns < 0 on failure. - int ReadEntireFile(const std::string &filename, std::vector &data, bool quiet = false); + // Fails (without allocating) if the file is larger than maxSize. + int ReadEntireFile(const std::string &filename, std::vector &data, bool quiet = false, size_t maxSize = (size_t)-1); void SetStartingDirectory(std::string_view dir) { std::lock_guard guard(lock); diff --git a/Core/FileSystems/tlzrc.cpp b/Core/FileSystems/tlzrc.cpp index 980495c5c4..a9b5f08fef 100644 --- a/Core/FileSystems/tlzrc.cpp +++ b/Core/FileSystems/tlzrc.cpp @@ -177,7 +177,9 @@ static int rc_bittree(LZRC_DECODE *rc, u8 *probs, int limit) */ static int rc_number(LZRC_DECODE *rc, u8 *prob, int n) { - int i, number = 1; + // Unsigned, since corrupt data can ask for more direct bits than fit (it then wraps). + int i; + u32 number = 1; if(n>3){ number = (number<<1)+rc_bit(rc, prob+3); @@ -210,7 +212,7 @@ static int rc_number(LZRC_DECODE *rc, u8 *prob, int n) } } - return number; + return (int)number; } @@ -305,13 +307,15 @@ int lzrc_decompress(void *out, int out_len, void *in, int in_len) } /* copy match bytes */ - if(match_dist>rc.out_ptr || match_dist<0){ + if(match_dist>rc.out_ptr || match_dist<=0){ printf("match_dist out of range! %08x\n", match_dist); return -1; } match_src = rc.output+rc.out_ptr-match_dist; for(i=0; ifile_offset/pgd->block_size; offset = pgd->file_offset%pgd->block_size; - if (size > (int)pgd->data_size) - size = (int)pgd->data_size; + // Unsigned: data_size comes from the (forgeable) header, and a value over 2^31 turned the + // clamp into a huge read. Only what's left of the file can be read, too. + const u32 available = pgd->file_offset < pgd->data_size ? pgd->data_size - pgd->file_offset : 0; + if (size < 0) + size = 0; + if ((u32)size > available) + size = (int)available; remain_size = size; KirkState *kirk = __ChnnlsvKirkState(); @@ -3093,6 +3098,11 @@ int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 out pspFileSystem.SeekFile(f->handle, (s32)f->pgd_offset, FILEMOVE_BEGIN); pspFileSystem.ReadFile(f->handle, pgd_header, 0x90); KirkState *kirk = __ChnnlsvKirkState(); + // Calling this again would otherwise leak the previous one. + if (f->pgdInfo) { + pgd_close(f->pgdInfo); + f->pgdInfo = nullptr; + } f->pgdInfo = pgd_open(kirk, pgd_header, 2, key_ptr); if (!f->pgdInfo) { f->npdrm = false; diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 60c92f70e7..40abb0bf55 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1256,14 +1256,14 @@ static bool KernelImportModuleFuncs(PSPModule *module, u32 *firstImportStubAddr, return true; } -static int gzipDecompress(u8 *OutBuffer, int OutBufferLength, u8 *InBuffer) { +static int gzipDecompress(u8 *OutBuffer, int OutBufferLength, const u8 *InBuffer, int InBufferLength) { int err; z_stream stream; u8 *outBufferPtr; outBufferPtr = OutBuffer; - stream.next_in = InBuffer; - stream.avail_in = (uInt)OutBufferLength; + stream.next_in = (Bytef *)InBuffer; + stream.avail_in = (uInt)InBufferLength; stream.next_out = outBufferPtr; stream.avail_out = (uInt)OutBufferLength; stream.zalloc = (alloc_func)0; @@ -1629,7 +1629,8 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load if (Read32(ptr) == SCE_MAGIC) { // "~SCE" const u32 headerSize = *(const u32_le*)(ptr + 4); - if (headerSize < elfSize) { + // Leave at least the magic read below. + if (headerSize < elfSize && elfSize - headerSize >= 4) { // Parse and print the lib info parsePrxLibInfo(ptr, headerSize); @@ -1646,11 +1647,13 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load DEBUG_LOG(Log::sceModule, "Decrypting ~PSP file"); head = (const PSP_Header *)ptr; devkitVersion = head->devkitversion; + // Not necessarily terminated. + const std::string headModName(head->modname, strnlen(head->modname, sizeof(head->modname))); bool wasDisabled; - if (ShouldHLEModuleForLoad(head->modname, &wasDisabled)) { + if (ShouldHLEModuleForLoad(headModName, &wasDisabled)) { int ver = (head->module_ver_hi << 8) | head->module_ver_lo; - INFO_LOG(Log::sceModule, "Loading module %s with version %04x, devkit %08x, crc %x", head->modname, ver, head->devkitversion, module->crc); + INFO_LOG(Log::sceModule, "Loading module %s with version %04x, devkit %08x, crc %x", headModName.c_str(), ver, head->devkitversion, module->crc); // TODO: Don't conflate these! reportedModule = true; @@ -1658,7 +1661,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load } if (wasDisabled) { - g_OSD.Show(OSDType::MESSAGE_WARNING, StringFromFormat("HLE for '%s' has been manually disabled", head->modname)); + g_OSD.Show(OSDType::MESSAGE_WARNING, StringFromFormat("HLE for '%s' has been manually disabled", headModName.c_str())); } const u8 *in = ptr; const bool isCompressed = (head->comp_attribute & 1) != 0; @@ -1673,7 +1676,17 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load return nullptr; } const auto maxElfSize = std::max(head->elf_size, head->psp_size); - newptr = new u8[maxElfSize]; + // elf_size isn't bounded by anything else. Nothing this big fits in PSP RAM. + if (maxElfSize > 64 * 1024 * 1024) { + *error_string = StringFromFormat("ELF/PRX corrupt, unreasonable size: %u", (u32)maxElfSize); + module->Cleanup(); + kernelObjects.Destroy(module->GetUID()); + error = SCE_KERNEL_ERROR_FILEERR; + return nullptr; + } + // Zeroed, since decryption and decompression can fill less than all of it and the ELF + // reader is handed the whole buffer. + newptr = new u8[maxElfSize](); elfSize = maxElfSize; ptr = newptr; int decryptedSize = pspDecryptPRX(in, (u8*)ptr, head->psp_size, prxSeed, fuseId); @@ -1722,13 +1735,13 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load } else { scheme = "gzip"; _dbg_assert_(Read32(ptr + 0x150) != ELF_MAGIC); - outBytes = gzipDecompress((u8 *)ptr, maxElfSize, temp); + outBytes = gzipDecompress((u8 *)ptr, maxElfSize, temp, decryptedSize); } free(temp); if (outBytes < 0) { // Bail out cleanly rather than falling through to parse whatever's left in the // buffer (still compressed, not a valid ELF) as if it were real code. - *error_string = StringFromFormat("Module '%s' %s decompression failed", head->modname, scheme); + *error_string = StringFromFormat("Module '%s' %s decompression failed", headModName.c_str(), scheme); delete [] newptr; module->Cleanup(); kernelObjects.Destroy(module->GetUID()); @@ -1736,7 +1749,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load error = SCE_KERNEL_ERROR_FILEERR; return nullptr; } - INFO_LOG(Log::sceModule, "'%s' is %s-compressed, decompressing (%d -> %d bytes, bufmax=%d).", head->modname, scheme, decryptedSize, outBytes, maxElfSize); + INFO_LOG(Log::sceModule, "'%s' is %s-compressed, decompressing (%d -> %d bytes, bufmax=%d).", headModName.c_str(), scheme, decryptedSize, outBytes, maxElfSize); } if (fakeLoadedModule) { @@ -1745,7 +1758,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load std::string elfFilename(KeepAfterLast(filename, '/')); if (elfFilename.empty() || startsWith(elfFilename, "sce_lbn")) { // Use the name from the header. - elfFilename = head->modname; + elfFilename = headModName; } DumpFileIfEnabled(ptr, (u32)elfSize, elfFilename.c_str(), DumpFileType::PRX); @@ -1884,7 +1897,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load else modinfoaddr = reader.GetSegmentVaddr(0) + (reader.GetSegmentPaddr(0) & 0x7FFFFFFF) - reader.GetSegmentOffset(0); - if (!Memory::IsValidAddress(modinfoaddr)) { + if (!Memory::IsValidRange(modinfoaddr, sizeof(PspModuleInfo))) { *error_string = StringFromFormat("Bad module info address %08x", modinfoaddr); ERROR_LOG(Log::sceModule, "Bad module info address %08x", modinfoaddr); delete[] newptr; @@ -1895,6 +1908,8 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load } modinfo = (const PspModuleInfo *)Memory::GetPointerUnchecked(modinfoaddr); + // Not necessarily terminated. + const std::string modInfoName(modinfo->name, strnlen(modinfo->name, sizeof(modinfo->name))); module->nm.nsegment = reader.GetNumSegments(); module->nm.attribute = modinfo->moduleAttrs; @@ -1902,9 +1917,9 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load // name check is for firmware 1.50, whose vshmain.prx declares no attributes at all - Sony // only started setting PSP_MODULE_VSH_MODE in 1.52. Without it the whole VSH bootstrap // below was skipped and the shell ran with none of its support modules loaded. - if ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0 || equals(modinfo->name, "vsh_module")) { + if ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0 || modInfoName == "vsh_module") { g_runningVSH = true; - INFO_LOG(Log::sceModule, "VSH mode module detected: %s", modinfo->name); + INFO_LOG(Log::sceModule, "VSH mode module detected: %s", modInfoName.c_str()); // The shell booted without a game: its drive starts out empty, for a disc to go in later. if (!pspFileSystem.GetSystem("disc0:")) { __UmdEject(); @@ -1913,7 +1928,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load // OK, even if it's an ELF module, it might be one we shouldn't fully load and execute! // This is seen with mpeg.prx in Tony Hawk's Underground 2, see #20568. - if (ShouldHLEModuleForLoad(modinfo->name)) { + if (ShouldHLEModuleForLoad(modInfoName)) { // We load it, but at least we don't run any part of it. module->isFake = true; } @@ -1988,7 +2003,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load module->libstub = modinfo->libstub; module->libstubend = modinfo->libstubend; - INFO_LOG(Log::Loader, "Module %s: %08x %08x %08x", modinfo->name, modinfo->gp, modinfo->libent, modinfo->libstub); + INFO_LOG(Log::Loader, "Module %s: %08x %08x %08x", modInfoName.c_str(), modinfo->gp, modinfo->libent, modinfo->libstub); DEBUG_LOG(Log::Loader,"==================================================="); u32 firstImportStubAddr = 0; @@ -2098,16 +2113,24 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load const u32_le *entPos = nullptr; const u32_le *entEnd = nullptr; + const u32_le *entBegin = nullptr; // A module that wasn't relocated (cut off before its relocations, see LoadInto) still has // link-time addresses here. if (Memory::IsValidRange(modinfo->libent, modinfo->libentend - modinfo->libent)) { entPos = (const u32_le *)Memory::GetPointerUnchecked(modinfo->libent); entEnd = (const u32_le *)Memory::GetPointerUnchecked(modinfo->libentend); + entBegin = entPos; } else { WARN_LOG(Log::Loader, "Garbage libent address %08x or end %08x, no exports", modinfo->libent, modinfo->libentend); } for (int m = 0; entPos < entEnd; ++m) { + const u32 entAddr = modinfo->libent + (u32)((const u8 *)entPos - (const u8 *)entBegin); + // Read as a whole struct below, even when the entry is shorter. + if (!Memory::IsValidRange(entAddr, sizeof(PspLibEntEntry))) { + WARN_LOG_REPORT(Log::Loader, "Export entry at %08x runs off the end of memory", entAddr); + break; + } const PspLibEntEntry *ent = (const PspLibEntEntry *)entPos; entPos += ent->size; if (ent->size == 0) { @@ -2118,7 +2141,7 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load const u32 variableCount = ent->size <= 4 ? ent->vcount : std::max((u32)ent->vcount , (u32)ent->vcountNew); const char *name; - if (Memory::IsValidAddress(ent->name)) { + if (ent->name != 0 && Memory::IsValidNullTerminatedString(ent->name)) { name = Memory::GetCharPointer(ent->name); } else if (ent->name == 0) { name = module->nm.name; @@ -2128,9 +2151,11 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load INFO_LOG(Log::Loader, "Exporting ent %d named %s, %d funcs, %d vars, resident %08x", m, name, ent->fcount, ent->vcount, ent->resident); - if (!Memory::IsValidAddress(ent->resident)) { + // The NIDs, then the addresses, one word each per function and variable. + const u32 residentBytes = ((u32)ent->fcount + variableCount) * 8; + if (!Memory::IsValidAddress(ent->resident) || !Memory::IsValidRange(ent->resident, residentBytes)) { if (ent->fcount + variableCount > 0) { - WARN_LOG_REPORT(Log::Loader, "Invalid export resident address %08x", ent->resident); + WARN_LOG_REPORT(Log::Loader, "Invalid export resident address %08x (%d funcs, %d vars)", ent->resident, ent->fcount, variableCount); } continue; } @@ -2193,6 +2218,12 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load WARN_LOG(Log::Loader, "Invalid var %d, nid %08x, export address %08x", j, nid, exportAddr); continue; } + // The thread parameter variables below are four words. + const bool isThreadParam = nid == NID_MODULE_START_THREAD_PARAMETER || nid == NID_MODULE_STOP_THREAD_PARAMETER || nid == NID_MODULE_REBOOT_BEFORE_THREAD_PARAMETER; + if ((isThreadParam && !Memory::IsValidRange(exportAddr, 16)) || (nid == NID_MODULE_SDK_VERSION && !Memory::IsValidRange(exportAddr, 4))) { + WARN_LOG(Log::Loader, "Var %d, nid %08x, export address %08x runs off the end of memory", j, nid, exportAddr); + continue; + } int size; switch (nid) { @@ -2265,8 +2296,8 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load delete [] newptr; - if (!reportedModule && ShouldHLEModuleForLoad(modinfo->name)) { - INFO_LOG(Log::sceModule, "Loading module %s with version %04x, devkit %08x", modinfo->name, modinfo->moduleVersion, devkitVersion); + if (!reportedModule && ShouldHLEModuleForLoad(modInfoName)) { + INFO_LOG(Log::sceModule, "Loading module %s with version %04x, devkit %08x", modInfoName.c_str(), modinfo->moduleVersion, devkitVersion); if (!strcmp(modinfo->name, "sceMpeg_library")) { __MpegLoadModule(modinfo->moduleVersion, module->crc); @@ -2354,7 +2385,7 @@ SceUID KernelLoadModule(const std::string &filename, std::string *error_string, u32 error = SCE_KERNEL_ERROR_ILLEGAL_OBJECT; u32 magic; - PSPModule *module = __KernelLoadELFFromPtr(&buffer[0], buffer.size(), 0, fromTop, error_string, &magic, filename, error); + PSPModule *module = __KernelLoadELFFromPtr(buffer.data(), buffer.size(), 0, fromTop, error_string, &magic, filename, error); if (module == nullptr) return error; @@ -2369,11 +2400,12 @@ static PSPModule *__KernelLoadModule(u8 *fileptr, size_t fileSize, SceKernelLMOp // PBP! u32_le version; memcpy(&version, fileptr + 4, 4); - u32_le offset0, offsets[16]; + u32_le offset0, offsets[16]{}; memcpy(&offset0, fileptr + 8, 4); int numfiles = (offset0 - 8) / 4; - if (numfiles <= 0 || numfiles > 16) { + // offsets[5] and [6] are used below. + if (numfiles < 7 || numfiles > 16) { *error_string = "Invalid PBP header - can't load"; return nullptr; } @@ -3686,11 +3718,13 @@ static u32 sceKernelLoadModuleByID(u32 id, u32 flags, u32 lmoptionPtr) { std::string error_string; pspFileSystem.SeekFile(handle, pos, FILEMOVE_BEGIN); PSPModule *module = nullptr; - u8 *temp = new u8[size - pos]; - pspFileSystem.ReadFile(handle, temp, size - pos); + // The game may have seeked past the end. + const size_t remaining = size > pos ? size - pos : 0; + u8 *temp = new u8[remaining](); + const size_t bytesRead = pspFileSystem.ReadFile(handle, temp, remaining); u32 magic; - module = __KernelLoadELFFromPtr(temp, size - pos, 0, lmoption ? lmoption->position == PSP_SMEM_High : false, &error_string, &magic, "", error); + module = __KernelLoadELFFromPtr(temp, std::min(bytesRead, remaining), 0, lmoption ? lmoption->position == PSP_SMEM_High : false, &error_string, &magic, "", error); delete [] temp; if (!module) { @@ -3805,6 +3839,9 @@ SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmo char fakeDebugFilename[512]; snprintf(fakeDebugFilename, sizeof(fakeDebugFilename), "moduleByPtr_%08x_%d", bufPtr, (int)size); + if (!Memory::IsValidRange(bufPtr, size)) { + return hleLogError(Log::Loader, SCE_KERNEL_ERROR_ILLEGAL_ADDR, "Bad module buffer %08x (%d bytes)", bufPtr, size); + } module = __KernelLoadELFFromPtr(Memory::GetPointerOrException(bufPtr), size, 0, lmoption ? lmoption->position == PSP_SMEM_High : false, &error_string, &magic, fakeDebugFilename, error); if (!module) { diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index ecf8ba6f5a..f76ea3c6cc 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -678,6 +678,10 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { // is quite convenient and makes it easy to add shared heuristics. for (int i = 0; i < numFiles; i++) { const char *fn = zip_get_name(z, i, 0); + if (!fn) { + // A corrupt central directory entry. + continue; + } zip_stat_t stat{}; zip_stat_index(z, i, 0, &stat); diff --git a/Core/PSPLoaders.cpp b/Core/PSPLoaders.cpp index 82cdd235c1..a22ff33bd8 100644 --- a/Core/PSPLoaders.cpp +++ b/Core/PSPLoaders.cpp @@ -143,7 +143,8 @@ bool LoadParamSFOFromDisc() { PSPFileInfo fileInfo = pspFileSystem.GetFileInfo(sfoPath.c_str()); if (fileInfo.exists) { std::vector paramsfo; - pspFileSystem.ReadEntireFile(sfoPath, paramsfo); + // A real PARAM.SFO is a KB or two. + pspFileSystem.ReadEntireFile(sfoPath, paramsfo, false, 1024 * 1024); if (g_paramSFO.ReadSFO(paramsfo)) { g_paramSFORaw = g_paramSFO; return true; diff --git a/Core/Replay.cpp b/Core/Replay.cpp index 7a32ca207a..7d3bac8582 100644 --- a/Core/Replay.cpp +++ b/Core/Replay.cpp @@ -15,6 +15,7 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. +#include #include #include #include @@ -482,8 +483,11 @@ uint32_t ReplayApplyDiskRead(void *data, uint32_t readSize, uint32_t dataSize, b { auto item = ReplayNextDisk(ReplayAction::FILE_READ, t); if (item && item->data.size() <= dataSize) { - memcpy(data, &item->data[0], item->data.size()); - return item->info.result; + if (!item->data.empty()) { + memcpy(data, item->data.data(), item->data.size()); + } + // The game sizes its next steps by this, so don't claim more than was copied. + return std::min((uint32_t)item->info.result, (uint32_t)item->data.size()); } return readSize; } @@ -492,7 +496,9 @@ uint32_t ReplayApplyDiskRead(void *data, uint32_t readSize, uint32_t dataSize, b { ReplayItem item = ReplayItemHeader(ReplayAction::FILE_READ, t, readSize); item.data.resize(readSize); - memcpy(&item.data[0], data, readSize); + if (readSize) { + memcpy(item.data.data(), data, readSize); + } replayItems.push_back(item); return readSize; } diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 836cbacba7..993ada0088 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -122,7 +122,8 @@ enum class OperationType { struct PendingRead { std::atomic done{ false }; CChunkFileReader::Error error = CChunkFileReader::ERROR_NONE; - std::unique_ptr buffer; + // From LoadFile, so malloc'd. + std::unique_ptr buffer{ nullptr, &free }; size_t size = 0; std::string gitVersion; // "PPSSPP " from the header, for the failure message. diff --git a/Core/Util/AtracTrack.cpp b/Core/Util/AtracTrack.cpp index b0cf329163..b1f9a0258d 100644 --- a/Core/Util/AtracTrack.cpp +++ b/Core/Util/AtracTrack.cpp @@ -179,7 +179,9 @@ int AnalyzeAtracTrack(const u8 *buffer, u32 size, Track *track, std::string *err } case FACT_CHUNK_MAGIC: { - track->endSample = Read32(buffer, offset); + if (chunkSize >= 4) { + track->endSample = Read32(buffer, offset); + } if (chunkSize >= 8) { track->firstSampleOffset = Read32(buffer, offset + 4); } @@ -417,6 +419,10 @@ int ParseWaveAT3(const u8 *data, u32 dataLength, TrackInfo *track) { } const u32 chunkID = Read32(data, &offset); const int chunkSize = RoundUpToEven(Read32(data, &offset)); + // A negative size would point back at this chunk's own header, looping forever. + if (chunkSize < 0) { + return SCE_ERROR_ATRAC_SIZE_TOO_SMALL; + } const int nextOffset = offset + chunkSize; // We allow the data chunk to be bigger than readSize, as we may not have read the whole file. if ((u32)(offset + chunkSize) > dataLength && chunkID != DATA_CHUNK_MAGIC) { @@ -634,7 +640,8 @@ static u32 ParseAA3Headers(u32 readSize, AA3Info *info, u32 fileSize, const u8 * offset = 0; } - if ((u32)offset + 0x22 > readSize) { + // The header fields read below go up to offset + 0x24. + if ((u32)offset + 0x24 > readSize) { return SCE_ERROR_ATRAC_AA3_SIZE_TOO_SMALL; } if (Parse3BytesBE(aa3Data, &offset) != 0x454133) { diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index 3e7fbbe935..a29b1ddf70 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -912,7 +912,8 @@ bool GameManager::InstallZippedISO(struct zip *z, int isoFileIndex, const Path & return false; } std::string fn = fnPtr; - size_t nameOffset = fn.rfind('/'); + // Zips made on Windows can use either separator. + size_t nameOffset = fn.find_last_of("/\\"); if (nameOffset == std::string::npos) { nameOffset = 0; } else { @@ -934,6 +935,13 @@ bool GameManager::InstallZippedISO(struct zip *z, int isoFileIndex, const Path & name = name.substr(2); } + // The name is used as a filename in the destination, so mustn't be able to point outside it. + if (!IsSafePathComponent(name)) { + ERROR_LOG(Log::IO, "Bad ISO filename in zip: '%s'", fn.c_str()); + SetInstallError(I18NCat::INSTALLZIP, "Zip archive corrupt"); + return false; + } + Path outputISOFilename = destDir; if (outputISOFilename.empty()) { outputISOFilename = Path(g_Config.currentDirectory); @@ -952,8 +960,12 @@ bool GameManager::InstallZippedISO(struct zip *z, int isoFileIndex, const Path & installProgress_ = 1.0f; InstallDone(); - ResetInstallError(); - return true; + if (success) { + ResetInstallError(); + } else { + SetInstallError(I18NCat::INSTALLZIP, "Installation failed"); + } + return success; } bool GameManager::InstallZippedPS1Disc(struct zip *z, const std::vector &files, const Path &destDir) { diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 44da1af5c7..734732f3db 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -697,6 +697,11 @@ int PSARReader::NextEntry(std::string *error) { const u32 chunkSize = ReadU32(block_.data() + 0x104); const u32 expandedSize = ReadU32(block_.data() + 0x108); + // pos_ is a u32, so a huge chunk would wrap it back to an earlier entry and loop forever. + if (chunkSize > limit_ - std::min((size_t)pos_, limit_)) { + *error = StringFromFormat("Entry '%s' runs past the end of the archive (%u bytes)", entryName_.c_str(), chunkSize); + return -1; + } if (expandedSize == 0) { entryIsDirectory_ = true; diff --git a/Core/Util/PkgUnpack.cpp b/Core/Util/PkgUnpack.cpp index 9cb7a443ae..d878312906 100644 --- a/Core/Util/PkgUnpack.cpp +++ b/Core/Util/PkgUnpack.cpp @@ -187,7 +187,7 @@ bool PkgReader::Open(FileLoader *loader, std::string *error) { info_.contentId = contentId; const s64 fileSize = loader->FileSize(); - if (fileSize < 0 || (u64)fileSize < totalSize || dataOffset_ + dataSize_ > (u64)fileSize) { + if (fileSize < 0 || (u64)fileSize < totalSize || dataOffset_ > (u64)fileSize || dataSize_ > (u64)fileSize - dataOffset_) { *error = "PKG file is truncated"; return false; } diff --git a/GPU/Common/ReplacedTexture.cpp b/GPU/Common/ReplacedTexture.cpp index 7d4737496b..6a77d4b698 100644 --- a/GPU/Common/ReplacedTexture.cpp +++ b/GPU/Common/ReplacedTexture.cpp @@ -553,6 +553,12 @@ ReplacedTexture::LoadLevelResult ReplacedTexture::LoadLevelData(VFSFileReference ERROR_LOG(Log::TexReplacement, "Could not load texture replacement info: %s - unsupported format %s", filename.c_str(), magic.c_str()); } + // The sizes come straight from the file headers, and everything below sizes buffers from them. + if (good && (level.w <= 0 || level.h <= 0 || level.w > 8192 || level.h > 8192)) { + ERROR_LOG(Log::TexReplacement, "Replacement texture %s has a bad size: %dx%d", filename.c_str(), level.w, level.h); + good = false; + } + // TODO: We no longer really need to have a split in this function, the upper and lower parts can be merged now. if (good && mipLevel != 0) { @@ -723,19 +729,22 @@ ReplacedTexture::LoadLevelResult ReplacedTexture::LoadLevelData(VFSFileReference bool result = transcoder.get_image_level_info(levelInfo, i, 0, 0); _dbg_assert_(result); - size_t dataSizeBytes = levelInfo.m_total_blocks * blockSize; + size_t dataSizeBytes = (size_t)levelInfo.m_total_blocks * blockSize; size_t outputSize = levelInfo.m_total_blocks; size_t outputPitch = levelInfo.m_num_blocks_x; // Support transcoded-to-RGBA8888 images too. if (!bc) { - dataSizeBytes = levelInfo.m_orig_width * levelInfo.m_orig_height * 4; - outputSize = levelInfo.m_orig_width * levelInfo.m_orig_height; + dataSizeBytes = (size_t)levelInfo.m_orig_width * levelInfo.m_orig_height * 4; + outputSize = (size_t)levelInfo.m_orig_width * levelInfo.m_orig_height; outputPitch = levelInfo.m_orig_width; } 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); + if (!result || dataSizeBytes == 0 || !transcoder.transcode_image_level(i, 0, 0, &out[0], (uint32_t)outputSize, transcoderFormat, 0, (uint32_t)outputPitch, levelInfo.m_orig_height, -1, -1, &transcodeState)) { + WARN_LOG(Log::TexReplacement, "KTX2: Failed to transcode level %d of %s", i, filename.c_str()); + std::fill(out.begin(), out.end(), 0); + } level.w = levelInfo.m_orig_width; level.h = levelInfo.m_orig_height; if (i != 0) @@ -822,15 +831,15 @@ ReplacedTexture::LoadLevelResult ReplacedTexture::LoadLevelData(VFSFileReference // We only use the first level for now. int numLevels = LoadZIMPtr(&zim[0], fileSize, w, h, &f, image); if (numLevels > 0) { - if (w[0] > level.w || h[0] > level.h) { + if (w[0] > level.w || h[0] > level.h || (f & ZIM_FORMAT_MASK) != ZIM_RGBA8888) { ERROR_LOG(Log::TexReplacement, "Texture replacement changed since header read: %s", filename.c_str()); free(image[0]); return LoadLevelResult::LOAD_ERROR; } - out.resize(level.w * level.h * 4); + out.resize((size_t)level.w * level.h * 4); if (w[0] == level.w) { - memcpy(&out[0], image[0], level.w * 4 * level.h); + memcpy(&out[0], image[0], (size_t)level.w * 4 * h[0]); } else { for (int y = 0; y < h[0]; ++y) { memcpy(&out[level.w * 4 * y], image[0] + w[0] * 4 * y, w[0] * 4); diff --git a/GPU/Common/TextureReplacer.cpp b/GPU/Common/TextureReplacer.cpp index 90a9f8b8ab..b143de8e11 100644 --- a/GPU/Common/TextureReplacer.cpp +++ b/GPU/Common/TextureReplacer.cpp @@ -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_; } diff --git a/UI/BackgroundAudio.cpp b/UI/BackgroundAudio.cpp index a9312197ce..ffc475cd5c 100644 --- a/UI/BackgroundAudio.cpp +++ b/UI/BackgroundAudio.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "Common/File/VFS/VFS.h" #include "Common/UI/Root.h" @@ -126,19 +127,25 @@ bool WavData::Read(RIFFReader &file_) { int playCount; }; - if (numLoops > 0 && smplData.size() >= 36 + sizeof(AtracLoopInfo) * numLoops) { + // Divided, since the product wraps a 32-bit size_t. + if (numLoops > 0 && smplData.size() >= 36 && (smplData.size() - 36) / sizeof(AtracLoopInfo) >= (size_t)numLoops) { AtracLoopInfo *loops = (AtracLoopInfo *)&smplData[36]; 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) { @@ -200,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; } diff --git a/UI/ImposeHome.cpp b/UI/ImposeHome.cpp index b4f8fa1af5..dfc99e7160 100644 --- a/UI/ImposeHome.cpp +++ b/UI/ImposeHome.cpp @@ -298,7 +298,7 @@ bool ImposeArt::Parse(const std::vector &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; diff --git a/UI/Store.cpp b/UI/Store.cpp index 1e8e67529d..65cbb8c8a2 100644 --- a/UI/Store.cpp +++ b/UI/Store.cpp @@ -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); diff --git a/ext/jpge/jpgd.cpp b/ext/jpge/jpgd.cpp index baf6ea0484..61487d1c51 100644 --- a/ext/jpge/jpgd.cpp +++ b/ext/jpge/jpgd.cpp @@ -3188,12 +3188,18 @@ namespace jpgd { *height = image_height; *actual_comps = decoder.get_num_components(); + // PPSSPP: Nothing we decode is anywhere near this, and the header alone (from a game, or + // a file) could otherwise make us allocate gigabytes. + if (image_width <= 0 || image_height <= 0 || image_width > 8192 || image_height > 8192) + return nullptr; + if (decoder.begin_decoding() != JPGD_SUCCESS) return nullptr; const int dst_bpl = image_width * req_comps; - uint8* pImage_data = (uint8*)jpgd_malloc(dst_bpl * image_height); + // PPSSPP: In size_t, the int product overflows for large (up to 32768x32768) images. + uint8* pImage_data = (uint8*)jpgd_malloc((size_t)dst_bpl * image_height); if (!pImage_data) return nullptr; @@ -3207,7 +3213,7 @@ namespace jpgd { return nullptr; } - uint8* pDst = pImage_data + y * dst_bpl; + uint8* pDst = pImage_data + (size_t)y * dst_bpl; if (((req_comps == 1) && (decoder.get_num_components() == 1)) || ((req_comps == 4) && (decoder.get_num_components() == 3))) memcpy(pDst, pScan_line, dst_bpl); diff --git a/ext/libkirk/amctrl.c b/ext/libkirk/amctrl.c index fdd05b80d5..0e635b9386 100644 --- a/ext/libkirk/amctrl.c +++ b/ext/libkirk/amctrl.c @@ -154,7 +154,8 @@ int sceDrmBBMacUpdate(KirkState *kirk, MAC_KEY *mkey, u8 *buf, int size) int retv = 0, ksize, p, type; u8 *kbuf; - if(mkey->pad_size>16){ + // The key can live in guest memory (sceDrmBBMacUpdate), so pad_size can be anything. + if(mkey->pad_size<0 || mkey->pad_size>16){ retv = 0x80510302; goto _exit; } @@ -204,7 +205,7 @@ int sceDrmBBMacFinal(KirkState *kirk, MAC_KEY *mkey, u8 *buf, u8 *vkey) u8 *kbuf, tmp[16], tmp1[16]; u32 t0, v0, v1; - if(mkey->pad_size>16) + if(mkey->pad_size<0 || mkey->pad_size>16) return 0x80510302; code = (mkey->type==2)? 0x3A : 0x38; @@ -609,6 +610,8 @@ PGD_DESC *pgd_open(KirkState *kirk, u8 *pgd_buf, int pgd_flag, u8 *pgd_vkey) //DEBUG_LOG(Log::HLE, "Open PGD ..."); pgd = (PGD_DESC*)malloc(sizeof(PGD_DESC)); + if (!pgd) + return NULL; memset(pgd, 0, sizeof(PGD_DESC)); pgd->key_index = *(u32*)(pgd_buf+4); diff --git a/headless/Compare.cpp b/headless/Compare.cpp index 16e7897dad..e35e9a667d 100644 --- a/headless/Compare.cpp +++ b/headless/Compare.cpp @@ -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 {