在 C# 应用程序中收到编组结构后不久发生堆损坏

Heap corruption shortly after receiving marshalled struct in C# app

我正在尝试包装新的 Windows 10 ProjFS features in C#. The first steps, as outlined on MSDN,工作正常:我将一个目录设置为我的虚拟化根目录,然后注册我的投影提供程序,包括其回调,如下所示:

static void Main(string[] args)
{
    // Create and mark clean directory as virtualization root.
    const string directory = @"C:\test_projfs"; // Warning: Deleted to get clean directory.
    if (Directory.Exists(directory))
        Directory.Delete(directory);
    Directory.CreateDirectory(directory);

    Guid guid = Guid.NewGuid();
    Marshal.ThrowExceptionForHR(PrjMarkDirectoryAsPlaceholder(directory, null, IntPtr.Zero,
        ref guid));

    // Set up the callback table for the projection provider.
    PrjCallbacks callbackTable = new PrjCallbacks
    {
        StartDirectoryEnumerationCallback = StartDirectoryEnumerationCallback,
        EndDirectoryEnumerationCallback = EndDirectoryEnumerationCallback,
        GetDirectoryEnumerationCallback = GetDirectoryEnumerationCallback,
        GetPlaceholderInfoCallback = GetPlaceholderInfoCallback,
        GetFileDataCallback = GetFileDataCallback
    };
    // Start the projection provider.
    IntPtr instanceHandle = IntPtr.Zero;
    Marshal.ThrowExceptionForHR(PrjStartVirtualizing(directory, ref callbackTable,
        IntPtr.Zero, IntPtr.Zero, ref instanceHandle));

    // Keep a test console application running.
    Console.ReadLine();
}

// Managed callbacks, simply returning S_OK for now.
static int StartDirectoryEnumerationCallback(ref PrjCallbackData callbackData, ref Guid enumerationId)
{
    return 0;
}
static int EndDirectoryEnumerationCallback(ref PrjCallbackData callbackData, ref Guid enumerationId) => 0;
static int GetDirectoryEnumerationCallback(ref PrjCallbackData callbackData, ref Guid enumerationId, string searchExpression, IntPtr dirEntryBufferHandle) => 0;
static int GetPlaceholderInfoCallback(ref PrjCallbackData callbackData) => 0;
static int GetFileDataCallback(ref PrjCallbackData callbackData, ulong byteOffset, uint length) => 0;

本机声明如下(很抱歉这个代码块可以滚动 - 基础知识已经很多了):

// Methods to mark directory as virtualization root, and start the projection provider.
[DllImport("ProjectedFSLib.dll", CharSet = CharSet.Unicode)]
static extern int PrjMarkDirectoryAsPlaceholder(string rootPathName,
    string targetPathName, IntPtr versionInfo, ref Guid virtualizationInstanceID);
[DllImport("ProjectedFSLib.dll", CharSet = CharSet.Unicode)]
static extern int PrjStartVirtualizing(string virtualizationRootPath,
    ref PrjCallbacks callbacks, IntPtr instanceContext, IntPtr options,
    ref IntPtr namespaceVirtualizationContext);

// Structure configuring the projection provider callbacks.
[StructLayout(LayoutKind.Sequential)]
struct PrjCallbacks
{
    public PrjStartDirectoryEnumerationCb StartDirectoryEnumerationCallback;
    public PrjEndDirectoryEnumerationCb EndDirectoryEnumerationCallback;
    public PrjGetDirectoryEnumerationCb GetDirectoryEnumerationCallback;
    public PrjGetPlaceholderInfoCb GetPlaceholderInfoCallback;
    public PrjGetFileDataCb GetFileDataCallback;
    public PrjQueryFileNameCb QueryFileNameCallback;
    public PrjNotificationCb NotificationCallback;
    public PrjCancelCommandCb CancelCommandCallback;
}

