fix(multimodal): tolerate malformed EXIF metadata during hashing (#56527) (#56576)

Signed-off-by: amasen02 <amasen02@users.noreply.github.com> Co-authored-by: amasen02 <amasen02@users.noreply.github.com>

Ama Senevirathne committed Sep 16, 2026 at 13:26 UTC 9ca6dbba71329a7b08711e2acfa17b31e542553c
2 files changed +49 -11
tests/multimodal/test_hasher.py
+30 -1
@@ -1,6 +1,8 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import contextlib
4 import hashlib
5 +import struct
6 import uuid
7 from io import BytesIO
8 from pathlib import Path
@@ -8,7 +10,7 @@ from pathlib import Path
10 import numpy as np
11 import pytest
12 import torch
11 -from PIL import Image, ImageDraw
13 +from PIL import Image, ImageDraw, ImageOps
14
15 from vllm.config.multimodal import MMHasherAlgorithm
16 from vllm.multimodal.hasher import MultiModalHasher
@@ -199,6 +201,33 @@ def test_hash_image_exif_id():
201 )
202
203
204 +def test_hash_image_malformed_exif():
205 + # Test that images with malformed EXIF headers (e.g. invalid TIFF header)
206 + # do not raise an unhandled exception during hashing and fall back to image data.
207 + buf = BytesIO()
208 + Image.new("RGB", (64, 48)).save(buf, "JPEG")
209 + jpg = buf.getvalue()
210 + rest = jpg[2:]
211 + rest = rest[2 + struct.unpack(">H", rest[2:4])[0] :]
212 + payload = b"Exif\x00\x00XXXX\x00\x00\x00\x08" + bytes(32)
213 + data = b"\xff\xd8\xff\xe1" + struct.pack(">H", len(payload) + 2) + payload + rest
214 +
215 + image = Image.open(BytesIO(data))
216 + with contextlib.suppress(Exception):
217 + image = ImageOps.exif_transpose(image)
218 + image.load()
219 +
220 + hasher = MultiModalHasher
221 + # Should hash without raising SyntaxError or any other exception
222 + hash_val = hasher.hash_kwargs("blake3", image=image)
223 + assert isinstance(hash_val, str) and len(hash_val) > 0
224 +
225 + # Also verify MediaWithBytes wrapping the image with malformed EXIF
226 + media_item = MediaWithBytes(image, data)
227 + hash_media = hasher.hash_kwargs("blake3", image=media_item)
228 + assert isinstance(hash_media, str) and len(hash_media) > 0
229 +
230 +
231 def _rgba_png_bytes() -> bytes:
232 image = Image.new("RGBA", (8, 8), (255, 0, 0, 128))
233 buf = BytesIO()
vllm/multimodal/hasher.py
+19 -10
@@ -48,6 +48,19 @@ def _get_hasher_factory(
48 raise ValueError(f"Unsupported hash algorithm: {algorithm}")
49
50
51 +def _get_image_id_bytes(image: Image.Image) -> bytes | None:
52 + try:
53 + exif = image.getexif()
54 + image_id = exif.get(Image.ExifTags.Base.ImageID)
55 + if isinstance(image_id, uuid.UUID):
56 + return image_id.bytes
57 + except Exception:
58 + # Tolerate malformed EXIF metadata (e.g. invalid TIFF header)
59 + # and fall back to serializing raw image data or bytes.
60 + pass
61 + return None
62 +
63 +
64 class MultiModalHasher:
65 @classmethod
66 def serialize_item(cls, obj: object) -> Iterable[bytes | memoryview]:
@@ -60,11 +73,9 @@ class MultiModalHasher:
73 return (np.array(obj).tobytes(),)
74
75 if isinstance(obj, Image.Image):
63 - exif = obj.getexif()
64 - if Image.ExifTags.Base.ImageID in exif and isinstance(
65 - exif[Image.ExifTags.Base.ImageID], uuid.UUID
66 - ):
67 - return (exif[Image.ExifTags.Base.ImageID].bytes,)
76 + image_id = _get_image_id_bytes(obj)
77 + if image_id is not None:
78 + return (image_id,)
79
80 data = {"mode": obj.mode, "data": np.asarray(obj)}
81 palette = obj.palette
@@ -76,11 +87,9 @@ class MultiModalHasher:
87 return cls.iter_item_to_bytes("image", data)
88
89 if isinstance(obj, MediaWithBytes) and isinstance(obj.media, Image.Image):
79 - exif = obj.media.getexif()
80 - if Image.ExifTags.Base.ImageID in exif and isinstance(
81 - exif[Image.ExifTags.Base.ImageID], uuid.UUID
82 - ):
83 - return (exif[Image.ExifTags.Base.ImageID].bytes,)
90 + image_id = _get_image_id_bytes(obj.media)
91 + if image_id is not None:
92 + return (image_id,)
93
94 if obj.io_config:
95 return cls.iter_item_to_bytes(