Repository: python-pillow/Pillow

Issue:
Conversion of TIFF with CCITT Group 4 Fax Encoding sometimes crashes under Windows
<!--
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?
I'm trying to convert various TIFF files to TIFF with LZW compression. I have now repeatedly run into TIFF files with CCITT Group 4 Fax Encoding which will make Python crash under Windows. 

### What did you expect to happen?
I expect an exception to be raised from the PIL TIFFImagePlugin, instead of a hard crash. 

### What actually happened?
On Windows, the code below with the TIFF file provided will crash the entire Python process without raising exceptions. Under WSL, an OSError is raised, however:

```
saving image
TIFFWriteDirectoryTagSubifd: Illegal value for SubIFD tag.
TIFFWriteDirectoryTagSubifd: Illegal value for SubIFD tag.
Traceback (most recent call last):
  File "main.py", line 33, in <module>
    img2tif(par_dir / "fail.tif", par_dir / "out")
  File "main.py", line 27, in img2tif
    im.save(str(img_out), compression="tiff_lzw")  # Python crashes here
  File "/home/jnik/.local/lib/python3.8/site-packages/PIL/Image.py", line 2151, in save
    save_handler(self, fp, filename)
  File "/home/jnik/.local/lib/python3.8/site-packages/PIL/TiffImagePlugin.py", line 1631, in _save
    raise OSError(f"encoder error {s} when writing image file")
OSError: encoder error -9 when writing image file
```

### What are your OS, Python and Pillow versions?

* OS: Windows 10 Pro 2004, 19041.685 and WSL Debian (`Linux 4.19.128-microsoft-standard #1 SMP Tue Jun 23 12:58:10 UTC 2020 x86_64 GNU/Linux`)
* Python: Tested under 3.8.5 and 3.9.1
* Pillow: 8.0.1

<!--
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.
-->


### Code and further information

```python
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from pathlib import Path

from PIL import Image


# -----------------------------------------------------------------------------
# Image conversion
# -----------------------------------------------------------------------------


def img2tif(img_file: Path, out_dir: Path) -> None:
    img_out: Path = out_dir / img_file.name
    img_out = img_out.with_suffix(".tif")
    print(img_out)
    print("opening image")
    im = Image.open(img_file)
    print("loading image")
    im.load()
    print("saving image")
    im.save(str(img_out), compression="tiff_lzw")  # Python crashes here
    print("saved image")  # Never reached


if __name__ == "__main__":
    par_dir = Path(__file__).parent
    img2tif(par_dir / "fail.tif", par_dir / "out")

```
I have attached both the code and the offending TIFF image here: [tif_fail.zip](https://github.com/python-pillow/Pillow/files/5724335/tif_fail.zip)
Note that opening the offending file in e.g. IrfanView and saving it again will fix the issue. So the TIFF file is definitely damaged somehow. I don't expect Pillow to handle the file, but rather fail more gracefully.


Discussion:

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -89,6 +89,7 @@
 PREDICTOR = 317
 COLORMAP = 320
 TILEOFFSETS = 324
+SUBIFD = 330
 EXTRASAMPLES = 338
 SAMPLEFORMAT = 339
 JPEGTABLES = 347
@@ -1559,12 +1560,14 @@ def _save(im, fp, filename):
         # The other tags expect arrays with a certain length (fixed or depending on
         # BITSPERSAMPLE, etc), passing arrays with a different length will result in
         # segfaults. Block these tags until we add extra validation.
+        # SUBIFD may also cause a segfault.
         blocklist = [
             REFERENCEBLACKWHITE,
             SAMPLEFORMAT,
             STRIPBYTECOUNTS,
             STRIPOFFSETS,
             TRANSFERFUNCTION,
+            SUBIFD,
         ]
 
         atts = {}


Test patch:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -4,7 +4,7 @@
 import pytest
 
 from PIL import Image, TiffImagePlugin
-from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
+from PIL.TiffImagePlugin import RESOLUTION_UNIT, SUBIFD, X_RESOLUTION, Y_RESOLUTION
 
 from .helper import (
     assert_image_equal,
@@ -161,6 +161,14 @@ def test_save_dpi_rounding(self, tmp_path):
                     reloaded.load()
                     assert (round(dpi), round(dpi)) == reloaded.info["dpi"]
 
+    def test_subifd(self, tmp_path):
+        outfile = str(tmp_path / "temp.tif")
+        with Image.open("Tests/images/g4_orientation_6.tif") as im:
+            im.tag_v2[SUBIFD] = 10000
+
+            # Should not segfault
+            im.save(outfile)
+
     def test_save_setting_missing_resolution(self):
         b = BytesIO()
         Image.open("Tests/images/10ct_32bit_128.tiff").save(