Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions engine/src/flutter/lib/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ if (enable_unittests) {
"painting/image_decoder_no_gl_unittests.h",
"painting/image_dispose_unittests.cc",
"painting/image_encoding_unittests.cc",
"painting/image_generator_apng_unittests.cc",
"painting/image_generator_registry_unittests.cc",
"painting/paint_unittests.cc",
"painting/path_unittests.cc",
Expand Down
30 changes: 24 additions & 6 deletions engine/src/flutter/lib/ui/painting/image_generator_apng.cc
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ std::unique_ptr<ImageGenerator> APNGImageGenerator::MakeFromData(
}
}

if (chunk->get_data_length() < sizeof(AnimationControlChunkData)) {
return nullptr;
}
const AnimationControlChunkData* animation_data =
CastChunkData<AnimationControlChunkData>(chunk);

Expand Down Expand Up @@ -430,6 +433,9 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p,
// The presence of an fcTL chunk is optional for the first (default) image
// of a PNG. Both cases are handled in APNGImage.
if (chunk->get_type() == kFrameControlChunkType) {
if (chunk->get_data_length() < sizeof(FrameControlChunkData)) {
return std::make_pair(std::nullopt, nullptr);
}
control_data = CastChunkData<FrameControlChunkData>(chunk);

ImageGenerator::FrameInfo frame_info;
Expand Down Expand Up @@ -491,7 +497,10 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p,
// sequence number prepended to its data, so subtract that space from
// the buffer.
if (chunk->get_type() == kFrameDataChunkType) {
chunk_space -= 4;
if (chunk->get_data_length() < kFrameDataSequenceNumberSize) {
return std::make_pair(std::nullopt, nullptr);
}
chunk_space -= kFrameDataSequenceNumberSize;
}
}

