Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 85033b0

Browse files
authored
[CP-beta] Drop APNG frames that don't fit entirely within the destination surface. (#57062)
This cherry-pick PR includes: #56928 followed by #57025 It supersedes #56978. ### Issue Link: What is the link to the issue this cherry-pick is addressing? Issue was reported over email. ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples Fixes an out-of-bounds memory write in APNG decoding. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) Fixes an issue in which an untrusted malformed APNG image could cause out of bounds memory writes, crashing the app. ### Workaround: Is there a workaround for this issue? There is no workaround. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Attempt to load the APNG used in the tests in the PR.
1 parent 2416efe commit 85033b0

File tree

5 files changed

+76
-2
lines changed

5 files changed

+76
-2
lines changed

lib/ui/fixtures/out_of_bounds.apng

126 Bytes
Binary file not shown.
126 Bytes
Binary file not shown.

lib/ui/painting/image_decoder_no_gl_unittests.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ TEST(ImageDecoderNoGLTest, ImpellerWideGamutIndexedPng) {
187187
#endif // IMPELLER_SUPPORTS_RENDERING
188188
}
189189

190-
TEST(ImageDecoderNoGLTest, ImepllerUnmultipliedAlphaPng) {
190+
TEST(ImageDecoderNoGLTest, ImpellerUnmultipliedAlphaPng) {
191191
#if defined(OS_FUCHSIA)
192192
GTEST_SKIP() << "Fuchsia can't load the test fixtures.";
193193
#endif

lib/ui/painting/image_generator_apng.cc

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,28 @@ bool APNGImageGenerator::GetPixels(const SkImageInfo& info,
110110
<< ") of APNG due to the frame missing data (frame_info).";
111111
return false;
112112
}
113+
if (
114+
// Check for unsigned integer wrapping for
115+
// frame.{x|y}_offset + frame_info.{width|height}().
116+
frame.x_offset >
117+
std::numeric_limits<uint32_t>::max() - frame_info.width() ||
118+
frame.y_offset >
119+
std::numeric_limits<uint32_t>::max() - frame_info.height() ||
120+
121+
frame.x_offset + frame_info.width() >
122+
static_cast<unsigned int>(info.width()) ||
123+
frame.y_offset + frame_info.height() >
124+
static_cast<unsigned int>(info.height())) {
125+
FML_DLOG(ERROR)
126+
<< "Decoded image at index " << image_index
127+
<< " (frame index: " << frame_index
128+
<< ") rejected because the destination region (x: " << frame.x_offset
129+
<< ", y: " << frame.y_offset << ", width: " << frame_info.width()
130+
<< ", height: " << frame_info.height()
131+
<< ") is not entirely within the destination surface (width: "
132+
<< info.width() << ", height: " << info.height() << ").";
133+
return false;
134+
}
113135

114136
//----------------------------------------------------------------------------
115137
/// 3. Composite the frame onto the canvas.
@@ -630,7 +652,19 @@ uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() {
630652
bool APNGImageGenerator::RenderDefaultImage(const SkImageInfo& info,
631653
void* pixels,
632654
size_t row_bytes) {
633-
SkCodec::Result result = images_[0].codec->getPixels(info, pixels, row_bytes);
655+
APNGImage& frame = images_[0];
656+
SkImageInfo frame_info = frame.codec->getInfo();
657+
if (frame_info.width() > info.width() ||
658+
frame_info.height() > info.height()) {
659+
FML_DLOG(ERROR)
660+
<< "Default image rejected because the destination region (width: "
661+
<< frame_info.width() << ", height: " << frame_info.height()
662+
<< ") is not entirely within the destination surface (width: "
663+
<< info.width() << ", height: " << info.height() << ").";
664+
return false;
665+
}
666+
667+
SkCodec::Result result = frame.codec->getPixels(info, pixels, row_bytes);
634668
if (result != SkCodec::kSuccess) {
635669
FML_DLOG(ERROR) << "Failed to decode the APNG's default/fallback image. "
636670
"SkCodec::Result: "

testing/dart/codec_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,46 @@ void main() {
252252
imageData = (await image.toByteData())!;
253253
expect(imageData.getUint32(imageData.lengthInBytes - 4), 0x00000000);
254254
});
255+
256+
test(
257+
'Animated apng frame decode does not crash with invalid destination region',
258+
() async {
259+
final Uint8List data = File(
260+
path.join('flutter', 'lib', 'ui', 'fixtures', 'out_of_bounds.apng'),
261+
).readAsBytesSync();
262+
263+
final ui.Codec codec = await ui.instantiateImageCodec(data);
264+
try {
265+
await codec.getNextFrame();
266+
fail('exception not thrown');
267+
} on Exception catch (e) {
268+
if (impellerEnabled) {
269+
expect(e.toString(), contains('Could not decompress image.'));
270+
} else {
271+
expect(e.toString(), contains('Codec failed'));
272+
}
273+
}
274+
});
275+
276+
test(
277+
'Animated apng frame decode does not crash with invalid destination region and bounds wrapping',
278+
() async {
279+
final Uint8List data = File(
280+
path.join('flutter', 'lib', 'ui', 'fixtures', 'out_of_bounds_wrapping.apng'),
281+
).readAsBytesSync();
282+
283+
final ui.Codec codec = await ui.instantiateImageCodec(data);
284+
try {
285+
await codec.getNextFrame();
286+
fail('exception not thrown');
287+
} on Exception catch (e) {
288+
if (impellerEnabled) {
289+
expect(e.toString(), contains('Could not decompress image.'));
290+
} else {
291+
expect(e.toString(), contains('Codec failed'));
292+
}
293+
}
294+
});
255295
}
256296

257297
/// Returns a File handle to a file in the skia/resources directory.

0 commit comments

Comments
 (0)