Repository: python-pillow/Pillow

Issue:
Win32: map.c integer overflow crashes PIL in tobytes()
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?
Running some tests using PIL test images, my program quit silently. Narrowed down:

```python
import traceback
from PIL import Image

print(Image.__version__)
try:
    with Image.open(r'Pillow-master/Tests/images/l2rgb_read.bmp') as im:
        print(f'mode: {im.mode} size: {im.size}')
        im.tobytes()
except Exception as e:
    print(f'Exception: {e.args} {e}')
    traceback.print_exc()
print('ok')
```

### What did you expect to happen?

I expected an Exception and traceback, because this is a test image of a truncated BMP that is supposed to be a huge image. It should then print 'ok'. For example, in WSL2, (Python 3.8.5, PIL 7.0.0) I do get "ValueError: buffer is not large enough" and the 'ok' prints.

### What actually happened?

```
15:14:17.99 e:\rdg\pil>testit
8.1.0
E:\rdg\pil\PIL\Image.py:2847: DecompressionBombWarning: Image size (151587072 pixels) exceeds limit of 89478485 pixels,
could be decompression bomb DOS attack.
  warnings.warn(
mode: L size: (3158064, 48)

15:14:52.54 e:\rdg\pil>
```

No exception, no 'ok' printed.

### What are your OS, Python and Pillow versions?

* OS:  Windows 10 Pro 1909
* Python: Python 3.9
* Pillow: PIL 8.1.0, or a build from the latest source.

### Discussion

Program dies in `tobytes()`. After considerable effort, I found that the test in `map.c`, `mapping_readimage()` line 214:

```c 
   if (mapper->offset + size > mapper->size) {
        PyErr_SetString(PyExc_OSError, "image file truncated");
        return NULL;
    }
```
fails (should test true and raise the OSError) due to integer overflow: `mapper->offset` is 2147483440, `size` is 75793536; the 32-bit signed sum is -2071690320.

The file is mapped, and eventually `tobytes()` goes to `ImagingRawEncode()` in `RawEncode.c`, which calls `state->shuffle()` to move the bytes using a "packer" that is simply `memcpy()`. The first attempt to fetch from the (non-existent) source causes the program to fail, returning to the command prompt silently, no fault or other message. Nasty.

I poke around and find that the image apparently exists for a test `test_overflow()` in `test_map.py`. This test is skipped for Win32 because "Win32 does not call map_buffer". But Win32 does call `mapping_readimage()` and the same file causes this crash!

### Resolution:

The "image file truncated" test in `mapping_readimage()` needs to be guarded against integer overflow. As Victor Stinner [writes](https://vstinner.readthedocs.io/c_language.html), "Check if an operation will overflow is hard, especially for signed numbers." I've written a bit of C, but I'll leave this one to you folks for now. My quick and dirty fix is to use:

```c
   if (mapper->offset + size > mapper->size || mapper->offset + size < 0)
```

but I suspect this is not a good fix.

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->




Discussion:
It seems likely that #4310 is a solution to this.
Thank you, you are correct. I applied cgohlke's patch and it does seem to work OK in Windows. I am new to this process. Why has #4310 not been accepted/merged into the main branch? What is needed to make that happen?

Resolving patch:
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -192,24 +192,14 @@ def load(self):
                 and args[0] in Image._MAPMODES
             ):
                 try:
-                    if hasattr(Image.core, "map"):
-                        # use built-in mapper  WIN32 only
-                        self.map = Image.core.map(self.filename)
-                        self.map.seek(offset)
-                        self.im = self.map.readimage(
-                            self.mode, self.size, args[1], args[2]
-                        )
-                    else:
-                        # use mmap, if possible
-                        import mmap
-
-                        with open(self.filename) as fp:
-                            self.map = mmap.mmap(
-                                fp.fileno(), 0, access=mmap.ACCESS_READ
-                            )
-                        self.im = Image.core.map_buffer(
-                            self.map, self.size, decoder_name, offset, args
-                        )
+                    # use mmap, if possible
+                    import mmap
+
+                    with open(self.filename) as fp:
+                        self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
+                    self.im = Image.core.map_buffer(
+                        self.map, self.size, decoder_name, offset, args
+                    )
                     readonly = 1
                     # After trashing self.im,
                     # we might need to reload the palette data.


Test patch:
diff --git a/Tests/test_map.py b/Tests/test_map.py
--- a/Tests/test_map.py
+++ b/Tests/test_map.py
@@ -4,10 +4,6 @@
 
 from PIL import Image
 
-from .helper import is_win32
-
-pytestmark = pytest.mark.skipif(is_win32(), reason="Win32 does not call map_buffer")
-
 
 def test_overflow():
     # There is the potential to overflow comparisons in map.c
@@ -27,6 +23,13 @@ def test_overflow():
     Image.MAX_IMAGE_PIXELS = max_pixels
 
 
+def test_tobytes():
+    # Previously raised an access violation on Windows
+    with Image.open("Tests/images/l2rgb_read.bmp") as im:
+        with pytest.raises((ValueError, MemoryError, OSError)):
+            im.tobytes()
+
+
 @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
 def test_ysize():
     numpy = pytest.importorskip("numpy", reason="NumPy not installed")