diff --git a/engine/src/flutter/lib/ui/fixtures/out_of_bounds.apng b/engine/src/flutter/lib/ui/fixtures/out_of_bounds.apng new file mode 100644 index 0000000000..33993c7960 Binary files /dev/null and b/engine/src/flutter/lib/ui/fixtures/out_of_bounds.apng differ diff --git a/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc b/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc index 04a0229c11..aad5032952 100644 --- a/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc +++ b/engine/src/flutter/lib/ui/painting/image_decoder_no_gl_unittests.cc @@ -197,7 +197,7 @@ TEST(ImageDecoderNoGLTest, ImpellerWideGamutIndexedPng) { #endif // IMPELLER_SUPPORTS_RENDERING } -TEST(ImageDecoderNoGLTest, ImepllerUnmultipliedAlphaPng) { +TEST(ImageDecoderNoGLTest, ImpellerUnmultipliedAlphaPng) { #if defined(OS_FUCHSIA) GTEST_SKIP() << "Fuchsia can't load the test fixtures."; #endif diff --git a/engine/src/flutter/lib/ui/painting/image_generator_apng.cc b/engine/src/flutter/lib/ui/painting/image_generator_apng.cc index 159d87638a..77b7b23767 100644 --- a/engine/src/flutter/lib/ui/painting/image_generator_apng.cc +++ b/engine/src/flutter/lib/ui/painting/image_generator_apng.cc @@ -110,6 +110,20 @@ bool APNGImageGenerator::GetPixels(const SkImageInfo& info, << ") of APNG due to the frame missing data (frame_info)."; return false; } + if (frame.x_offset + frame_info.width() > + static_cast(info.width()) || + frame.y_offset + frame_info.height() > + static_cast(info.height())) { + FML_DLOG(ERROR) + << "Decoded image at index " << image_index + << " (frame index: " << frame_index + << ") rejected because the destination region (x: " << frame.x_offset + << ", y: " << frame.y_offset << ", width: " << frame_info.width() + << ", height: " << frame_info.height() + << ") is not entirely within the destination surface (width: " + << info.width() << ", height: " << info.height() << ")."; + return false; + } //---------------------------------------------------------------------------- /// 3. Composite the frame onto the canvas. @@ -630,7 +644,19 @@ uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() { bool APNGImageGenerator::RenderDefaultImage(const SkImageInfo& info, void* pixels, size_t row_bytes) { - SkCodec::Result result = images_[0].codec->getPixels(info, pixels, row_bytes); + APNGImage& frame = images_[0]; + SkImageInfo frame_info = frame.codec->getInfo(); + if (frame_info.width() > info.width() || + frame_info.height() > info.height()) { + FML_DLOG(ERROR) + << "Default image rejected because the destination region (width: " + << frame_info.width() << ", height: " << frame_info.height() + << ") is not entirely within the destination surface (width: " + << info.width() << ", height: " << info.height() << ")."; + return false; + } + + SkCodec::Result result = frame.codec->getPixels(info, pixels, row_bytes); if (result != SkCodec::kSuccess) { FML_DLOG(ERROR) << "Failed to decode the APNG's default/fallback image. " "SkCodec::Result: " diff --git a/engine/src/flutter/testing/dart/codec_test.dart b/engine/src/flutter/testing/dart/codec_test.dart index e21b099a8c..197369c609 100644 --- a/engine/src/flutter/testing/dart/codec_test.dart +++ b/engine/src/flutter/testing/dart/codec_test.dart @@ -252,6 +252,26 @@ void main() { imageData = (await image.toByteData())!; expect(imageData.getUint32(imageData.lengthInBytes - 4), 0x00000000); }); + + test( + 'Animated apng frame decode does not crash with invalid destination region', + () async { + final Uint8List data = File( + path.join('flutter', 'lib', 'ui', 'fixtures', 'out_of_bounds.apng'), + ).readAsBytesSync(); + + final ui.Codec codec = await ui.instantiateImageCodec(data); + try { + await codec.getNextFrame(); + fail('exception not thrown'); + } on Exception catch (e) { + if (impellerEnabled) { + expect(e.toString(), contains('Could not decompress image.')); + } else { + expect(e.toString(), contains('Codec failed')); + } + } + }); } /// Returns a File handle to a file in the skia/resources directory.