Flow.Launcher icon indicating copy to clipboard operation
Flow.Launcher copied to clipboard

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

Open benello opened this issue 3 weeks ago • 1 comments

Checks

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

  • [x] 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... https://github.com/Flow-Launcher/Flow.Launcher/blob/c6c413202cd55544fdb4fe6eade1ab5de894cee9/Flow.Launcher.Infrastructure/Image/ImageHashGenerator.cs#L15 https://github.com/Flow-Launcher/Flow.Launcher/blob/c6c413202cd55544fdb4fe6eade1ab5de894cee9/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs#L52 https://github.com/Flow-Launcher/Flow.Launcher/blob/c6c413202cd55544fdb4fe6eade1ab5de894cee9/Flow.Launcher.Infrastructure/Image/ImageLoader.cs#L198 https://github.com/Flow-Launcher/Flow.Launcher/blob/c6c413202cd55544fdb4fe6eade1ab5de894cee9/Flow.Launcher.Infrastructure/Image/ImageLoader.cs#L118

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.

benello avatar Nov 16 '25 19:11 benello