Expand Down Expand Up @@ -527,17 +536,21 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p,
// Copy the image data/ancillary chunks.
for (const ChunkHeader* c : image_chunks) {
if (c->get_type() == kFrameDataChunkType) {
FML_DCHECK(c->get_data_length() >= kFrameDataSequenceNumberSize);

// Write a new IDAT chunk header.
ChunkHeader* write_header =
reinterpret_cast<ChunkHeader*>(write_cursor);
write_header->set_data_length(c->get_data_length() - 4);
write_header->set_data_length(c->get_data_length() -
kFrameDataSequenceNumberSize);
write_header->set_type(kImageDataChunkType);
write_cursor += sizeof(ChunkHeader);

// Copy all of the data except for the 4 byte sequence number at the
// beginning of the fdAT data.
memcpy(write_cursor,
reinterpret_cast<const uint8_t*>(c) + sizeof(ChunkHeader) + 4,
reinterpret_cast<const uint8_t*>(c) + sizeof(ChunkHeader) +
kFrameDataSequenceNumberSize,
write_header->get_data_length());
write_cursor += write_header->get_data_length();

Expand Down Expand Up @@ -645,8 +658,13 @@ void APNGImageGenerator::ChunkHeader::UpdateChunkCrc32() {
uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() {
// Exclude the length field at the beginning of the chunk header.
size_t length = sizeof(ChunkHeader) - 4 + get_data_length();
uint8_t* chunk_data_p = reinterpret_cast<uint8_t*>(this) + 4;
const uint8_t* chunk_data = reinterpret_cast<const uint8_t*>(this) + 4;
return ComputeCrc32(chunk_data, length);
}

uint32_t APNGImageGenerator::ComputeCrc32(const uint8_t* data, size_t length) {
uint32_t crc = 0;
const uint8_t* data_p = data;

// zlib's crc32 can only take 16 bits at a time for the length, but PNG
// supports a 32 bit chunk length, so looping is necessary here.
Expand All @@ -658,9 +676,9 @@ uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() {
length16 = std::numeric_limits<uint16_t>::max();
}

crc = crc32(crc, chunk_data_p, length16);
crc = crc32(crc, data_p, length16);
length -= length16;
chunk_data_p += length16;
data_p += length16;
} while (length > 0);

return crc;
Expand Down
6 changes: 6 additions & 0 deletions engine/src/flutter/lib/ui/painting/image_generator_apng.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ class APNGImageGenerator : public ImageGenerator {

static std::unique_ptr<ImageGenerator> MakeFromData(sk_sp<SkData> data);

/// Computes the CRC of the data in a PNG chunk.
static uint32_t ComputeCrc32(const uint8_t* data, size_t length);

private:
static constexpr uint8_t kPngSignature[8] = {137, 80, 78, 71, 13, 10, 26, 10};
static constexpr size_t kChunkCrcSize = 4;

/// The size of the sequence number at the beginning of an fdAT chunk.
static constexpr size_t kFrameDataSequenceNumberSize = 4;
Comment thread
jason-simmons marked this conversation as resolved.

enum ChunkType {
kImageHeaderChunkType = 'IHDR',
kAnimationControlChunkType = 'acTL',
Expand Down
127 changes: 127 additions & 0 deletions engine/src/flutter/lib/ui/painting/image_generator_apng_unittests.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/lib/ui/painting/image_generator_apng.h"

#include <cstdint>
#include <cstring>
#include <vector>

#include "flutter/lib/ui/painting/image_generator_registry.h"
#include "flutter/testing/testing.h"
#include "third_party/skia/include/core/SkData.h"

namespace flutter {
namespace testing {

namespace {

// Writes a big-endian uint32_t to a buffer.
void WriteBE32(std::vector<uint8_t>& buf, uint32_t val) {
buf.push_back((val >> 24) & 0xFF);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a static assert for little endian please

static_assert(std::endian::native == std::endian::little, 
              "This code requires a Little Endian architecture.");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not depend on the endianness of the host. The bytes will be written in order from most significant to least significant on any architecture.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has the assumption that val is in little endian. That will only be the case when the host is little endian. If we are on a big endian machine we don't have to do this swizzling.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val is a uint32_t, and the shifts of val done by this function will behave consistently regardless of the host architecture's endianness.

This function writes the value of val into buf in the big-endian format required by the PNG spec. It will work as intended on both big- and little-endian architectures.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jason's right, this will operate the same across the different machines, lgtm

buf.push_back((val >> 16) & 0xFF);
buf.push_back((val >> 8) & 0xFF);
buf.push_back(val & 0xFF);
}

// Writes a big-endian uint16_t to a buffer.
void WriteBE16(std::vector<uint8_t>& buf, uint16_t val) {
buf.push_back((val >> 8) & 0xFF);
buf.push_back(val & 0xFF);
}

// Appends a PNG chunk (length + type + data + CRC) to the buffer.
void AppendChunk(std::vector<uint8_t>& buf,
const char type[4],
const std::vector<uint8_t>& data) {
FML_CHECK(data.size() <= std::numeric_limits<uint32_t>::max());
WriteBE32(buf, static_cast<uint32_t>(data.size()));
Comment thread
gaaclarke marked this conversation as resolved.
size_t type_start = buf.size();
buf.insert(buf.end(), type, type + 4);
buf.insert(buf.end(), data.begin(), data.end());
uint32_t crc = APNGImageGenerator::ComputeCrc32(buf.data() + type_start,
4 + data.size());
WriteBE32(buf, crc);
}

// Builds a minimal valid APNG with a malicious fdAT chunk whose
// data_length is less than 4, which would trigger an integer underflow
// in DemuxNextImage() without the bounds check fix.
std::vector<uint8_t> BuildMaliciousApng(uint32_t fdat_data_length) {
std::vector<uint8_t> apng;

// PNG signature
const uint8_t sig[] = {137, 80, 78, 71, 13, 10, 26, 10};
apng.insert(apng.end(), sig, sig + 8);

// IHDR: 1x1 RGBA, 8-bit
{
std::vector<uint8_t> ihdr;
WriteBE32(ihdr, 1); // width
WriteBE32(ihdr, 1); // height
ihdr.push_back(8); // bit depth
ihdr.push_back(6); // color type (RGBA)
ihdr.push_back(0); // compression
ihdr.push_back(0); // filter
ihdr.push_back(0); // interlace
AppendChunk(apng, "IHDR", ihdr);
}

// acTL: 1 frame, loop forever
{
std::vector<uint8_t> actl;
WriteBE32(actl, 1); // num_frames
WriteBE32(actl, 0); // num_plays (0 = infinite)
AppendChunk(apng, "acTL", actl);
}

// fcTL for frame 0
{
std::vector<uint8_t> fctl;
WriteBE32(fctl, 0); // sequence_number
WriteBE32(fctl, 1); // width
WriteBE32(fctl, 1); // height
WriteBE32(fctl, 0); // x_offset
WriteBE32(fctl, 0); // y_offset
WriteBE16(fctl, 1); // delay_num
WriteBE16(fctl, 10); // delay_den
fctl.push_back(0); // dispose_op
fctl.push_back(0); // blend_op
AppendChunk(apng, "fcTL", fctl);
}

// Malicious fdAT for frame 0: data_length < 4
// An fdAT chunk must have at least 4 bytes (sequence number).
// With data_length < 4, the subtraction in DemuxNextImage() underflows.
AppendChunk(apng, "fdAT", std::vector<uint8_t>(fdat_data_length, 0));

// IEND
AppendChunk(apng, "IEND", {});

return apng;
}

} // namespace

// Verify that the APNG decoder can handle fdAT chunks whose length is shorter
// than the required 4-byte sequence number.
TEST(APNGImageGeneratorTest, FdATWithShortDataLengthDoesNotCrash) {
ImageGeneratorRegistry registry;

auto make_generator = [](uint32_t fdat_length) -> auto {
auto apng_bytes = BuildMaliciousApng(fdat_length);
auto data = SkData::MakeWithCopy(apng_bytes.data(), apng_bytes.size());
return APNGImageGenerator::MakeFromData(data);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting for awareness:

Up until now, tests for the APNG demuxer have lived a level above this at the codec level in engine/src/flutter/testing/dart/codec_test.dart with pre-generated APNG fixtures. This is mainly due to the image generator state machine being so intertwined with the classes driving it.

I kinda like the idea of generating bad APNGs inline, since it makes the structure of the bad file easier to decipher, and IMO since this is a "crash on initial parse" bug rather than a "crash after advancing N frames" bug... seems fine to place it this deep.

};

// The decoder should reject fdAT chunks that are less than 4 bytes long.
EXPECT_EQ(make_generator(0), nullptr);
EXPECT_EQ(make_generator(2), nullptr);

// Creating the generator should succeed if the fdAT has sufficient length.
EXPECT_NE(make_generator(4), nullptr);
}

} // namespace testing
} // namespace flutter
Loading