Repository: python-pillow/Pillow

Issue:
Unable to read tiff while moving between pages
Please note that this error occurs rarely, and it is quite hard to reproduce. It does not depend on the file itself, but more like on a number of conditions surrounding handling it. Steps below are one of the ways I am able to somehow reproduce it on one of our files, but even the imports order seem to matter, and trying to open the same file again wouldn't give similar effect.

### What did you do?
* Download file from s3 storage using boto3, save it to some local path
* Import Pillow, open this file either by name or by the file handle (crashes both ways, checked it)
* Seek to some page, load
* Seek to another page (first warn here), load (second warn here)

### What actually happened?
* on seeking the second page, Pillow prints out `OSError: [Errno 22] Invalid argument`
* on trying to load this page, the following occurs
```
TIFFFetchDirectory: Can not read TIFF directory count.
TIFFReadDirectory: Failed to read directory at offset 4294966312.
```

Stack trace and locals:
``` 
/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py:794: UserWarning: Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py", line 766, in load
    fp.seek(here)
OSError: [Errno 22] Invalid argument

/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py:795: UserWarning: {'self': <PIL.TiffImagePlugin.ImageFileDirectory_v2 object at 0x7f98f63813d0>, 'fp': <_io.BufferedReader name='failed.tiff'>, 'i': 8, 'tag': 282, 'typ': 5, 'count': 1, 'data': b'\xcc\x00\x00\x00\x01\x00\x00\x00', 'tagname': 'XResolution', 'typname': 'rational', 'msg': OSError(22, 'Invalid argument'), 'unit_size': 8, 'handler': <function ImageFileDirectory_v2.load_rational at 0x7f98f48168c0>, 'size': 8, 'here': -874, 'offset': 758, 'traceback': <module 'traceback' from '/usr/local/lib/python3.7/traceback.py'>}
  warnings.warn(str(locals()))
/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py:796: UserWarning: [Errno 22] Invalid argument
  warnings.warn(str(msg))
```

It basically crashes on `fp.seek(here)` when `here` is negative.
Trying to seek/load again fixes everything, re-opening the file also does not raise anything.

### What are your OS, Python and Pillow versions?

* OS: Debian 10
* Python: 3.7.9
* Pillow: 8.1.0

```python
import boto3
session = boto3.session.Session()
client = session.client('s3', region_name='region', endpoint_url='...', aws_access_key_id='...', aws_secret_access_key='...')
a = client.get_object(Bucket='...', Key='....tiff')
from pathlib import Path
Path('/failed.tiff').write_bytes(a['Body'].read())
from PIL import Image
a = Image.open('failed.tiff')
a.seek(1)
a.load()
a.seek(0) # Invalid argument
a.load() # Failed to read
```

