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(