netcoredbg icon indicating copy to clipboard operation
netcoredbg copied to clipboard

Call to dbgshim.EnumerateCLRs always returns a null array and 0 CLRs when trying to attach to process

Open sanjacob opened this issue 8 months ago • 13 comments

I have not been able to debug any process using netcoredbg. As a minimal example, I tried debugging the following code:

// Program.cs (.NET 6.0)
while (true) { }

Console.WriteLine("Hello, World!");

I build and run the program from VS and then calling netcoredbg as such:

netcoredbg --attach XXXX --interpreter=vscode

But keep getting the error

Error: 0x80070057 Failed to attach to XXXX

By debugging the debugger I traced the root cause to the function EnumerateCLRs inside manageddebugger.cpp, which calls dbgshim.EnumerateCLRs(...). Which seems to always return null arrays and 0 as the length. I have tried both the compiled binaries and building from source.

Here is the output of my build:

cmake .. -G "Visual Studio 17 2022" -DCMAKE_POLICY_VERSION_MINIMUM="3.5" -A Win32
CMake Deprecation Warning at CMakeLists.txt:1 (cmake_minimum_required):
  Compatibility with CMake < 3.10 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value.  Or, use the <min>...<max> syntax
  to tell CMake that the project requires at least <min> but has been updated
  to work with policies introduced by <max> or earlier.


remote: Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
From https://github.com/dotnet/runtime
 * branch                release/8.0 -> FETCH_HEAD