Additional debug info between seek/load
```
>>> a = Image.open('failed.tiff')
tag: ImageWidth (256) - type: short (3) - value: b'\xc0\x06'
tag: ImageLength (257) - type: short (3) - value: b'l\x08'
tag: BitsPerSample (258) - type: short (3) - value: b'\x01\x00'
tag: Compression (259) - type: short (3) - value: b'\x04\x00'
tag: PhotometricInterpretation (262) - type: short (3) - value: b'\x01\x00'
tag: StripOffsets (273) - type: long (4) - value: b'\x08\x00\x00\x00'
tag: RowsPerStrip (278) - type: short (3) - value: b'l\x08'
tag: StripByteCounts (279) - type: long (4) - value: b'W\x02\x00\x00'
tag: XResolution (282) - type: rational (5) Tag Location: 718 - Data Location: 758 - value: b'\xcc\x00\x00\x00\x01\x00\x00\x00'
tag: YResolution (283) - type: rational (5) Tag Location: 730 - Data Location: 766 - value: b'\xc4\x00\x00\x00\x01\x00\x00\x00'
tag: PlanarConfiguration (284) - type: short (3) - value: b'\x01\x00'
tag: ResolutionUnit (296) - type: short (3) - value: b'\x02\x00'
tag: ImageWidth (256) - type: short (3) - value: b'\xc0\x06'
tag: ImageLength (257) - type: short (3) - value: b'l\x08'
tag: BitsPerSample (258) - type: short (3) - value: b'\x01\x00'
tag: Compression (259) - type: short (3) - value: b'\x04\x00'
tag: PhotometricInterpretation (262) - type: short (3) - value: b'\x01\x00'
tag: StripOffsets (273) - type: long (4) - value: b'\x08\x00\x00\x00'
tag: RowsPerStrip (278) - type: short (3) - value: b'l\x08'
tag: StripByteCounts (279) - type: long (4) - value: b'W\x02\x00\x00'
tag: XResolution (282) - type: rational (5) Tag Location: 718 - Data Location: 758 - value: b'\xcc\x00\x00\x00\x01\x00\x00\x00'
tag: YResolution (283) - type: rational (5) Tag Location: 730 - Data Location: 766 - value: b'\xc4\x00\x00\x00\x01\x00\x00\x00'
tag: PlanarConfiguration (284) - type: short (3) - value: b'\x01\x00'
tag: ResolutionUnit (296) - type: short (3) - value: b'\x02\x00'
>>> a.seek(1)
tag: ImageWidth (256) - type: short (3) - value: b'\xc0\x06'
tag: ImageLength (257) - type: short (3) - value: b'l\x08'
tag: BitsPerSample (258) - type: short (3) - value: b'\x01\x00'
tag: Compression (259) - type: short (3) - value: b'\x04\x00'
tag: PhotometricInterpretation (262) - type: short (3) - value: b'\x01\x00'
tag: StripOffsets (273) - type: long (4) - value: b'\x18\x03\x00\x00'
tag: RowsPerStrip (278) - type: short (3) - value: b'l\x08'
tag: StripByteCounts (279) - type: long (4) - value: b's\x02\x00\x00'
tag: XResolution (282) - type: rational (5) Tag Location: 1530 - Data Location: 1570 - value: b'\xcc\x00\x00\x00\x01\x00\x00\x00'
tag: YResolution (283) - type: rational (5) Tag Location: 1542 - Data Location: 1578 - value: b'\xc4\x00\x00\x00\x01\x00\x00\x00'
tag: PlanarConfiguration (284) - type: short (3) - value: b'\x01\x00'
tag: ResolutionUnit (296) - type: short (3) - value: b'\x02\x00'
tag: ImageWidth (256) - type: short (3) - value: b'\xc0\x06'
tag: ImageLength (257) - type: short (3) - value: b'l\x08'
tag: BitsPerSample (258) - type: short (3) - value: b'\x01\x00'
tag: Compression (259) - type: short (3) - value: b'\x04\x00'
tag: PhotometricInterpretation (262) - type: short (3) - value: b'\x01\x00'
tag: StripOffsets (273) - type: long (4) - value: b'\x18\x03\x00\x00'
tag: RowsPerStrip (278) - type: short (3) - value: b'l\x08'
tag: StripByteCounts (279) - type: long (4) - value: b's\x02\x00\x00'
tag: XResolution (282) - type: rational (5) Tag Location: 1530 - Data Location: 1570 - value: b'\xcc\x00\x00\x00\x01\x00\x00\x00'
tag: YResolution (283) - type: rational (5) Tag Location: 1542 - Data Location: 1578 - value: b'\xc4\x00\x00\x00\x01\x00\x00\x00'
tag: PlanarConfiguration (284) - type: short (3) - value: b'\x01\x00'
tag: ResolutionUnit (296) - type: short (3) - value: b'\x02\x00'
>>> a.load()
<PixelAccess object at 0x7f580262ac70>
>>> a.seek(0)
tag: ImageWidth (256) - type: short (3) - value: b'\xc0\x06'
tag: ImageLength (257) - type: short (3) - value: b'l\x08'
tag: BitsPerSample (258) - type: short (3) - value: b'\x01\x00'
tag: Compression (259) - type: short (3) - value: b'\x04\x00'
tag: PhotometricInterpretation (262) - type: short (3) - value: b'\x01\x00'
tag: StripOffsets (273) - type: long (4) - value: b'\x08\x00\x00\x00'
tag: RowsPerStrip (278) - type: short (3) - value: b'l\x08'
tag: StripByteCounts (279) - type: long (4) - value: b'W\x02\x00\x00'
tag: XResolution (282) - type: rational (5) Tag Location: -874 - Data Location: 758
/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py:794: UserWarning: Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py", line 766, in load
    fp.seek(here)
OSError: [Errno 22] Invalid argument

  warnings.warn(traceback.format_exc())
/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py:795: UserWarning: {'self': <PIL.TiffImagePlugin.ImageFileDirectory_v2 object at 0x7f5802552f50>, 'fp': <_io.BufferedReader name='failed.tiff'>, 'i': 8, 'tag': 282, 'typ': 5, 'count': 1, 'data': b'\xcc\x00\x00\x00\x01\x00\x00\x00', 'tagname': 'XResolution', 'typname': 'rational', 'msg': OSError(22, 'Invalid argument'), 'unit_size': 8, 'handler': <function ImageFileDirectory_v2.load_rational at 0x7f580256a050>, 'size': 8, 'here': -874, 'offset': 758, 'traceback': <module 'traceback' from '/usr/local/lib/python3.7/traceback.py'>}
  warnings.warn(str(locals()))
/usr/local/lib/python3.7/site-packages/PIL/TiffImagePlugin.py:796: UserWarning: [Errno 22] Invalid argument
  warnings.warn(str(msg))
>>> a.load()
TIFFFetchDirectory: Can not read TIFF directory count.
TIFFReadDirectory: Failed to read directory at offset 4294966312.
<PixelAccess object at 0x7f5803f4f2b0>
```

