[xfa] Fix GIF decode continuation Fixes the GIF decoder to continue decoding across various boundaries. The old logic only works for a single scanline within a single image data sub-block, which obviously doesn't hold for real GIF images. Critically, the existing logic fails to recover from source buffer underflow and destination buffer overflow correctly. To fix this: 1. LZWDecompressor::Decode() doesn't discard previously-decoded state. 2. ProgressiveDecoder incrementally feeds LZWDecompressor new data. Note that ProgressiveDecoder.BUG_895009 does not contain a valid GIF, which the fixed decoder correctly fails to decode; the test expectations have been updated accordingly. Fixed: pdfium:904, pdfium:1337 Change-Id: I79db5c8962ff40a9b47e680f5e34e62950c1d495 Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/92390 Auto-Submit: K. Moon <kmoon@chromium.org> Reviewed-by: Lei Zhang <thestig@chromium.org> Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/core/fxcodec/gif/cfx_gifcontext.cpp b/core/fxcodec/gif/cfx_gifcontext.cpp index 6ce9570..a377998 100644 --- a/core/fxcodec/gif/cfx_gifcontext.cpp +++ b/core/fxcodec/gif/cfx_gifcontext.cpp
@@ -213,6 +213,8 @@ std::vector<uint8_t, FxAllocAllocator<uint8_t>> img_data; size_t read_marker = input_buffer_->GetPosition(); + // TODO(crbug.com/pdfium/1793): This logic can be simplified a lot, but it + // probably makes more sense to switch to a different GIF decoder altogether. if (decode_status_ == GIF_D_STATUS_IMG_DATA) { if (!ReadAllOrNone(&img_data_size, sizeof(img_data_size))) return GifDecoder::Status::kUnfinished; @@ -224,20 +226,21 @@ return GifDecoder::Status::kUnfinished; } - if (!lzw_decompressor_.get()) { + if (!lzw_decompressor_) { lzw_decompressor_ = LZWDecompressor::Create(GetPaletteExp(gif_image), gif_image->code_exp); + if (!lzw_decompressor_) { + DecodingFailureAtTailCleanup(gif_image); + return GifDecoder::Status::kError; + } } + lzw_decompressor_->SetSource(img_data.data(), img_data_size); + SaveDecodingStatus(GIF_D_STATUS_IMG_DATA); img_row_offset_ += img_row_avail_size_; img_row_avail_size_ = gif_img_row_bytes - img_row_offset_; - LZWDecompressor::Status ret = - lzw_decompressor_.get() - ? lzw_decompressor_->Decode( - img_data.data(), img_data_size, - gif_image->row_buffer.data() + img_row_offset_, - &img_row_avail_size_) - : LZWDecompressor::Status::kError; + LZWDecompressor::Status ret = lzw_decompressor_->Decode( + gif_image->row_buffer.data() + img_row_offset_, &img_row_avail_size_); if (ret == LZWDecompressor::Status::kError) { DecodingFailureAtTailCleanup(gif_image); return GifDecoder::Status::kError; @@ -263,19 +266,14 @@ return GifDecoder::Status::kUnfinished; } - if (!lzw_decompressor_.get()) { - lzw_decompressor_ = LZWDecompressor::Create( - GetPaletteExp(gif_image), gif_image->code_exp); - } + lzw_decompressor_->SetSource(img_data.data(), img_data_size); + SaveDecodingStatus(GIF_D_STATUS_IMG_DATA); img_row_offset_ += img_row_avail_size_; img_row_avail_size_ = gif_img_row_bytes - img_row_offset_; - ret = lzw_decompressor_.get() - ? lzw_decompressor_->Decode( - img_data.data(), img_data_size, - gif_image->row_buffer.data() + img_row_offset_, - &img_row_avail_size_) - : LZWDecompressor::Status::kError; + ret = lzw_decompressor_->Decode( + gif_image->row_buffer.data() + img_row_offset_, + &img_row_avail_size_); } } @@ -295,18 +293,15 @@ } else { ReadScanline(gif_image->row_num++, gif_image->row_buffer.data()); } + img_row_offset_ = 0; img_row_avail_size_ = gif_img_row_bytes; - ret = lzw_decompressor_.get() - ? lzw_decompressor_->Decode( - img_data.data(), img_data_size, - gif_image->row_buffer.data() + img_row_offset_, - &img_row_avail_size_) - : LZWDecompressor::Status::kError; + ret = lzw_decompressor_->Decode( + gif_image->row_buffer.data() + img_row_offset_, + &img_row_avail_size_); } - if (ret == LZWDecompressor::Status::kInsufficientDestSize || - ret == LZWDecompressor::Status::kError) { + if (ret == LZWDecompressor::Status::kError) { DecodingFailureAtTailCleanup(gif_image); return GifDecoder::Status::kError; }
diff --git a/core/fxcodec/gif/lzw_decompressor.cpp b/core/fxcodec/gif/lzw_decompressor.cpp index 50469aa..8325ce2 100644 --- a/core/fxcodec/gif/lzw_decompressor.cpp +++ b/core/fxcodec/gif/lzw_decompressor.cpp
@@ -33,25 +33,28 @@ : code_size_(code_exp), code_color_end_(static_cast<uint16_t>(1 << (color_exp + 1))), code_clear_(static_cast<uint16_t>(1 << code_exp)), - code_end_(static_cast<uint16_t>((1 << code_exp) + 1)) {} + code_end_(static_cast<uint16_t>((1 << code_exp) + 1)) { + ClearTable(); +} LZWDecompressor::~LZWDecompressor() = default; -LZWDecompressor::Status LZWDecompressor::Decode(const uint8_t* src_buf, - uint32_t src_size, - uint8_t* dest_buf, +void LZWDecompressor::SetSource(const uint8_t* src_buf, uint32_t src_size) { + next_in_ = src_buf; + avail_in_ = src_size; +} + +LZWDecompressor::Status LZWDecompressor::Decode(uint8_t* dest_buf, uint32_t* dest_size) { - if (!src_buf || src_size == 0 || !dest_buf || !dest_size) + if (!next_in_ || !dest_buf || !dest_size) return Status::kError; + if (avail_in_ == 0) + return Status::kUnfinished; + if (*dest_size == 0) return Status::kInsufficientDestSize; - next_in_ = src_buf; - avail_in_ = src_size; - - ClearTable(); - uint32_t i = 0; if (decompressed_next_ != 0) { uint32_t extracted_size = ExtractData(dest_buf, *dest_size);
diff --git a/core/fxcodec/gif/lzw_decompressor.h b/core/fxcodec/gif/lzw_decompressor.h index 390cedb..b99b473 100644 --- a/core/fxcodec/gif/lzw_decompressor.h +++ b/core/fxcodec/gif/lzw_decompressor.h
@@ -34,10 +34,8 @@ uint8_t code_exp); ~LZWDecompressor(); - Status Decode(const uint8_t* src_buf, - uint32_t src_size, - uint8_t* dest_buf, - uint32_t* dest_size); + void SetSource(const uint8_t* src_buf, uint32_t src_size); + Status Decode(uint8_t* dest_buf, uint32_t* dest_size); // Used by unittests, should not be called in production code. uint32_t ExtractDataForTest(uint8_t* dest_buf, uint32_t dest_size) {
diff --git a/core/fxcodec/gif/lzw_decompressor_unittest.cpp b/core/fxcodec/gif/lzw_decompressor_unittest.cpp index 99e186e..f14fa14 100644 --- a/core/fxcodec/gif/lzw_decompressor_unittest.cpp +++ b/core/fxcodec/gif/lzw_decompressor_unittest.cpp
@@ -4,9 +4,15 @@ #include "core/fxcodec/gif/lzw_decompressor.h" +#include <stdint.h> +#include <string.h> + +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/base/cxx17_backports.h" +using ::testing::ElementsAreArray; + TEST(LZWDecompressor, CreateBadParams) { EXPECT_EQ(nullptr, LZWDecompressor::Create(0x10, 0x02)); EXPECT_EQ(nullptr, LZWDecompressor::Create(0x04, 0x0F)); @@ -91,21 +97,23 @@ uint8_t output_data[10]; uint32_t output_size = pdfium::size(output_data); - EXPECT_EQ( - LZWDecompressor::Status::kError, - decompressor->Decode(nullptr, image_size, output_data, &output_size)); + decompressor->SetSource(nullptr, image_size); EXPECT_EQ(LZWDecompressor::Status::kError, - decompressor->Decode(image_data, 0, output_data, &output_size)); - EXPECT_EQ( - LZWDecompressor::Status::kError, - decompressor->Decode(image_data, image_size, nullptr, &output_size)); + decompressor->Decode(output_data, &output_size)); + + decompressor->SetSource(image_data, 0); + EXPECT_EQ(LZWDecompressor::Status::kUnfinished, + decompressor->Decode(output_data, &output_size)); + + decompressor->SetSource(image_data, image_size); EXPECT_EQ(LZWDecompressor::Status::kError, - decompressor->Decode(image_data, image_size, output_data, nullptr)); + decompressor->Decode(nullptr, &output_size)); + EXPECT_EQ(LZWDecompressor::Status::kError, + decompressor->Decode(output_data, nullptr)); output_size = 0; - EXPECT_EQ( - LZWDecompressor::Status::kInsufficientDestSize, - decompressor->Decode(image_data, image_size, output_data, &output_size)); + EXPECT_EQ(LZWDecompressor::Status::kInsufficientDestSize, + decompressor->Decode(output_data, &output_size)); } TEST(LZWDecompressor, Decode1x1SingleColour) { @@ -122,9 +130,9 @@ memset(output_data, 0, sizeof(output_data)); uint32_t output_size = pdfium::size(output_data); - EXPECT_EQ( - LZWDecompressor::Status::kSuccess, - decompressor->Decode(image_data, image_size, output_data, &output_size)); + decompressor->SetSource(image_data, image_size); + EXPECT_EQ(LZWDecompressor::Status::kSuccess, + decompressor->Decode(output_data, &output_size)); EXPECT_EQ(pdfium::size(output_data), output_size); EXPECT_TRUE(0 == memcmp(expected_data, output_data, sizeof(expected_data))); @@ -154,9 +162,9 @@ memset(output_data, 0, sizeof(output_data)); uint32_t output_size = pdfium::size(output_data); - EXPECT_EQ( - LZWDecompressor::Status::kSuccess, - decompressor->Decode(kImageData, image_size, output_data, &output_size)); + decompressor->SetSource(kImageData, image_size); + EXPECT_EQ(LZWDecompressor::Status::kSuccess, + decompressor->Decode(output_data, &output_size)); EXPECT_EQ(pdfium::size(output_data), output_size); EXPECT_TRUE(0 == memcmp(kExpectedData, output_data, sizeof(kExpectedData))); @@ -188,14 +196,39 @@ memset(output_data, 0, sizeof(output_data)); uint32_t output_size = pdfium::size(output_data); - EXPECT_EQ( - LZWDecompressor::Status::kSuccess, - decompressor->Decode(kImageData, image_size, output_data, &output_size)); + decompressor->SetSource(kImageData, image_size); + EXPECT_EQ(LZWDecompressor::Status::kSuccess, + decompressor->Decode(output_data, &output_size)); EXPECT_EQ(pdfium::size(output_data), output_size); EXPECT_TRUE(0 == memcmp(kExpectedData, output_data, sizeof(kExpectedData))); } +TEST(LZWDecompressor, MultipleDecodes) { + auto decompressor = LZWDecompressor::Create(/*color_exp=*/0, /*code_exp=*/2); + ASSERT_NE(nullptr, decompressor); + + static constexpr uint8_t kImageData[] = {0x84, 0x6f, 0x05}; + decompressor->SetSource(kImageData, pdfium::size(kImageData)); + + static constexpr uint8_t kExpectedScanline[] = {0x00, 0x00, 0x00, 0x00}; + uint8_t output_data[pdfium::size(kExpectedScanline)]; + + memset(output_data, 0xFF, sizeof(output_data)); + uint32_t output_size = pdfium::size(output_data); + EXPECT_EQ(LZWDecompressor::Status::kInsufficientDestSize, + decompressor->Decode(output_data, &output_size)); + EXPECT_EQ(pdfium::size(kExpectedScanline), output_size); + EXPECT_THAT(output_data, ElementsAreArray(kExpectedScanline)); + + memset(output_data, 0xFF, sizeof(output_data)); + output_size = pdfium::size(output_data); + EXPECT_EQ(LZWDecompressor::Status::kSuccess, + decompressor->Decode(output_data, &output_size)); + EXPECT_EQ(pdfium::size(kExpectedScanline), output_size); + EXPECT_THAT(output_data, ElementsAreArray(kExpectedScanline)); +} + TEST(LZWDecompressor, HandleColourCodeOutOfPalette) { uint8_t palette_exp = 0x2; // Image uses 10 colours, so the palette exp // should be 3, 2^(3+1) = 16 colours. @@ -213,7 +246,7 @@ memset(output_data, 0, sizeof(output_data)); uint32_t output_size = pdfium::size(output_data); - EXPECT_EQ( - LZWDecompressor::Status::kError, - decompressor->Decode(kImageData, image_size, output_data, &output_size)); + decompressor->SetSource(kImageData, image_size); + EXPECT_EQ(LZWDecompressor::Status::kError, + decompressor->Decode(output_data, &output_size)); }
diff --git a/core/fxcodec/progressive_decoder_unittest.cpp b/core/fxcodec/progressive_decoder_unittest.cpp index 4c1b2c4..8902f6b 100644 --- a/core/fxcodec/progressive_decoder_unittest.cpp +++ b/core/fxcodec/progressive_decoder_unittest.cpp
@@ -105,6 +105,117 @@ EXPECT_THAT(bitmap->GetScanline(0), ElementsAre(0xc0, 0x80, 0x40, 0xff)); } +TEST(ProgressiveDecoder, GifInsufficientCodeSize) { + // This GIF causes `LZWDecompressor::Create()` to fail because the minimum + // code size is too small for the palette. + static constexpr uint8_t kInput[] = { + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x01, 0x00, 0x01, 0x00, 0x82, + 0x01, 0x00, 0x40, 0x80, 0xc0, 0x80, 0x80, 0x80, 0x81, 0x81, 0x81, + 0x82, 0x82, 0x82, 0x83, 0x83, 0x83, 0x84, 0x84, 0x84, 0x85, 0x85, + 0x85, 0x86, 0x86, 0x86, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x02, 0x2, 0x44, 0x01, 0x00, 0x3b}; + + auto decoder = std::make_unique<ProgressiveDecoder>(); + + auto source = pdfium::MakeRetain<CFX_ReadOnlyMemoryStream>(kInput); + CFX_DIBAttribute attr; + FXCODEC_STATUS status = + decoder->LoadImageInfo(source, FXCODEC_IMAGE_GIF, &attr, true); + ASSERT_EQ(FXCODEC_STATUS::kFrameReady, status); + + ASSERT_EQ(1, decoder->GetWidth()); + ASSERT_EQ(1, decoder->GetHeight()); + + auto bitmap = pdfium::MakeRetain<CFX_DIBitmap>(); + bitmap->Create(decoder->GetWidth(), decoder->GetHeight(), + FXDIB_Format::kArgb); + + size_t frames; + std::tie(status, frames) = decoder->GetFrames(); + ASSERT_EQ(FXCODEC_STATUS::kDecodeReady, status); + ASSERT_EQ(1u, frames); + + status = DecodeToBitmap(*decoder, bitmap); + EXPECT_EQ(FXCODEC_STATUS::kError, status); +} + +TEST(ProgressiveDecoder, GifDecodeAcrossScanlines) { + // This GIF contains an LZW code unit split across 2 scanlines. The decoder + // must continue decoding the second scanline using the residual data. + static constexpr uint8_t kInput[] = { + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x04, 0x00, 0x02, 0x00, 0x80, 0x01, + 0x00, 0x40, 0x80, 0xc0, 0x80, 0x80, 0x80, 0x2c, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x02, 0x00, 0x00, 0x02, 0x03, 0x84, 0x6f, 0x05, 0x00, 0x3b}; + + auto decoder = std::make_unique<ProgressiveDecoder>(); + + auto source = pdfium::MakeRetain<CFX_ReadOnlyMemoryStream>(kInput); + CFX_DIBAttribute attr; + FXCODEC_STATUS status = + decoder->LoadImageInfo(source, FXCODEC_IMAGE_GIF, &attr, true); + ASSERT_EQ(FXCODEC_STATUS::kFrameReady, status); + + ASSERT_EQ(4, decoder->GetWidth()); + ASSERT_EQ(2, decoder->GetHeight()); + + auto bitmap = pdfium::MakeRetain<CFX_DIBitmap>(); + bitmap->Create(decoder->GetWidth(), decoder->GetHeight(), + FXDIB_Format::kArgb); + + size_t frames; + std::tie(status, frames) = decoder->GetFrames(); + ASSERT_EQ(FXCODEC_STATUS::kDecodeReady, status); + ASSERT_EQ(1u, frames); + + status = DecodeToBitmap(*decoder, bitmap); + EXPECT_EQ(FXCODEC_STATUS::kDecodeFinished, status); + EXPECT_THAT(bitmap->GetScanline(0), + ElementsAre(0xc0, 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff, 0xc0, + 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff)); + EXPECT_THAT(bitmap->GetScanline(1), + ElementsAre(0xc0, 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff, 0xc0, + 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff)); +} + +TEST(ProgressiveDecoder, GifDecodeAcrossSubblocks) { + // This GIF contains a scanline split across 2 data sub-blocks. The decoder + // must continue decoding in the second sub-block. + static constexpr uint8_t kInput[] = { + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x04, 0x00, 0x02, 0x00, + 0x80, 0x01, 0x00, 0x40, 0x80, 0xc0, 0x80, 0x80, 0x80, 0x2c, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x02, + 0x02, 0x84, 0x6f, 0x01, 0x05, 0x00, 0x3b}; + + auto decoder = std::make_unique<ProgressiveDecoder>(); + + auto source = pdfium::MakeRetain<CFX_ReadOnlyMemoryStream>(kInput); + CFX_DIBAttribute attr; + FXCODEC_STATUS status = + decoder->LoadImageInfo(source, FXCODEC_IMAGE_GIF, &attr, true); + ASSERT_EQ(FXCODEC_STATUS::kFrameReady, status); + + ASSERT_EQ(4, decoder->GetWidth()); + ASSERT_EQ(2, decoder->GetHeight()); + + auto bitmap = pdfium::MakeRetain<CFX_DIBitmap>(); + bitmap->Create(decoder->GetWidth(), decoder->GetHeight(), + FXDIB_Format::kArgb); + + size_t frames; + std::tie(status, frames) = decoder->GetFrames(); + ASSERT_EQ(FXCODEC_STATUS::kDecodeReady, status); + ASSERT_EQ(1u, frames); + + status = DecodeToBitmap(*decoder, bitmap); + EXPECT_EQ(FXCODEC_STATUS::kDecodeFinished, status); + EXPECT_THAT(bitmap->GetScanline(0), + ElementsAre(0xc0, 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff, 0xc0, + 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff)); + EXPECT_THAT(bitmap->GetScanline(1), + ElementsAre(0xc0, 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff, 0xc0, + 0x80, 0x40, 0xff, 0xc0, 0x80, 0x40, 0xff)); +} + TEST(ProgressiveDecoder, BUG_895009) { static constexpr uint8_t kInput[] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x62, 0x00, 0x21, 0x1b, 0x27, 0x01, @@ -478,7 +589,7 @@ ASSERT_EQ(1u, frames); status = DecodeToBitmap(*decoder, bitmap); - EXPECT_EQ(FXCODEC_STATUS::kDecodeFinished, status); + EXPECT_EQ(FXCODEC_STATUS::kError, status); } #endif // PDF_ENABLE_XFA_GIF
diff --git a/testing/SUPPRESSIONS b/testing/SUPPRESSIONS index 24ca5c0..0784cb3 100644 --- a/testing/SUPPRESSIONS +++ b/testing/SUPPRESSIONS
@@ -386,8 +386,6 @@ xfa_example.in win * * * # TODO(pdfium:1354): Remove after associated bug is fixed xfa_example.in * * * skia,skiapaths -# TODO(pdfium:1167): Remove after associated bug is fixed -xfa_gif_image.in * * * * # TODO(pdfium:1743): Remove after associated bug is fixed xfa_node_caption.pdf * * * skia,skiapaths # TODO(pdfium:1095): Remove after associated bug is fixed
diff --git a/testing/fuzzers/pdf_lzw_fuzzer.cc b/testing/fuzzers/pdf_lzw_fuzzer.cc index f019b66..1292ec0 100644 --- a/testing/fuzzers/pdf_lzw_fuzzer.cc +++ b/testing/fuzzers/pdf_lzw_fuzzer.cc
@@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <stddef.h> +#include <stdint.h> + #include <vector> #include "core/fxcodec/gif/lzw_decompressor.h" @@ -15,7 +18,7 @@ static constexpr size_t kMaxFuzzBytes = 1024 * 1024 * 1024; // 1 GB. void LZWFuzz(const uint8_t* src_buf, - size_t src_size, + uint32_t src_size, uint8_t color_exp, uint8_t code_exp) { std::unique_ptr<LZWDecompressor> decompressor = @@ -29,9 +32,9 @@ // This cast should be safe since the caller is checking for overflow on // the initial data. uint32_t dest_size = static_cast<uint32_t>(dest_buf.size()); + decompressor->SetSource(src_buf, src_size); if (LZWDecompressor::Status::kInsufficientDestSize != - decompressor->Decode(src_buf, static_cast<uint32_t>(src_size), - dest_buf.data(), &dest_size)) { + decompressor->Decode(dest_buf.data(), &dest_size)) { return; } }
diff --git a/testing/resources/pixel/xfa_specific/bug_1337_expected.pdf.0.png b/testing/resources/pixel/xfa_specific/bug_1337_expected.pdf.0.png index 08c11b0..c0d8380 100644 --- a/testing/resources/pixel/xfa_specific/bug_1337_expected.pdf.0.png +++ b/testing/resources/pixel/xfa_specific/bug_1337_expected.pdf.0.png Binary files differ