Your branch is up to date with 'origin/release/8.0'.
Already on 'release/8.0'
VERBOSE: dotnet-install: Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:
VERBOSE: dotnet-install: - The SDK needs to be installed without user interaction and without admin rights.
VERBOSE: dotnet-install: - The SDK installation doesn't need to persist across multiple CI runs.
VERBOSE: dotnet-install: To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download
to get the installer.
VERBOSE: dotnet-install: Get-CLIArchitecture-From-Architecture -Architecture "x86"
VERBOSE: dotnet-install: Get-NormalizedQuality -Quality ""
VERBOSE: dotnet-install: Normalized quality: ''
VERBOSE: dotnet-install: Get-NormalizedChannel -Channel "8.0"
VERBOSE: dotnet-install: Normalized channel: '8.0'
VERBOSE: dotnet-install: Get-NormalizedProduct -Runtime ""
VERBOSE: dotnet-install: Normalized product: 'dotnet-sdk'
VERBOSE: dotnet-install: Action 'Product discovery' took 0.0359503 seconds
VERBOSE: dotnet-install: Resolve-Installation-Path -InstallDir "C:/.../netcoredbg/.dotnet"
VERBOSE: dotnet-install: InstallRoot: C:/.../netcoredbg/.dotnet
VERBOSE: Initialized feeds: https://builds.dotnet.microsoft.com/dotnet https://ci.dot.net/public
VERBOSE: dotnet-install: Get-AkaMSDownloadLink -Channel "8.0" -Quality "" -Internal "False" -Product "dotnet-sdk" -Architecture "x86"
VERBOSE: dotnet-install: Retrieving primary payload URL from aka.ms link for channel: '8.0', quality: '' product: 'dotnet-sdk', os: 'win', architecture: 'x86'.
VERBOSE: dotnet-install: Constructed aka.ms link: 'https://aka.ms/dotnet/8.0/dotnet-sdk-win-x86.zip'.
VERBOSE: dotnet-install: Received response:
StatusCode: 301, ReasonPhrase: 'Moved Permanently', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Request-Context: appId=cid-v1:d94c0f68-64bf-4036-8409-a0e761bb7ee1
  X-Response-Cache-Status: True
  Pragma: no-cache
  Connection: keep-alive
  Strict-Transport-Security: max-age=31536000 ; includeSubDomains
  Cache-Control: no-store, no-cache, max-age=0
  Date: Tue, 15 Jul 2025 21:30:58 GMT
  Location: https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/dotnet-sdk-8.0.412-win-x86.zip
  Server: Kestrel
  Content-Length: 0
  Expires: Tue, 15 Jul 2025 21:30:58 GMT
}
VERBOSE: dotnet-install: The redirect location retrieved: 'https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/dotnet-sdk-8.0.412-win-x86.zip'.
VERBOSE: dotnet-install: Received response:
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  x-ms-request-id: a829f155-c01e-0030-38ad-f5dbaf000000
  x-ms-version: 2009-09-19
  x-ms-lease-status: unlocked
  x-ms-blob-type: BlockBlob
  Connection: keep-alive
  Akamai-GRN: 0.94972017.1752615058.5a49146
  Cache-Control: public, max-age=849155
  Date: Tue, 15 Jul 2025 21:30:58 GMT
  ETag: 0x8DDBE44EAAA4627
  Content-Length: 271432569
  Content-Type: application/octet-stream
  Last-Modified: Tue, 08 Jul 2025 17:28:56 GMT
}
VERBOSE: dotnet-install: Retrieved primary named payload URL from aka.ms link:
'https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/dotnet-sdk-8.0.412-win-x86.zip'.
VERBOSE: dotnet-install: Downloading using legacy url will not be attempted.
VERBOSE: dotnet-install: Version: '8.0.412'.
VERBOSE: dotnet-install: Get-Product-Version -SpecificVersion "8.0.412" -PackageDownloadLink
"https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/dotnet-sdk-8.0.412-win-x86.zip"
VERBOSE: dotnet-install: Get-Product-Version-Url -Flattened "True" -AzureFeed "" -SpecificVersion "8.0.412" -PackageDownloadLink
"https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/dotnet-sdk-8.0.412-win-x86.zip"
VERBOSE: dotnet-install: Constructed productVersion link: https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/sdk-productVersion.txt
VERBOSE: dotnet-install: Get-Product-Version-Url -Flattened "False" -AzureFeed "" -SpecificVersion "8.0.412" -PackageDownloadLink
"https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/dotnet-sdk-8.0.412-win-x86.zip"
VERBOSE: dotnet-install: Constructed productVersion link: https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/productVersion.txt
VERBOSE: dotnet-install: Checking for the existence of https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/sdk-productVersion.txt
VERBOSE: dotnet-install: Product version: '8.0.412'.
VERBOSE: dotnet-install: Generated aka.ms link https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.412/dotnet-sdk-8.0.412-win-x86.zip with version 8.0.412
VERBOSE: dotnet-install: Checking if the version 8.0.412 is already installed
VERBOSE: dotnet-install: Is-Dotnet-Package-Installed -InstallRoot "C:/.../netcoredbg/.dotnet" -RelativePathToPackage "sdk" -SpecificVersion
"8.0.412"
VERBOSE: dotnet-install: Is-Dotnet-Package-Installed: DotnetPackagePath=C:\...\netcoredbg\.dotnet\sdk\8.0.412
dotnet-install: .NET Core SDK with version '8.0.412' is already installed.
dotnet-install: Adding to current process PATH: "C:\...\netcoredbg\.dotnet\". Note: This change will not be visible if PowerShell was run as a child process.
Building with 8.0.19 CoreCLR
CMake Deprecation Warning at third_party/linenoise-ng/CMakeLists.txt:3 (cmake_minimum_required):
  Compatibility with CMake < 3.10 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value.  Or, use the <min>...<max> syntax
  to tell CMake that the project requires at least <min> but has been updated
  to work with policies introduced by <max> or earlier.


-- Build mode: Release
CMake Warning (dev) at third_party/linenoise-ng/CMakeLists.txt:16 (set):
  implicitly converting 'path' to 'STRING' type.
This warning is for project developers.  Use -Wno-dev to suppress it.

-- Compiler type MSVC: C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x86/cl.exe
-- Configuring done (5.3s)
-- Generating done (0.1s)
-- Build files have been written to: C:/.../netcoredbg/build

From the comments in the code, I guess the coreclr module is not found, but I don't know where to go from there.

sanjacob avatar Jul 16 '25 17:07 sanjacob

