Repository: python-pillow/Pillow

Problem statement:
out of memory when processing this GIF
source: http://lavender.b0.upaiyun.com/ex.gif

![example.jpg](http://lavender.b0.upaiyun.com/ex.gif)

```python
import PIL
import PIL.Image

im = PIL.Image.open('example.gif')
print(im.size)
print(im.n_frames)  # will crash
```

```shell
[root /tmp] python3 run.py 
(320, 240)
Killed
```

The problem appear on this line https://github.com/python-pillow/Pillow/blob/master/PIL/GifImagePlugin.py#L261, self.dispose_extent is very huge. 

env: python3.5 on linux, free mem is 2G.

thanks.



Hints:
any idea?
Well, as you've noted, the dispose_extent of frame 90 is rather large: 
```
(Pdb) self.dispose_extent
(37042, 49425, 90174, 109354)
(Pdb) frame
90
```

This is fed to crop, which ultimately returns a new image. Or, tries to, as that's a rather large image.

Any potential fix for #2383 could interact badly with this, as the dispose_extent winds up being the tile extents as well. 

Crop should really have the DecompressionBomb warning applied, as it can increase the size of the image. But in this case, we're calling into the core image object crop, not Image.crop, so we'd need to either hoist it to a python level function or call the decompression_bomb_check from the C layer. 





Patch:
diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py
--- a/PIL/GifImagePlugin.py
+++ b/PIL/GifImagePlugin.py
@@ -257,7 +257,7 @@ def _seek(self, frame):
 
             # only dispose the extent in this frame
             if self.dispose:
-                self.dispose = self.dispose.crop(self.dispose_extent)
+                self.dispose = self._crop(self.dispose, self.dispose_extent)
         except (AttributeError, KeyError):
             pass
 
@@ -280,7 +280,7 @@ def load_end(self):
         if self._prev_im and self.disposal_method == 1:
             # we do this by pasting the updated area onto the previous
             # frame which we then use as the current image content
-            updated = self.im.crop(self.dispose_extent)
+            updated = self._crop(self.im, self.dispose_extent)
             self._prev_im.paste(updated, self.dispose_extent,
                                 updated.convert('RGBA'))
             self.im = self._prev_im
diff --git a/PIL/Image.py b/PIL/Image.py
--- a/PIL/Image.py
+++ b/PIL/Image.py
@@ -1042,18 +1042,31 @@ def crop(self, box=None):
         if box is None:
             return self.copy()
 
-        x0, y0, x1, y1 = map(int, map(round, box))
+        return self._new(self._crop(self.im, box))
 
-        if x0 == 0 and y0 == 0 and (x1, y1) == self.size:
-            return self.copy()
+    def _crop(self, im, box):
+        """
+        Returns a rectangular region from the core image object im.
+
+        This is equivalent to calling im.crop((x0, y0, x1, y1)), but
+        includes additional sanity checks.
+
+        :param im: a core image object
+        :param box: The crop rectangle, as a (left, upper, right, lower)-tuple.
+        :returns: A core image object.
+        """
+
+        x0, y0, x1, y1 = map(int, map(round, box))
 
         if x1 < x0:
             x1 = x0
         if y1 < y0:
             y1 = y0
 
-        return self._new(self.im.crop((x0, y0, x1, y1)))
+        _decompression_bomb_check((x1, y1))
 
+        return im.crop((x0, y0, x1, y1))
+         
     def draft(self, mode, size):
         """
         Configures the image file loader so it returns a version of the


Test patch:
diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py
--- a/Tests/test_decompression_bomb.py
+++ b/Tests/test_decompression_bomb.py
@@ -1,4 +1,4 @@
-from helper import unittest, PillowTestCase
+from helper import unittest, PillowTestCase, hopper
 
 from PIL import Image
 
@@ -35,9 +35,24 @@ def test_warning(self):
         self.assertEqual(Image.MAX_IMAGE_PIXELS, 10)
 
         # Act / Assert
-        self.assert_warning(
-            Image.DecompressionBombWarning,
-            lambda: Image.open(TEST_FILE))
+        self.assert_warning(Image.DecompressionBombWarning,
+                            lambda: Image.open(TEST_FILE))
+
+class TestDecompressionCrop(PillowTestCase):
+
+    def setUp(self):
+        self.src = hopper()
+        Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width
+
+    def tearDown(self):
+        Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
+
+    def testEnlargeCrop(self):
+        # Crops can extend the extents, therefore we should have the
+        # same decompression bomb warnings on them.
+        box = (0, 0, self.src.width * 2, self.src.height * 2)
+        self.assert_warning(Image.DecompressionBombWarning,
+                            lambda: self.src.crop(box))    
 
 if __name__ == '__main__':
     unittest.main()