runtime: [.NET 6.0.5] HeapValidate() returns false for the last heap returned by GetProcessHeaps(), when both WinAPI calls are made thru P/Invoke

Description

In a minimal .NET 6 console test app (running on a Win10 host OS with .NET 6.0.5 runtime and Visual Studio 2022 installed), calling the GetProcessHeaps() WinAPI function through C# P/Invoke, and then calling HeapValidate(), also through P/Invoke, on the very last Heap returned by the former, always returns false.

Additionally, when other more complex C++ based DLLs that are performing the same GetProcessHeaps() + HeapValidate() call sequence internally, are accessed through C# P/Invoke, they may also trigger a Memory Access Violation in ntdll.dll sometimes.

The same use case neither returns false, nor optionally crashes, when the test app is targeting .NET Framework 4.7.2. So the question is: Is this .NET6-specific behavior expected, or a bug?

Here is a minimal C# code sample demonstrating the HeapValidate() returning false:

/* TestApp.csproj defined as:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>disable</Nullable>
  </PropertyGroup>
</Project>
*/

using System;
using System.Runtime.InteropServices;

internal static class TestApp
{
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern UInt32 GetProcessHeaps(UInt32 NumberOfHeaps, IntPtr[] ProcessHeaps);

    [Flags]
    private enum HeapValidateFlags : uint
    {
        HEAP_NO_FLAGS_ZERO = 0x00000000,
        HEAP_NO_SERIALIZE = 0x00000001
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool HeapValidate(IntPtr hHeap, HeapValidateFlags dwFlags, IntPtr lpMem);

    private static void Main()
    {
        IntPtr[] heaps;
        {
            uint numHeaps = GetProcessHeaps(0, null);
            heaps = new IntPtr[numHeaps];
            GetProcessHeaps(numHeaps, heaps);
        }
        foreach (IntPtr heap in heaps)
        {
            bool heapValid = HeapValidate(heap, HeapValidateFlags.HEAP_NO_FLAGS_ZERO, IntPtr.Zero);
            Console.WriteLine("heapValid = {0}", heapValid);
        }
    }
}

Configuration

  • .NET 6 runtime: .NET 6.0.5
  • OS: Windows 10 21H2 (OS build 19044.2604)
  • Arch: x64

Regression?

The same GetProcessHeaps() and HeapValidate() WinAPI P/Invoke call sequence does not fail on .NET Framework 4.7.2, nor crashing in ntdll.dll.

Other information

When the WinAPI + P/Invoke sequence mentioned above triggers a Mem Access Violation crash in ntdll.dll, the crash stack trace looks like the following:

ntdll.dll!RtlEnterCriticalSection() ntdll.dll!RtlValidateHeap() KernelBase.dll!HeapValidate()

Thank you.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 22 (13 by maintainers)

Most upvoted comments

@jkotas or @AndyAyersMS Do either of you have any thoughts on this?

This has to do with Control Flow Guard security mitigation (CFG).

.NET Core apps have CFG enabled by default. .NET Framework apps have CFG disable by default.

When the CFG is enable, Windows OS protects the dynamic unwind heap by flipping the memory protections from R/O to R/W before performing any operation on the heap and back from R/W to R/O after the operation. This is done as part of the security hardening of the CFG feature.

The stray HeapValidate call on the dynamic unwind heap is missing the R/O to R/W flip and that’s why it is crashing. There is nothing corrupted. The problem is that the HeapValidate call needs to take a lock, taking the lock needs to write to the heap memory that results into a crash because the memory is read-only.

You can reproduce the same behavior with .NET Framework if you enable CFG on the .NET Framework app. You can do that by running editbin /GUARD:CF Program.exe in the VS command prompt. Similarly, you can make .NET Core app “work” by disabling the CFG security mitigation by editing the .exe (I would not recommend doing that).

My guess is that this issue is by design from the Windows OS point of view. Applications are not supposed to be calling HeapValidate on heaps that they do not own, such as internal Windows OS heaps.

My guess is that this issue is by design from the Windows OS point of view. Applications are not supposed to be calling HeapValidate on heaps that they do not own, such as internal Windows OS heaps.

This is good analysis. I tried using the HeapLock() API and it A/V immediately. Playing with the CFG flags made things a lot better.

@smihaila I’m going to close this as “by design”. Sorry for the confusion, but glad for the education.

I would guess something unrelated to unwind is corrupting the table entries. Seems like we could track this down with time travel debugging.

@smihaila I have added triple back ticks around your code to make it readable.