Skip to content

Commit 7c392b5

Browse files
committed
Fix integer overflow in image decoders
1 parent bed715c commit 7c392b5

File tree

4 files changed

+107
-13
lines changed

4 files changed

+107
-13
lines changed

Tests/LibGfx/TestImageDecoder.cpp

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2338,3 +2338,60 @@ TEST_CASE(test_dicom)
23382338
for (int x = 0; x < frame.image->width(); ++x)
23392339
EXPECT_EQ(frame.image->get_pixel(x, y), reference_frame.image->get_pixel(x, y));
23402340
}
2341+
2342+
// Regression tests for integer-overflow fixes in image decoders.
2343+
// Each malformed file exercises a specific arithmetic-sanitisation guard and
2344+
// must be decoded without crashing and without allocating runaway memory.
2345+
2346+
TEST_CASE(test_tiff_strip_count_overflow)
2347+
{
2348+
// ImageLength=10, RowsPerStrip=7, but 4 StripOffsets supplied.
2349+
// segment_index 2 → tile_row 2 → rows_used = 7*2 = 14 > 10 → Error before any decode.
2350+
Array test_inputs = {
2351+
TEST_INPUT("tiff/malformed/tiff_strip_overflow.tiff"sv),
2352+
};
2353+
for (auto path : test_inputs) {
2354+
auto file = TRY_OR_FAIL(Core::MappedFile::map(path));
2355+
auto plugin_or_error = Gfx::TIFFImageDecoderPlugin::create(file->bytes());
2356+
if (plugin_or_error.is_error())
2357+
continue;
2358+
// frame(0) must return an error — not crash, not hang
2359+
auto frame_or_error = plugin_or_error.value()->frame(0);
2360+
EXPECT(frame_or_error.is_error());
2361+
}
2362+
}
2363+
2364+
TEST_CASE(test_tiff_ccitt_huge_dimensions)
2365+
{
2366+
// ImageWidth=ImageLength=65535, CCITT-RLE compression.
2367+
// 65535*65535 = 4,294,836,225 > UINT32_MAX → Checked<u32> overflow → Error.
2368+
Array test_inputs = {
2369+
TEST_INPUT("tiff/malformed/tiff_ccitt_huge_dimensions.tiff"sv),
2370+
};
2371+
for (auto path : test_inputs) {
2372+
auto file = TRY_OR_FAIL(Core::MappedFile::map(path));
2373+
auto plugin_or_error = Gfx::TIFFImageDecoderPlugin::create(file->bytes());
2374+
if (plugin_or_error.is_error())
2375+
continue;
2376+
auto frame_or_error = plugin_or_error.value()->frame(0);
2377+
EXPECT(frame_or_error.is_error());
2378+
}
2379+
}
2380+
2381+
TEST_CASE(test_bmp_rle_buffer_size_overflow)
2382+
{
2383+
// width=65536, height=65536, RLE8: 65536*65536 wraps u32 → Checked<u32> catches it.
2384+
// width=0x3FFFFFFF, height=2, RLE24: round_up(w,4)*4 overflows u32 → caught.
2385+
Array test_inputs = {
2386+
TEST_INPUT("bmp/malformed/bmp_rle8_overflow.bmp"sv),
2387+
TEST_INPUT("bmp/malformed/bmp_rle24_overflow.bmp"sv),
2388+
};
2389+
for (auto path : test_inputs) {
2390+
auto file = TRY_OR_FAIL(Core::MappedFile::map(path));
2391+
auto plugin_or_error = Gfx::BMPImageDecoderPlugin::create(file->bytes());
2392+
if (plugin_or_error.is_error())
2393+
continue;
2394+
auto frame_or_error = plugin_or_error.value()->frame(0);
2395+
EXPECT(frame_or_error.is_error());
2396+
}
2397+
}

