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 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-05 12:11:00 +01:00
1 parent 56cf35ac95
commit 382a8eecea
5 files changed
+81 -16

No files matched your search

+21 -4
View File
@@ -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;
}
+8 -3
View File
@@ -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);
+28
View File
@@ -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;
+16 -7
View File
@@ -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);
+8 -2
View File
@@ -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);