Skip to content

BUG: Flow.Launcher.Infrastructure.Image.ImageLoader hash collisions #4116

@benello

Description

@benello

Checks

  • I have checked that this issue has not already been reported.

  • I am using the latest version of Flow Launcher.

  • I am using the prerelease version of Flow Launcher.

Problem Description

Diagnosis details

  • Code path: Flow.Launcher.Infrastructure.Image.ImageLoader
    • When loadFullImage == false and the file is an image extension handled by the thumbnail path, it invokes:
      image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
  • GetThumbnail calls WindowsThumbnailProvider.GetThumbnail(...), which uses the Shell to provide an HBITMAP.
  • Under certain conditions (e.g., extraction failure, missing or unreachable resource, or just a visually identical small-size render), the Shell returns the same or a generic icon, producing identical pixel data across files.
  • If the hashing algorithm operates on non-normalized or encoded data (e.g., JPEG streams with MemoryStream.GetBuffer()), collisions are more likely or non-deterministic. Even with raw pixel hashing, if the input pixels are identical, the hash will be identical.

CallStack if you will...

public string GetHashFromImage(ImageSource imageSource)

public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)

private static ImageResult GetThumbnailResult(ref string path, bool loadFullImage = false)

private static async ValueTask<ImageResult> LoadInternalAsync(string path, bool loadFullImage = false)

Expected behavior

  • Distinct image files should produce different hashes (or, if the project intends a visual hash, the policy should be documented and the pipeline should minimize accidental equivalence).

Actual behavior

  • Different image files can produce identical hashes when the Shell returns identical thumbnails (e.g., generic icon or identical small-size render), causing cache key collisions and wrong icon reuse.

Suspected root cause

  • Hashing relies on Shell-provided thumbnails in the small-icon path (ThumbnailOnly), which can be identical for different files.
  • Additional instability may occur if the hashing uses encoded streams or non-normalized pixel data.

Proposed fix

  • Compute hashes from normalized raw pixels (e.g., convert to PixelFormats.Pbgra32, copy via CopyPixels, hash with SHA-1/SHA-256) to remove encoder/stride artifacts.
  • Create BitmapSource with BitmapSizeOptions.FromWidthAndHeight(width, height) and Freeze() for deterministic sizing/DPI.
  • Optional policy change (if file-identity is desired): For image files, hash a deterministic decode of the original image rather than the Shell thumbnail. Keep thumbnail hashing for non-image files.

Workarounds

  • Use loadFullImage == true for image files when computing cache keys, or disable hash-based de-duplication for thumbnails.

Additional context

  • Collisions observed with upgrade.png vs remove.png at small icon size using ThumbnailOnly.

Rough patch that I tested and seemed to work.

public string GetHashFromImage(ImageSource imageSource)
        {
            if (imageSource is not BitmapSource image)
            {
                return null;
            }

            try
            {
                // Normalize pixel format to ensure consistent hashing regardless of source format/DPI
                BitmapSource normalized = image;

                if (image.Format != PixelFormats.Pbgra32)
                {
                    var converted = new FormatConvertedBitmap();
                    converted.BeginInit();
                    converted.Source = image;
                    converted.DestinationFormat = PixelFormats.Pbgra32;
                    converted.EndInit();
                    converted.Freeze();
                    normalized = converted;
                }

                // Copy raw pixels. This avoids encoder differences (e.g., JPEG compression artifacts)
                var width = normalized.PixelWidth;
                var height = normalized.PixelHeight;
                var bpp = normalized.Format.BitsPerPixel;
                var stride = (width * bpp + 7) / 8; // WPF allows unaligned stride here
                var pixels = new byte[stride * height];
                normalized.CopyPixels(pixels, stride, 0);

                using var sha1 = SHA1.Create();
                var hashBytes = sha1.ComputeHash(pixels);
                return Convert.ToBase64String(hashBytes);
            }
            catch
            {
                return null;
            }

To Reproduce

  1. Prepare two different image files (e.g., upgrade.png and remove.png).
  2. Ensure the small icon path is used (i.e., loadFullImage == false).
  3. Generate thumbnails with:
    var img1 = WindowsThumbnailProvider.GetThumbnail(path1, ImageLoader.SmallIconSize, ImageLoader.SmallIconSize, ThumbnailOptions.ThumbnailOnly);
    var img2 = WindowsThumbnailProvider.GetThumbnail(path2, ImageLoader.SmallIconSize, ImageLoader.SmallIconSize, ThumbnailOptions.ThumbnailOnly);
  4. Hash with ImageHashGenerator.GetHashFromImage(...).
  5. Compare the two hashes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions