Flow.Launcher
Flow.Launcher copied to clipboard
BUG: Flow.Launcher.Infrastructure.Image.ImageLoader hash collisions
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 == falseand the file is an image extension handled by the thumbnail path, it invokes:image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
- When
GetThumbnailcallsWindowsThumbnailProvider.GetThumbnail(...), which uses the Shell to provide anHBITMAP.- 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 viaCopyPixels, hash with SHA-1/SHA-256) to remove encoder/stride artifacts. - Create
BitmapSourcewithBitmapSizeOptions.FromWidthAndHeight(width, height)andFreeze()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 == truefor image files when computing cache keys, or disable hash-based de-duplication for thumbnails.
Additional context
- Collisions observed with
upgrade.pngvsremove.pngat small icon size usingThumbnailOnly.
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
- Prepare two different image files (e.g.,
upgrade.pngandremove.png). - Ensure the small icon path is used (i.e.,
loadFullImage == false). - 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); - Hash with
ImageHashGenerator.GetHashFromImage(...). - Compare the two hashes.