I'm trying to iterate through some files and fetch their shell icons; to accomplish this, I'm using DirectoryInfo.EnumerateFileSystemInfos
and some P/Invoke to call the Win32 SHGetFileInfo
function. But the combination of the two seems to corrupt memory somewhere internally, resulting in ugly crashes.
I've boiled down my code to two similar test cases, both of which crash seemingly without reason. If I don't call DirectoryInfo.EnumerateFileSystemInfos
, no crash appears; if I don't call SHGetFileInfo
, no crash appears. Note that I've removed the actual use of the FileSystemInfo
objects in my code, since I can get it to reproduce simply by iterating over them and asking for the text file icon over and over. But why?
Here are my complete, minimal test cases. Run them under the VS debugger to ensure no optimizations are enabled:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
namespace IconCrashRepro
{
// Compile for .NET 4 (I'm using 4.5.1).
// Also seems to fail in 3.5 with GetFileSystemInfos() instead of EnumerateFileSystemInfos()
public class Program
{
// Compile for .NET 4 (I'm using 4.5.1)
public static void Main()
{
// Keep a list of the objects we generate so
// that they're not garbage collected right away
var sources = new List<BitmapSource>();
// Any directory seems to do the trick, so long
// as it's not empty. Within VS, '.' should be
// the Debug folder
var dir = new DirectoryInfo(@".");
// Track the number of iterations, just to see
ulong iteration = 0;
while (true)
{
// This is where things get interesting -- without the EnumerateFileSystemInfos,
// the bug does not appear. Without the call to SHGetFileInfo, the bug also
// does not appear. It seems to be the combination that causes problems.
var infos = dir.EnumerateFileSystemInfos().ToList();
Debug.Assert(infos.Count > 0);
foreach (var info in infos)
{
var shFileInfo = new SHFILEINFO();
var result = SHGetFileInfo(".txt", (uint)FileAttributes.Normal, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), SHGFI_USEFILEATTRIBUTES | SHGFI_ICON | SHGFI_SMALLICON);
//var result = SHGetFileInfo(info.FullName, (uint)info.Attributes, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), SHGFI_USEFILEATTRIBUTES | SHGFI_ICON | SHGFI_SMALLICON);
if (result != IntPtr.Zero && shFileInfo.hIcon != IntPtr.Zero)
{
var bmpSource = Imaging.CreateBitmapSourceFromHIcon(
shFileInfo.hIcon,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
sources.Add(bmpSource);
// Originally I was releasing the handle, but even if
// I don't the bug occurs!
//DestroyIcon(shFileInfo.hIcon);
}
// Execution fails during Collect; if I remove the
// call to Collect, execution fails later during
// CreateBitmapSourceFromHIcon (it calls
// AddMemoryPressure internally which I suspect
// results in a collect at that point).
GC.Collect();
++iteration;
}
}
}
public static void OtherBugRepro()
{
// Rename this to Main() to run.
// Removing any single line from this method
// will stop it from crashing -- including the
// empty if and the Debug.Assert!
var sources = new List<BitmapSource>();
var dir = new DirectoryInfo(@".");
var infos = dir.EnumerateFileSystemInfos().ToList();
Debug.Assert(infos.Count > 0);
// Crashes on the second iteration -- says that
// `infos` has been modified during loop execution!!
foreach (var info in infos)
{
var shFileInfo = new SHFILEINFO();
var result = SHGetFileInfo(".txt", (uint)FileAttributes.Normal, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), SHGFI_USEFILEATTRIBUTES | SHGFI_ICON | SHGFI_SMALLICON);
if (result != IntPtr.Zero && shFileInfo.hIcon != IntPtr.Zero)
{
if (sources.Count == 1000) { }
}
}
}
[StructLayout(LayoutKind.Sequential)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
private const uint SHGFI_ICON = 0x100;
private const uint SHGFI_LARGEICON = 0x0;
private const uint SHGFI_SMALLICON = 0x1;
private const uint SHGFI_USEFILEATTRIBUTES = 0x10;
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SHGetFileInfo([MarshalAs(UnmanagedType.LPWStr)] string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(IntPtr hIcon);
}
}
Can anyone spot the bug? Any help is appreciated!
You are calling the Unicode version of the function, but passing the ANSI version of the struct. You need to specify the CharSet
in the SHFILEINFO
struct declaration.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With