Userland/Libraries/LibGfx/ImageFormats/BMPLoader.cpp

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <AK/BuiltinWrappers.h>
99
#include <AK/ByteString.h>
10+
#include <AK/Checked.h>
1011
#include <AK/Debug.h>
1112
#include <AK/Error.h>
1213
#include <AK/Function.h>
@@ -1023,16 +1024,15 @@ static ErrorOr<void> uncompress_bmp_rle_data(BMPLoadingContext& context, ByteBuf
10231024
// ByteBuffer asserts that allocating the memory never fails.
10241025
// FIXME: ByteBuffer should return either RefPtr<> or Optional<>.
10251026
// Decoding the RLE data on-the-fly might actually be faster, and avoids this topic entirely.
1026-
u32 buffer_size;
1027-
if (compression == Compression::RLE24) {
1028-
buffer_size = total_rows * round_up_to_power_of_two(total_columns, 4) * 4;
1029-
} else {
1030-
buffer_size = total_rows * round_up_to_power_of_two(total_columns, 4);
1031-
}
1032-
if (buffer_size > 300 * MiB) {
1027+
Checked<u32> checked_buffer_size = total_rows;
1028+
checked_buffer_size *= round_up_to_power_of_two(total_columns, 4);
1029+
if (compression == Compression::RLE24)
1030+
checked_buffer_size *= 4;
1031+
if (checked_buffer_size.has_overflow() || checked_buffer_size.value() > 300 * MiB) {
10331032
dbgln("Suspiciously large amount of RLE data");
10341033
return Error::from_string_literal("Suspiciously large amount of RLE data");
10351034
}
1035+
u32 buffer_size = checked_buffer_size.value();
10361036
auto buffer_result = ByteBuffer::create_zeroed(buffer_size);
10371037
if (buffer_result.is_error()) {
10381038
dbgln("Not enough memory for buffer allocation");

Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.cpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <AK/Array.h>
88
#include <AK/BitStream.h>
9+
#include <AK/Checked.h>
910
#include <AK/MemoryStream.h>
1011
#include <LibGfx/Color.h>
1112
#include <LibGfx/ImageFormats/CCITTCommon.h>
@@ -343,7 +344,15 @@ ErrorOr<ByteBuffer> decode_ccitt_rle(ReadonlyBytes bytes, u32 image_width, u32 i
343344
auto bit_stream = make<BigEndianInputBitStream>(MaybeOwned<Stream>(*strip_stream));
344345

345346
// Note: We put image_height extra-space to handle at most one alignment to byte boundary per line.
346-
ByteBuffer decoded_bytes = TRY(ByteBuffer::create_zeroed(ceil_div(image_width * image_height, 8) + image_height));
347+
Checked<u32> pixel_count = image_width;
348+
pixel_count *= image_height;
349+
if (pixel_count.has_overflow())
350+
return Error::from_string_literal("CCITTDecoder: image dimensions too large");
351+
Checked<u32> allocation_size = pixel_count.value() / 8 + (pixel_count.value() % 8 != 0 ? 1 : 0);
352+
allocation_size += image_height;
353+
if (allocation_size.has_overflow())
354+
return Error::from_string_literal("CCITTDecoder: image dimensions too large");
355+
ByteBuffer decoded_bytes = TRY(ByteBuffer::create_zeroed(allocation_size.value()));
347356
auto output_stream = make<FixedMemoryStream>(decoded_bytes.bytes());
348357
auto decoded_bits = make<BigEndianOutputBitStream>(MaybeOwned<Stream>(*output_stream));
349358

@@ -362,7 +371,15 @@ ErrorOr<ByteBuffer> decode_ccitt_group3(ReadonlyBytes bytes, u32 image_width, u3
362371
auto bit_stream = make<BigEndianInputBitStream>(MaybeOwned<Stream>(*strip_stream));
363372

364373
// Note: We put image_height extra-space to handle at most one alignment to byte boundary per line.
365-
ByteBuffer decoded_bytes = TRY(ByteBuffer::create_zeroed(ceil_div(image_width * image_height, 8) + image_height));
374+
Checked<u32> pixel_count = image_width;
375+
pixel_count *= image_height;
376+
if (pixel_count.has_overflow())
377+
return Error::from_string_literal("CCITTDecoder: image dimensions too large");
378+
Checked<u32> allocation_size = pixel_count.value() / 8 + (pixel_count.value() % 8 != 0 ? 1 : 0);
379+
allocation_size += image_height;
380+
if (allocation_size.has_overflow())
381+
return Error::from_string_literal("CCITTDecoder: image dimensions too large");
382+
ByteBuffer decoded_bytes = TRY(ByteBuffer::create_zeroed(allocation_size.value()));
366383
auto output_stream = make<FixedMemoryStream>(decoded_bytes.bytes());
367384
auto decoded_bits = make<BigEndianOutputBitStream>(MaybeOwned<Stream>(*output_stream));
368385

Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
#include "TIFFLoader.h"
8+
#include <AK/Checked.h>
89
#include <AK/ConstrainedStream.h>
910
#include <AK/Debug.h>
1011
#include <AK/Endian.h>
@@ -384,7 +385,20 @@ class TIFFLoadingContext {
384385
for (u32 segment_index = 0; segment_index < offsets.size(); ++segment_index) {
385386
TRY(m_stream->seek(offsets[segment_index]));
386387

387-
auto const rows_in_segment = segment_index < offsets.size() - 1 ? segment_length : *m_metadata.image_length() - segment_length * segment_index;
388+
// For tiled images, segment_index is a linear index across all tile columns then rows.
389+
// The tile row is segment_index / segment_per_rows; all tiles in the same tile row
390+
// start at the same image y-offset. Using segment_index directly would give a false
391+
// overflow for valid tiled TIFFs with multiple tile columns.
392+
u32 const tile_row = segment_index / segment_per_rows;
393+
394+
// Validate that rows already decoded by prior tile rows don't exceed image_length,
395+
// which would cause unsigned underflow in the last-segment row calculation below.
396+
Checked<u32> rows_used_before_this_tile_row = segment_length;
397+
rows_used_before_this_tile_row *= tile_row;
398+
if (rows_used_before_this_tile_row.has_overflow() || rows_used_before_this_tile_row.value() > *m_metadata.image_length())
399+
return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid strip/tile configuration: too many segments for image height");
400+
401+
auto const rows_in_segment = segment_index < offsets.size() - 1 ? segment_length : *m_metadata.image_length() - rows_used_before_this_tile_row.value();
388402
auto decoded_bytes = TRY(segment_decoder(byte_counts[segment_index], { segment_width, rows_in_segment }));
389403

390404
ByteBuffer differential_predictor_buffer;
@@ -397,15 +411,21 @@ class TIFFLoadingContext {
397411
auto decoded_stream = make<BigEndianInputBitStream>(move(decoded_segment));
398412

399413
for (u32 row = 0; row < segment_length; row++) {
400-
auto const image_row = row + segment_length * (segment_index / segment_per_rows);
401-
if (image_row >= *m_metadata.image_length())
414+
Checked<u32> checked_image_row = segment_length;
415+
checked_image_row *= tile_row;
416+
checked_image_row += row;
417+
if (checked_image_row.has_overflow() || checked_image_row.value() >= *m_metadata.image_length())
402418
break;
419+
auto const image_row = checked_image_row.value();
403420

404421
for (u32 column = 0; column < segment_width; ++column) {
405422
// If image_length % segment_length != 0, the last tile will be padded.
406423
// This variable helps us to skip these last columns. Note that we still
407424
// need to read the sample from the stream.
408-
auto const image_column = column + segment_width * (segment_index % segment_per_rows);
425+
Checked<u32> checked_image_column = segment_width;
426+
checked_image_column *= (segment_index % segment_per_rows);
427+
checked_image_column += column;
428+
auto const image_column = checked_image_column.has_overflow() ? m_image_width : checked_image_column.value();
409429

410430
if (m_photometric_interpretation == PhotometricInterpretation::CMYK) {
411431
auto const cmyk = TRY(read_color_cmyk(*decoded_stream));

0 commit comments

Comments
 (0)