// Callback signatures.
delegate int PrjStartDirectoryEnumerationCb(ref PrjCallbackData callbackData, ref Guid enumerationId);
delegate int PrjEndDirectoryEnumerationCb(ref PrjCallbackData callbackData, ref Guid enumerationId);
delegate int PrjGetDirectoryEnumerationCb(ref PrjCallbackData callbackData, ref Guid enumerationId, string searchExpression, IntPtr dirEntryBufferHandle);
delegate int PrjGetPlaceholderInfoCb(ref PrjCallbackData callbackData);
delegate int PrjGetFileDataCb(ref PrjCallbackData callbackData, ulong byteOffset, uint length);
delegate int PrjQueryFileNameCb(ref PrjCallbackData callbackData);
delegate int PrjNotificationCb(ref PrjCallbackData callbackData, bool isDirectory, int notification, string destinationFileName, IntPtr operationParameters);
delegate int PrjCancelCommandCb(ref PrjCallbackData callbackData);

// Callback data passed to each of the callbacks above.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct PrjCallbackData
{
    public uint Size;
    public uint Flags;
    public IntPtr NamespaceVirtualizationContext;
    public int CommandId;
    public Guid FileId;
    public Guid DataStreamId;
    public string FilePathName;
    public IntPtr VersionInfo;
    public uint TriggeringProcessId;
    public string TriggeringProcessImageFileName;
    public IntPtr InstanceContext;
}

但是,我在编组从 OS 传递给回调的结构时似乎做错了什么,因为它导致托管应用程序立即停止而没有托管异常,并且只记录堆损坏Windows 事件查看器中的错误代码。

详细地说,我收到 PRJ_START_DIRECTORY_ENUMERATION_CB callback (PrjStartDirectoryEnumerationCb in my code) to start enumerating my directories. It passes a pointer to a PRJ_CALLBACK_DATA 结构(在我的代码中 PrjCallbackData)。

现在,虽然我显然将结构很好地接收到我的托管回调中,所有值都有意义直到最后一个成员 InstanceContext,但应用程序在尝试 return 值 0 时立即崩溃(S_OK).

static int StartDirectoryEnumerationCallback(ref PrjCallbackData callbackData, ref Guid enumerationId)
{
    return 0; // Crashes here or when stepping out of this method.
}

我试图找出我的错误所在,但由于调试立即停止,没有任何异常(我没有过滤掉任何异常),我没有走得太远。我意识到,当我更改回调以愚蠢地接受 IntPtr 而不是 ref PrjCallbackData 时,应用程序不会崩溃。

static int StartDirectoryEnumerationCallback(IntPtr callbackData, ref Guid enumerationId)
{
    return 0; // No crash executing this with IntPtr passed in.
}

delegate int PrjStartDirectoryEnumerationCb(IntPtr callbackData, ref Guid enumerationId);

从逻辑上讲,如果没有我可以访问的重要信息,这不会让我走得太远。

我是不是漏掉了一步?这么简单的结构映射是不是可以直接实现?

如果感兴趣,事件查看器条目如下所示。我是 运行 带有 .NET Framework 4.6 的新式 C# 项目文件中的应用程序(应该解释 "dotnet.exe" 可执行文件名称)。如果需要其他信息,我很乐意提供。

Faulting application name: dotnet.exe, version: 2.1.26919.1, time stamp: 0x5ba1bb46
Faulting module name: ntdll.dll, version: 10.0.17763.1, time stamp: 0xa369e897
Exception code: 0xc0000374
Fault offset: 0x00000000000fb349
Faulting process id: 0xfa8
Faulting application start time: 0x01d46d623aee076d
Faulting application path: C:\Program Files\dotnet\dotnet.exe
Faulting module path: C:\WINDOWS\SYSTEM32\ntdll.dll
Report Id: adcfba5c-dfd4-428d-8eb5-81aceada1983
Faulting package full name: 
Faulting package-relative application ID: 

请注意,如果您想尝试上面的示例代码,您必须在 Windows 10 1809 中安装 Projected Filesystem 功能,并将其编译为 x64(没有用于 x86 / AnyCPU 配置的本机库).

正如 Simon 评论的那样,在签名中只有一个 IntPtr 而不是结构,然后使用 Marshal.PtrToStructure<PrjCallbackData>(callbackData) 检索传递的结构的 copy我的回调工作正常。

或者,Hans 对结构中的字符串字段使用 IntPtr 的解决方案(并将结构保留在签名中)也有效,但我无法简单地访问字符串数据。

幸运的是,我不必将任何内容写回这个结构,否则我 运行 会在将其写回原始结构时遇到问题,而不是 copy 的结构,所以西蒙的解决方案在这里就足够了。

如果对完整代码覆盖感兴趣,可以找到存储库 here