Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this interop crash the .NET runtime?

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!

like image 392
Cameron Avatar asked Jun 04 '14 21:06

Cameron


1 Answers

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)]
like image 93
David Heffernan Avatar answered Oct 17 '22 21:10

David Heffernan