update: wrong Pillow version, it was 8.1.0, trying to reproduce on 8.2.0 rn


Discussion:
v8.2.0 also affected. It honestly looks like something inside python's `io` lib. `fp.tell()` just returns negative. Also reproducible on Mac with similar python/Pillow version

One more update: With FileIO it works fine, the problem only arises when using the default BufferedReader
Would you be able to upload a copy of the file?

If you say that you can't reproduce using a local copy of the file, then are you able to put together a script that reproduces after downloading the file from GitHub (that you would have just uploaded)?
Github does not support tiff :( uploaded to dropbox: https://www.dropbox.com/s/t3jgs7gejt7wvwz/file.tiff?dl=1. Nothing out of ordinary, two pages, each page saying "test"

Working (failing) example:

```python
import tempfile

import requests
from PIL import Image

a = requests.get('https://www.dropbox.com/s/t3jgs7gejt7wvwz/file.tiff?dl=1')
from pathlib import Path
with tempfile.TemporaryDirectory() as tmpdir:
    path = Path(f'{tmpdir}/test.tiff')
    path.write_bytes(a.content)
    a = Image.open(f'{tmpdir}/test.tiff')
    a.seek(1)
    a.load()
    a.seek(0) # Invalid argument
    a.load() # Failed to read
```

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1052,6 +1052,11 @@ def seek(self, frame):
 
     def _seek(self, frame):
         self.fp = self.__fp
+
+        # reset buffered io handle in case fp
+        # was passed to libtiff, invalidating the buffer
+        self.fp.tell()
+
         while len(self._frame_pos) <= frame:
             if not self.__next:
                 raise EOFError("no more images in TIFF file")
@@ -1059,9 +1064,6 @@ def _seek(self, frame):
                 f"Seeking to frame {frame}, on frame {self.__frame}, "
                 f"__next {self.__next}, location: {self.fp.tell()}"
             )
-            # reset buffered io handle in case fp
-            # was passed to libtiff, invalidating the buffer
-            self.fp.tell()
             self.fp.seek(self.__next)
             self._frame_pos.append(self.__next)
             logger.debug("Loading tags, location: %s" % self.fp.tell())


Test patch:
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -578,6 +578,17 @@ def test_multipage_nframes(self):
 
         TiffImagePlugin.READ_LIBTIFF = False
 
+    def test_multipage_seek_backwards(self):
+        TiffImagePlugin.READ_LIBTIFF = True
+        with Image.open("Tests/images/multipage.tiff") as im:
+            im.seek(1)
+            im.load()
+
+            im.seek(0)
+            assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
+
+        TiffImagePlugin.READ_LIBTIFF = False
+
     def test__next(self):
         TiffImagePlugin.READ_LIBTIFF = True
         with Image.open("Tests/images/hopper.tif") as im: