Skip to content

Image Cache Hash Collision via Missing Dimension Metadata #9360

Description

@3em0

Checklist / 检查清单

  • I have searched existing issues, and this is a new bug report. / 我已经搜索过现有的 issues,确认这是一个新的 bug report。

Bug Description / Bug 描述

Template._save_pil_image() currently uses SHA256(image.tobytes()) as the image cache key. PIL.Image.tobytes() returns only the flattened pixel byte stream and does not include the image mode, width, or height.

As a result, two visually different images with identical raw pixel bytes but different dimensions can produce the same cache path. If the first image has already been cached, the second image reuses the first image's PNG file instead of saving its own content.

This can cause multimodal inference or training to receive the wrong image. For example, a later sample may submit image B while the model actually reads the previously cached image A.

How to Reproduce / 如何复现

Environment:

  • ms-swift current main branch
  • Python environment with Pillow installed

Steps:

  1. Create two RGB images from the same raw bytes with different dimensions, for example 120x80 and 80x120.
  2. Save the first image with Template._save_pil_image().
  3. Save the second image with Template._save_pil_image() using the same cache directory.
  4. Observe that the current implementation returns the same cache path for both calls.

Minimal reproduction:

from PIL import Image
from swift.template.base import Template

width_a, height_a = 120, 80
width_b, height_b = 80, 120
assert width_a * height_a == width_b * height_b

pixels = bytearray()
for i in range(width_a * height_a):
    row = i // width_a
    pixels.extend((255, 60, 60) if row % 10 < 5 else (60, 60, 255))

img_bytes = bytes(pixels)
image_a = Image.frombytes('RGB', (width_a, height_a), img_bytes)
image_b = Image.frombytes('RGB', (width_b, height_b), img_bytes)

path_a = Template._save_pil_image(image_a)
path_b = Template._save_pil_image(image_b)

assert path_a != path_b

Expected behavior:

  • Images with different dimensions should produce different cache paths.
  • Each cached file should preserve the dimensions of the corresponding input image.

Actual behavior:

  • The two images produce the same hash/cache path because the current hash input only includes flattened pixel bytes.

Additional Information / 补充信息

Root cause:

  • image.tobytes() does not include image dimensions or mode metadata.
  • Including image.mode, image.width, and image.height in the hash input prevents cache key collisions across images with different dimensions.

Suggested fix:

img_meta = f'{image.mode}:{image.width}:{image.height}:'.encode()
img_hash = hashlib.sha256(img_meta + image.tobytes()).hexdigest()

Suggested regression test:

  • Construct two images from identical raw bytes with dimensions 120x80 and 80x120.
  • Verify that _save_pil_image() returns different paths for the two images.
  • Verify that both cached files preserve the correct dimensions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions