From 382a8eecea4fd0ad9e510de80045e52a6e1edacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 12:11:00 +0100 Subject: [PATCH] Images: Limit sizes, and handle ZIM and JPEG load failures - JPEGs over 8192 pixels in either dimension are rejected (in jpgd, so it covers sceJpeg too), and its allocation size is computed in size_t, since the int product overflowed and gave a heap overflow. Image loading also checks the caller's maximum before decoding. - Image loading premultiplied a ZIM that failed to load (null pointer), and treated 16-bit ZIMs as 32-bit (twice the buffer). Reject those, and ZIMs over 8192, and a zlib stream that ends early. - PNG: pngLoadPtr's error path could free a caller's uninitialized pointer, and pngLoad didn't check its allocation or decode. - Texture replacements: reject header sizes of 0 or over 8192 for every format (DDS sizes overflowed an int), transcode KTX2 mips with their own height, and don't copy more ZIM rows than were decoded. Co-Authored-By: Claude Opus 5.5 --- Common/Data/Format/PNGLoad.cpp | 25 +++++++++++++++++++++---- Common/Data/Format/ZIMLoad.cpp | 11 ++++++++--- Common/Render/ManagedTexture.cpp | 28 ++++++++++++++++++++++++++++ GPU/Common/ReplacedTexture.cpp | 23 ++++++++++++++++------- ext/jpge/jpgd.cpp | 10 ++++++++-- 5 files changed, 81 insertions(+), 16 deletions(-) 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/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/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/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);