diff --git a/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java b/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java index ad9ea6e061..6bf5dec3e9 100644 --- a/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java +++ b/third_party/gif_decoder/src/main/java/com/bumptech/glide/gifdecoder/StandardGifDecoder.java @@ -76,6 +76,11 @@ public class StandardGifDecoder implements GifDecoder { @ColorInt private static final int COLOR_TRANSPARENT_BLACK = 0x00000000; + /** Maximum logical screen dimension a single GIF frame may declare (8K). */ + private static final int MAX_VALID_DIMENSION = 8192; + /** Maximum total pixel count per frame (8K x 8K). */ + private static final long MAX_VALID_PIXEL_COUNT = (long) MAX_VALID_DIMENSION * MAX_VALID_DIMENSION; + // Global File Header values and parsing flags. /** * Active color table. @@ -359,6 +364,17 @@ public synchronized void setData(@NonNull GifHeader header, @NonNull ByteBuffer if (sampleSize <= 0) { throw new IllegalArgumentException("Sample size must be >=0, not: " + sampleSize); } + if (header.width <= 0 || header.height <= 0 + || header.width > MAX_VALID_DIMENSION + || header.height > MAX_VALID_DIMENSION) { + this.status = STATUS_FORMAT_ERROR; + return; + } + long pixelCount = (long) header.width * (long) header.height; + if (pixelCount > MAX_VALID_PIXEL_COUNT) { + this.status = STATUS_FORMAT_ERROR; + return; + } // Make sure sample size is a power of 2. sampleSize = Integer.highestOneBit(sampleSize); this.status = STATUS_OK; @@ -383,7 +399,7 @@ public synchronized void setData(@NonNull GifHeader header, @NonNull ByteBuffer downsampledHeight = header.height / sampleSize; // Now that we know the size, init scratch arrays. // TODO Find a way to avoid this entirely or at least downsample it (either should be possible). - mainPixels = bitmapProvider.obtainByteArray(header.width * header.height); + mainPixels = bitmapProvider.obtainByteArray((int) pixelCount); mainScratch = bitmapProvider.obtainIntArray(downsampledWidth * downsampledHeight); } diff --git a/third_party/gif_decoder/src/test/java/com/bumptech/glide/gifdecoder/GifDecoderTest.java b/third_party/gif_decoder/src/test/java/com/bumptech/glide/gifdecoder/GifDecoderTest.java index ac9fadedac..c58e326088 100644 --- a/third_party/gif_decoder/src/test/java/com/bumptech/glide/gifdecoder/GifDecoderTest.java +++ b/third_party/gif_decoder/src/test/java/com/bumptech/glide/gifdecoder/GifDecoderTest.java @@ -8,6 +8,8 @@ import androidx.annotation.NonNull; import com.bumptech.glide.testutil.TestUtil; import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -17,7 +19,7 @@ /** Tests for {@link com.bumptech.glide.gifdecoder.GifDecoder}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = 19) +@Config public class GifDecoderTest { private MockProvider provider; @@ -59,6 +61,8 @@ public void testCanDecodeFramesFromTestGif() throws IOException { @Test public void testFrameIndexStartsAtNegativeOne() { GifHeader gifheader = new GifHeader(); + gifheader.width = 1; + gifheader.height = 1; gifheader.frameCount = 4; byte[] data = new byte[0]; GifDecoder decoder = new StandardGifDecoder(provider); @@ -69,6 +73,8 @@ public void testFrameIndexStartsAtNegativeOne() { @Test public void testTotalIterationCountIsOneIfNetscapeLoopCountDoesntExist() { GifHeader gifheader = new GifHeader(); + gifheader.width = 1; + gifheader.height = 1; gifheader.loopCount = GifHeader.NETSCAPE_LOOP_COUNT_DOES_NOT_EXIST; byte[] data = new byte[0]; GifDecoder decoder = new StandardGifDecoder(provider); @@ -79,6 +85,8 @@ public void testTotalIterationCountIsOneIfNetscapeLoopCountDoesntExist() { @Test public void testTotalIterationCountIsForeverIfNetscapeLoopCountIsForever() { GifHeader gifheader = new GifHeader(); + gifheader.width = 1; + gifheader.height = 1; gifheader.loopCount = GifHeader.NETSCAPE_LOOP_COUNT_FOREVER; byte[] data = new byte[0]; GifDecoder decoder = new StandardGifDecoder(provider); @@ -89,6 +97,8 @@ public void testTotalIterationCountIsForeverIfNetscapeLoopCountIsForever() { @Test public void testTotalIterationCountIsTwoIfNetscapeLoopCountIsOne() { GifHeader gifheader = new GifHeader(); + gifheader.width = 1; + gifheader.height = 1; gifheader.loopCount = 1; byte[] data = new byte[0]; GifDecoder decoder = new StandardGifDecoder(provider); @@ -99,6 +109,8 @@ public void testTotalIterationCountIsTwoIfNetscapeLoopCountIsOne() { @Test public void testAdvanceIncrementsFrameIndex() { GifHeader gifheader = new GifHeader(); + gifheader.width = 1; + gifheader.height = 1; gifheader.frameCount = 4; byte[] data = new byte[0]; GifDecoder decoder = new StandardGifDecoder(provider); @@ -110,6 +122,8 @@ public void testAdvanceIncrementsFrameIndex() { @Test public void testAdvanceWrapsIndexBackToZero() { GifHeader gifheader = new GifHeader(); + gifheader.width = 1; + gifheader.height = 1; gifheader.frameCount = 2; byte[] data = new byte[0]; GifDecoder decoder = new StandardGifDecoder(provider); @@ -123,6 +137,8 @@ public void testAdvanceWrapsIndexBackToZero() { @Test public void testSettingDataResetsFramePointer() { GifHeader gifheader = new GifHeader(); + gifheader.width = 1; + gifheader.height = 1; gifheader.frameCount = 4; byte[] data = new byte[0]; GifDecoder decoder = new StandardGifDecoder(provider); @@ -170,13 +186,49 @@ public void testFirstFrameMustClearBeforeDrawingWhenLastFrameIsDisposalNone() th assertTrue(firstFrame.sameAs(firstFrameTwice)); } + @Test + public void testDecodeOOMWithLargeGif() throws Exception { + byte[] data = buildMaliciousGif(30000, 30000); + GifHeaderParser headerParser = new GifHeaderParser(); + headerParser.setData(data); + GifHeader header = headerParser.parseHeader(); + GifDecoder decoder = new StandardGifDecoder(provider); + decoder.setData(header, data); + assertEquals(GifDecoder.STATUS_FORMAT_ERROR, decoder.getStatus()); + } + + @Test + public void testDecodeNegativeArraySizeWithNegativeDimensions() throws Exception { + byte[] data = buildMaliciousGif(32768, 32767); + GifHeaderParser headerParser = new GifHeaderParser(); + headerParser.setData(data); + GifHeader header = headerParser.parseHeader(); + GifDecoder decoder = new StandardGifDecoder(provider); + decoder.setData(header, data); + assertEquals(GifDecoder.STATUS_FORMAT_ERROR, decoder.getStatus()); + } + + private static byte[] buildMaliciousGif(int width, int height) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(baos); + out.writeBytes("GIF89a"); + out.write(width & 0xFF); out.write((width >> 8) & 0xFF); + out.write(height & 0xFF); out.write((height >> 8) & 0xFF); + out.write(0xF7); // GCT=1, color depth=7, GCT size=7 + out.write(0x00); out.write(0x00); // bg index, pixel aspect + for (int i = 0; i < 256; i++) { // 768-byte global color table + out.write(0x00); out.write(0x00); out.write(0x00); + } + out.write(0x3B); // GIF trailer + return baos.toByteArray(); + } + private static class MockProvider implements GifDecoder.BitmapProvider { @NonNull @Override public Bitmap obtain(int width, int height, Bitmap.Config config) { Bitmap result = Bitmap.createBitmap(width, height, config); - Shadows.shadowOf(result).setMutable(true); return result; }