Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions Tests/test_file_tiff_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,56 @@ def test_tag_offset() -> None:
ifd.load(f)


def test_tag_data_larger_than_file() -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()

# Two BYTE tags that both claim the whole 30 byte IFD as their value.
tag_count = struct.pack("<H", 2)
tags = struct.pack("<HHLL", 0, 1, 30, 0) + struct.pack("<HHLL", 1, 1, 30, 0)
next_offset = struct.pack("<L", 0)

f = io.BytesIO(tag_count + tags + next_offset)
with pytest.warns(UserWarning, match="Tag data larger than file"):
ifd.load(f)
assert len(ifd[0]) == 30
assert 1 not in ifd


def test_tag_data_larger_than_file_across_frames() -> None:
# Three 1x1 frames, each with a private BYTE tag that points to the same
# payload, which is more than half of the file.
payload_size = 400
ifd_size = 2 + 10 * 12 + 4
pixel_offset = 8 + payload_size
first_ifd = pixel_offset + 1

data = b"II*\x00" + struct.pack("<L", first_ifd)
data += b"Z" * payload_size + b"\x7f"
for i in range(3):
data += struct.pack("<H", 10)
for tag in (256, 257, 258, 259, 262, 277):
value = 8 if tag == 258 else 1
data += struct.pack("<HHLH2x", tag, 3, 1, value)
for tag, value in ((273, pixel_offset), (278, 1), (279, 1)):
data += struct.pack("<HHLL", tag, 4, 1, value)
data += struct.pack("<HHLL", 65000, 1, payload_size, 8)
data += struct.pack("<L", first_ifd + (i + 1) * ifd_size if i < 2 else 0)

with Image.open(io.BytesIO(data)) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[65000]) == payload_size

for frame in (1, 2):
with pytest.warns(UserWarning, match="Tag data larger than file"):
im.seek(frame)
assert 65000 not in im.tag_v2
assert im.n_frames == 3

# Reloading a frame gives the same tags as before
im.seek(0)
assert len(im.tag_v2[65000]) == payload_size


def test_tag_group_data() -> None:
base_ifd = TiffImagePlugin.ImageFileDirectory_v2()
interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965)
Expand Down
29 changes: 29 additions & 0 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,10 @@ def __init__(
self.tagtype: dict[int, int] = {}
""" Dictionary of tag types """
self.reset()
# Maximum total out-of-line tag data that load() may read, as well
# as the size of the stream. Set by TiffImageFile so that
# all frames of a file share one budget.
self._tag_data_limit: int | None = None
self.next = (
self._unpack("Q", ifh[8:])[0]
if self._bigtiff
Expand Down Expand Up @@ -895,13 +899,20 @@ def _ensure_read(self, fp: IO[bytes], size: int) -> bytes:
def load(self, fp: IO[bytes]) -> None:
self.reset()
self._offset = fp.tell()
fp.seek(0, io.SEEK_END)
limit = fp.tell()
fp.seek(self._offset)
if self._tag_data_limit is not None:
limit = min(limit, self._tag_data_limit)
self._tag_data_size = 0

try:
tag_count = (
self._unpack("Q", self._ensure_read(fp, 8))
if self._bigtiff
else self._unpack("H", self._ensure_read(fp, 2))
)[0]
total_size = 0
for i in range(tag_count):
tag, typ, count, data = (
self._unpack("HHQ8s", self._ensure_read(fp, 20))
Expand All @@ -928,6 +939,13 @@ def load(self, fp: IO[bytes]) -> None:
if offset >= 2**63:
warnings.warn("Tag offset too large")
continue
# Well-formed IFD values should fit within the stream they
# are read from, even before accounting for overlaps.
total_size += size
if total_size > limit:
warnings.warn(f"Tag data larger than file. Skipping tag {tag}")
continue
self._tag_data_size += size
fp.seek(offset)
data = ImageFile._safe_read(fp, size)
fp.seek(here)
Expand Down Expand Up @@ -1207,6 +1225,13 @@ def _open(self) -> None:
self._fp = self.fp
self._frame_pos: list[int] = []

# Out-of-line tag data is budgeted across all frames, so that many
# frames cannot each read the same data. Each frame's limit is fixed
# when it is first loaded, so that reloading it gives the same tags.
self.fp.seek(0, io.SEEK_END)
self._tag_data_remaining = self.fp.tell()
self._tag_data_limits: list[int] = []

logger.debug("*** TiffImageFile._open ***")
logger.debug("- __first: %s", self.__first)
logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes)
Expand Down Expand Up @@ -1259,8 +1284,11 @@ def _seek(self, frame: int) -> None:
raise ValueError(msg)
self.fp.seek(self.__next)
self._frame_pos.append(self.__next)
self._tag_data_limits.append(self._tag_data_remaining)
logger.debug("Loading tags, location: %s", self.fp.tell())
self.tag_v2._tag_data_limit = self._tag_data_remaining
self.tag_v2.load(self.fp)
self._tag_data_remaining -= self.tag_v2._tag_data_size
if self.tag_v2.next in self._frame_pos:
# This IFD has already been processed
# Declare this to be the end of the image
Expand All @@ -1273,6 +1301,7 @@ def _seek(self, frame: int) -> None:
self.is_animated = self.__next != 0
self.__frame += 1
self.fp.seek(self._frame_pos[frame])
self.tag_v2._tag_data_limit = self._tag_data_limits[frame]
self.tag_v2.load(self.fp)
if XMP in self.tag_v2:
xmp = self.tag_v2[XMP]
Expand Down
Loading