I decided to build dbgshim.dll from source in order to figure out where things go wrong. You can follow along in https://github.com/dotnet/diagnostics/blob/main/src/dbgshim/dbgshim.cpp

  1. netcoredbg (manageddebugger.cpp) calls EnumerateCLRs() from dbgshim.dll to list available runtimes this is actually an abstraction (dbgshim.h) which finds the actual dbgshim.dll and loads it
  2. EnumerateCLRs() is called and everything goes well until HRESULT hr = GetRuntime(debuggeePID, clrRuntimeInfo); where S_FALSE is received. The function assumes this means everything went well and there were no runtimes found.
  3. Inside GetRuntime, the process is queried fine, we get the internal modules running as part of the process as usual. Then we loop over those modules, getting their path successfully, until we hit hr = GetTargetCLRMetrics(...)
    // Get the DBI/DAC index info for the regular coreclr module or check if single-file app by looking for the
    // DotNetRuntimeInfo export. We need to get the metrics too because that is required to get the startup event.
    DWORD rvaContinueStartupEvent = 0;
    hr = GetTargetCLRMetrics(modulePath, &clrRuntimeInfo.EngineMetrics, &clrRuntimeInfo.ClrInfo, &rvaContinueStartupEvent);
    
    hr will now contain an error, usually 0x80004005 which corresponds to E_FAIL (unspecified error)
  4. Now going into GetTargetCLRMetrics(...) we have a few sanity checks which are the cause of the errors. While some .dll files fail because of an "INVALID_HANDLE_VALUE", these seem to be part of System32. The vast majority of errors seem to be because of this check:
    if (!pedecoder.HasDirectoryEntry(IMAGE_DIRECTORY_ENTRY_EXPORT) ||
        !pedecoder.CheckDirectoryEntry(IMAGE_DIRECTORY_ENTRY_EXPORT))
    {
        return E_FAIL;
    }
    
  5. Thankfully it seems pedecoder is built as part of dotnet/diagnostics too. (The files are inside src\shared\inc). This is the offending function:
    inline BOOL PEDecoder::HasDirectoryEntry(int entry) const
    {
        CONTRACTL
        {
            INSTANCE_CHECK;
            PRECONDITION(CheckNTHeaders());
            NOTHROW;
            GC_NOTRIGGER;
            SUPPORTS_DAC;
        }
        CONTRACTL_END;
    
        if (Has32BitNTHeaders())
            return (GetNTHeaders32()->OptionalHeader.DataDirectory[entry].VirtualAddress != 0);
        else
            return (GetNTHeaders64()->OptionalHeader.DataDirectory[entry].VirtualAddress != 0);
    }
    
    Now the question is why is the DataDirectory header empty. Furthermore, I am of the opinion that the more the user knows about why something isn't working, the better. And the truth is, it is bullshit to expect a user to dig into a system library in order to figure out why they are getting an error. I know this is not even a netcoredbg issue, but that is where I started my investigation and for documentation and advice I thought I should leave it here.

sanjacob avatar Jul 16 '25 22:07 sanjacob

This can happen when trying to attach to the wrong unmanaged process. How did you choose pid of process to attach to?

gbalykov avatar Jul 17 '25 07:07 gbalykov

@gbalykov By looking into visual studio, and task manager

sanjacob avatar Jul 17 '25 15:07 sanjacob

What version of windows do you have?

gbalykov avatar Jul 17 '25 15:07 gbalykov

@gbalykov Windows 11 Enterprise Version 10.0.26100 Build 26100

sanjacob avatar Jul 17 '25 16:07 sanjacob

@gbalykov Opening various executables and dll's with CFF explorer it seems all of their Export Dir entries have an empty virtual address.

Edit: I have now found some counterexamples

sanjacob avatar Jul 17 '25 17:07 sanjacob

Image

sanjacob avatar Jul 17 '25 17:07 sanjacob

If the runtime info is not in the export section, where could it be? More importantly, how does Visual Studio and Rider both get away with it? Worth mentioning there is a .pdb file as well as a Debug Directory entry, and .NET dir entry, but I am not sure if they could replace the export section.

sanjacob avatar Jul 17 '25 18:07 sanjacob

@gbalykov Any advice?

sanjacob avatar Jul 22 '25 21:07 sanjacob

Have you tried launch of debuggee process by netcoredbg instead of attach?

gbalykov avatar Jul 31 '25 07:07 gbalykov

@gbalykov Unfortunately I don't think that is possible since it is a process created by another process.

sanjacob avatar Aug 01 '25 22:08 sanjacob

@gbalykov I don't mind writing my own debugger, please just point me in the right direction

sanjacob avatar Aug 06 '25 19:08 sanjacob

The vast majority of errors seem to be because of this check:

Which dll exactly gets E_FAILS in GetTargetCLRMetrics? Is your test built in debug mode?

Unfortunately I don't think that is possible since it is a process created by another process.

You can manually launch dll of your test from powershell.

gbalykov avatar Aug 07 '25 09:08 gbalykov