From b5fba65277daddf28779d29bd9b0000099c7a32d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 29 Mar 2026 20:05:29 -0700 Subject: [PATCH] fix: BLP loader OOB read on ARGB8888 and signed overflow on dimensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARGB8888 decompression read pixelCount*4 bytes from mipData without checking that mipSize was large enough — a truncated BLP caused heap OOB reads. Also, 'int pixelCount = width * height' overflowed for large dimensions (signed int UB). Now validates dimensions <= 4096, uses uint32_t arithmetic, and checks mipSize >= required for ARGB8888. --- src/pipeline/blp_loader.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/pipeline/blp_loader.cpp b/src/pipeline/blp_loader.cpp index 7aaaf7f3..e83cd84a 100644 --- a/src/pipeline/blp_loader.cpp +++ b/src/pipeline/blp_loader.cpp @@ -71,8 +71,11 @@ BLPImage BLPLoader::loadBLP1(const uint8_t* data, size_t size) { const uint8_t* mipData = data + offset; - // Allocate output buffer - int pixelCount = image.width * image.height; + if (image.width <= 0 || image.height <= 0 || image.width > 4096 || image.height > 4096) { + LOG_ERROR("BLP1 dimensions out of range: ", image.width, "x", image.height); + return BLPImage(); + } + uint32_t pixelCount = static_cast(image.width) * static_cast(image.height); image.data.resize(pixelCount * 4); // RGBA8 decompressPalette(mipData, image.data.data(), header.palette, @@ -141,9 +144,19 @@ BLPImage BLPLoader::loadBLP2(const uint8_t* data, size_t size) { const uint8_t* mipData = data + offset; - // Allocate output buffer - int pixelCount = image.width * image.height; - image.data.resize(pixelCount * 4); // RGBA8 + if (image.width <= 0 || image.height <= 0 || image.width > 4096 || image.height > 4096) { + LOG_ERROR("BLP2 dimensions out of range: ", image.width, "x", image.height); + return BLPImage(); + } + uint32_t pixelCount = static_cast(image.width) * static_cast(image.height); + uint32_t requiredArgb = pixelCount * 4; + // For ARGB8888 the source must be at least pixelCount*4 bytes; for DXT/palette + // the source is smaller but the decompressors have their own internal bounds. + if (image.compression == BLPCompression::ARGB8888 && mipSize < requiredArgb) { + LOG_ERROR("BLP2 ARGB8888 mipSize (", mipSize, ") < required (", requiredArgb, ")"); + return BLPImage(); + } + image.data.resize(requiredArgb); // RGBA8 switch (image.compression) { case BLPCompression::DXT1: @@ -164,7 +177,7 @@ BLPImage BLPLoader::loadBLP2(const uint8_t* data, size_t size) { break; case BLPCompression::ARGB8888: - for (int i = 0; i < pixelCount; i++) { + for (uint32_t i = 0; i < pixelCount; i++) { image.data[i * 4 + 0] = mipData[i * 4 + 2]; // R image.data[i * 4 + 1] = mipData[i * 4 + 1]; // G image.data[i * 4 + 2] = mipData[i * 4 + 0]; // B