From fc1576b99ac49ca2ae04c981d0dcb061d073bffa Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 16 Apr 2026 17:44:17 -0400 Subject: [PATCH 1/5] Add cdac-dump-inspect script for ad-hoc dump analysis Add a developer tool for inspecting .NET crash dumps using ClrMD and the cDAC. Supports three commands: - descriptor: print the full contract descriptor (contracts, types, globals) - threads: list managed threads with OS ID and state - stacks: walk managed stacks showing instruction pointers and method descriptors Includes a PowerShell wrapper for easy invocation, README documentation, cycle detection for corrupt thread lists, and descriptor size validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/managed/cdac/scripts/README.md | 73 ++++ .../managed/cdac/scripts/cdac-dump-inspect.cs | 324 ++++++++++++++++++ .../cdac/scripts/cdac-dump-inspect.csproj | 16 + .../cdac/scripts/cdac-dump-inspect.ps1 | 54 +++ 4 files changed, 467 insertions(+) create mode 100644 src/native/managed/cdac/scripts/README.md create mode 100644 src/native/managed/cdac/scripts/cdac-dump-inspect.cs create mode 100644 src/native/managed/cdac/scripts/cdac-dump-inspect.csproj create mode 100644 src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 diff --git a/src/native/managed/cdac/scripts/README.md b/src/native/managed/cdac/scripts/README.md new file mode 100644 index 00000000000000..7a4ed08a57ff99 --- /dev/null +++ b/src/native/managed/cdac/scripts/README.md @@ -0,0 +1,73 @@ +# cDAC Scripts + +Ad-hoc developer tools for inspecting .NET crash dumps using the cDAC (contract-based Data Access) reader. + +## cdac-dump-inspect + +A command-line tool that opens a .NET process dump with [ClrMD](https://github.com/microsoft/clrmd) and the cDAC, and runs diagnostic commands against it. + +### Prerequisites + +- The repo's local .dotnet SDK (built via `build.cmd`/`build.sh`) +- A .NET crash dump (Windows minidump, Linux coredump, or macOS coredump) from a runtime that includes the `DotNetRuntimeContractDescriptor` export + +### Quick Start + +From the repo root: + +```powershell +# Print the contract descriptor (contracts, types, globals) +./src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 descriptor + +# List managed threads +./src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 threads + +# Print managed stack traces +./src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 stacks +``` + +Or run directly with `dotnet run`: + +```powershell +.dotnet/dotnet run --project src/native/managed/cdac/scripts/cdac-dump-inspect.csproj -c Release -- descriptor +``` + +### Commands + +| Command | Description | +|---------|-------------| +| `descriptor` | Print the full contract descriptor: version, baseline, contracts with versions, types with fields, and globals | +| `threads` | List all managed threads with OS ID, thread state, and address | +| `stacks` | Walk the managed stack for each thread, showing instruction pointers and method descriptors | + +### Options (PowerShell wrapper) + +| Option | Description | +|--------|-------------| +| `-Release` | Build in Release configuration (default: Debug) | + +### Examples + +```powershell +# Inspect contracts in a Linux coredump +./src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 descriptor ~/dumps/crash.coredump + +# List threads in a Windows minidump +./src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 threads C:\dumps\app.dmp + +# Get stack traces (Release build for no debug assertions) +./src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 stacks C:\dumps\app.dmp -Release +``` + +### Notes + +- The dump must be from a runtime that embeds the cDAC contract descriptor (the `DotNetRuntimeContractDescriptor` export in coreclr). Older runtimes or stripped builds may not include it. +- The tool reads dumps using ClrMD's `DataTarget.LoadDump`, which supports Windows minidumps, Linux ELF coredumps, and macOS Mach-O coredumps. +- Thread and stack commands require matching data descriptor versions between the dump's runtime and the locally-built cDAC contracts. Version mismatches may produce errors or empty results. +- For Release builds, debug assertions in the cDAC are disabled, which allows reading dumps with minor version mismatches. + +### See Also + +- [cDAC overview](../README.md) +- [cDAC tests](../tests/README.md) +- [Contract descriptor format](../../../../docs/design/datacontracts/contract-descriptor.md) diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.cs b/src/native/managed/cdac/scripts/cdac-dump-inspect.cs new file mode 100644 index 00000000000000..216110e88c2d83 --- /dev/null +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.cs @@ -0,0 +1,324 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Usage: +// dotnet run -- descriptor Print the raw contract descriptor +// dotnet run -- threads List managed threads +// dotnet run -- stacks Print managed stack traces + +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.DataContractReader; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +if (args.Length < 2) +{ + Console.WriteLine("Usage: cdac-dump-inspect "); + Console.WriteLine("Commands:"); + Console.WriteLine(" descriptor Print the raw contract descriptor (contracts, types, globals)"); + Console.WriteLine(" threads List managed threads"); + Console.WriteLine(" stacks Print managed stack traces for all threads"); + return 1; +} + +string command = args[0]; +string dumpPath = args[1]; + +if (!File.Exists(dumpPath)) +{ + Console.Error.WriteLine($"Dump not found: {dumpPath}"); + return 1; +} + +try +{ + switch (command) + { + case "descriptor": + DumpDescriptor(dumpPath); + break; + case "threads": + DumpThreads(dumpPath); + break; + case "stacks": + DumpStacks(dumpPath); + break; + default: + Console.Error.WriteLine($"Unknown command: {command}"); + return 1; + } +} +catch (System.Exception ex) +{ + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; +} + +return 0; + +// --------------------------------------------------------------------------- + +static ulong FindContractDescriptor(DataTarget dt) +{ + foreach (ModuleInfo module in dt.DataReader.EnumerateModules()) + { + string? fileName = module.FileName; + if (fileName is null) + continue; + + int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')); + string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; + if (!name.Contains("coreclr", StringComparison.OrdinalIgnoreCase) || + name.Contains("dac", StringComparison.OrdinalIgnoreCase)) + continue; + + ulong addr = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); + if (addr != 0) + { + if (dt.DataReader.PointerSize == 4) + addr &= 0xFFFF_FFFF; + return addr; + } + } + + throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export."); +} + +static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) +{ + ulong contractAddr = FindContractDescriptor(dt); + + if (!ContractDescriptorTarget.TryCreate( + contractAddr, + dt.DataReader.Read, + (ulong address, Span buffer) => throw new NotSupportedException("Read-only dump."), + (uint threadId, uint contextFlags, Span buffer) => + dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? buffer.Length : -1, + [CoreCLRContracts.Register], + out ContractDescriptorTarget? target)) + { + throw new InvalidOperationException("Failed to create cDAC target."); + } + + return target; +} + +static void DumpDescriptor(string dumpPath) +{ + using DataTarget dt = DataTarget.LoadDump(dumpPath); + ulong contractAddr = FindContractDescriptor(dt); + int ptrSize = dt.DataReader.PointerSize; + + Console.WriteLine($"Dump: {dumpPath}"); + Console.WriteLine($"Pointer size: {ptrSize}"); + Console.WriteLine($"Contract descriptor at: 0x{contractAddr:x}"); + + ulong addr = contractAddr; + + byte[] magic = new byte[8]; + dt.DataReader.Read(addr, magic); + Console.WriteLine($"Magic: {System.Text.Encoding.ASCII.GetString(magic).TrimEnd('\0')}"); + addr += 8; + + Span buf4 = stackalloc byte[4]; + dt.DataReader.Read(addr, buf4); + uint flags = BitConverter.ToUInt32(buf4); + int targetPtrSize = (flags & 0x2) == 0 ? 8 : 4; + Console.WriteLine($"Flags: 0x{flags:x} (target pointer size: {targetPtrSize})"); + addr += 4; + + dt.DataReader.Read(addr, buf4); + uint descriptorSize = BitConverter.ToUInt32(buf4); + if (descriptorSize > 10 * 1024 * 1024) + { + Console.Error.WriteLine($"Descriptor size {descriptorSize} exceeds 10MB limit. Dump may be corrupted."); + return; + } + addr += 4; + + Span bufPtr = stackalloc byte[ptrSize]; + dt.DataReader.Read(addr, bufPtr); + ulong descriptorPtr = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); + + byte[] jsonBytes = new byte[descriptorSize]; + int jsonRead = dt.DataReader.Read(descriptorPtr, jsonBytes); + + var descriptor = ContractDescriptorParser.ParseCompact(jsonBytes.AsSpan(0, jsonRead)); + if (descriptor is null) + { + Console.WriteLine("Failed to parse contract descriptor JSON."); + return; + } + + Console.WriteLine($"Version: {descriptor.Version}"); + Console.WriteLine($"Baseline: {descriptor.Baseline}"); + + if (descriptor.Contracts is { Count: > 0 }) + { + Console.WriteLine($"\nContracts ({descriptor.Contracts.Count}):"); + foreach (var kvp in descriptor.Contracts.OrderBy(c => c.Key)) + Console.WriteLine($" {kvp.Key} = {kvp.Value}"); + } + + if (descriptor.Types is { Count: > 0 }) + { + Console.WriteLine($"\nTypes ({descriptor.Types.Count}):"); + foreach (var kvp in descriptor.Types.OrderBy(t => t.Key)) + { + string size = kvp.Value.Size is not null ? $" (size: {kvp.Value.Size})" : ""; + int fieldCount = kvp.Value.Fields?.Count ?? 0; + Console.WriteLine($" {kvp.Key}{size} [{fieldCount} fields]"); + if (kvp.Value.Fields is not null) + { + foreach (var field in kvp.Value.Fields.OrderBy(f => f.Key)) + Console.WriteLine($" {field.Key}: offset={field.Value.Offset}, type={field.Value.Type ?? "?"}"); + } + } + } + + if (descriptor.Globals is { Count: > 0 }) + { + Console.WriteLine($"\nGlobals ({descriptor.Globals.Count}):"); + foreach (var kvp in descriptor.Globals.OrderBy(g => g.Key)) + { + var g = kvp.Value; + string val = g.NumericValue.HasValue ? $"0x{g.NumericValue.Value:x}" : g.StringValue ?? "?"; + string prefix = g.Type is not null ? $"type={g.Type}" : $"indirect={g.Indirect}"; + Console.WriteLine($" {kvp.Key}: {prefix}, value={val}"); + } + } +} + +static void DumpThreads(string dumpPath) +{ + using DataTarget dt = DataTarget.LoadDump(dumpPath); + var cdac = CreateCdacTarget(dt); + + Console.WriteLine($"Dump: {dumpPath}\n"); + + IThread threadContract = cdac.Contracts.GetContract(); + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + Console.WriteLine($"Thread count: {storeData.ThreadCount}\n"); + + int idx = 0; + HashSet visited = []; + TargetPointer threadAddr = storeData.FirstThread; + while (threadAddr != TargetPointer.Null) + { + if (!visited.Add(threadAddr.Value)) + { + Console.WriteLine($"Cycle detected in thread list at {threadAddr}"); + break; + } + try + { + ThreadData td = threadContract.GetThreadData(threadAddr); + Console.WriteLine($"Thread {idx}: OS ID=0x{td.OSId:x}, State=0x{(uint)td.State:x}, Addr={threadAddr}"); + threadAddr = td.NextThread; + } + catch (System.Exception ex) + { + Console.WriteLine($"Thread {idx}: Error reading at {threadAddr} - {ex.Message}"); + break; + } + idx++; + } +} + +static void DumpStacks(string dumpPath) +{ + using DataTarget dt = DataTarget.LoadDump(dumpPath); + var cdac = CreateCdacTarget(dt); + + Console.WriteLine($"Dump: {dumpPath}\n"); + + IThread threadContract = cdac.Contracts.GetContract(); + IStackWalk stackWalk = cdac.Contracts.GetContract(); + IRuntimeTypeSystem rts = cdac.Contracts.GetContract(); + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + int idx = 0; + HashSet visited = []; + TargetPointer threadAddr = storeData.FirstThread; + while (threadAddr != TargetPointer.Null) + { + if (!visited.Add(threadAddr.Value)) + { + Console.WriteLine($"Cycle detected in thread list at {threadAddr}"); + break; + } + + ThreadData td; + try + { + td = threadContract.GetThreadData(threadAddr); + } + catch (System.Exception ex) + { + Console.WriteLine($"Thread {idx} ({threadAddr}): Error - {ex.Message}\n"); + break; + } + + Console.WriteLine($"Thread {idx} (OS ID: 0x{td.OSId:x}):"); + + try + { + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(td)) + { + try + { + TargetPointer ip = stackWalk.GetInstructionPointer(frame); + TargetPointer mdPtr = stackWalk.GetMethodDescPtr(frame); + string frameName; + + if (mdPtr != TargetPointer.Null) + { + try + { + MethodDescHandle mdHandle = rts.GetMethodDescHandle(mdPtr); + if (rts.IsNoMetadataMethod(mdHandle, out string methodName)) + { + frameName = methodName; + } + else + { + TargetPointer mt = rts.GetMethodTable(mdHandle); + frameName = $"MD@0x{mdPtr.Value:x} (MT: 0x{mt.Value:x})"; + } + } + catch + { + frameName = $"MethodDesc@0x{mdPtr.Value:x}"; + } + } + else + { + TargetPointer frameAddr = stackWalk.GetFrameAddress(frame); + if (frameAddr != TargetPointer.Null) + { + try { frameName = $"[{stackWalk.GetFrameName(frameAddr)}]"; } + catch { frameName = $"[InternalFrame@0x{frameAddr.Value:x}]"; } + } + else + { + frameName = "[Native Frame]"; + } + } + + Console.WriteLine($" 0x{ip.Value:x16} {frameName}"); + } + catch (System.Exception ex) + { + Console.WriteLine($" "); + } + } + } + catch (System.Exception ex) + { + Console.WriteLine($" Stack walk failed: {ex.Message}"); + } + + Console.WriteLine(); + threadAddr = td.NextThread; + idx++; + } +} diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.csproj b/src/native/managed/cdac/scripts/cdac-dump-inspect.csproj new file mode 100644 index 00000000000000..521b9ed0fc76ae --- /dev/null +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.csproj @@ -0,0 +1,16 @@ + + + Exe + $(NetCoreAppToolCurrent) + enable + enable + true + $(NoWarn);NETCDAC0001 + + + + + + + + \ No newline at end of file diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 b/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 new file mode 100644 index 00000000000000..21b11f526be29b --- /dev/null +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 @@ -0,0 +1,54 @@ +#!/usr/bin/env pwsh +# cdac-dump-inspect.ps1 — Wrapper to build and run the cDAC dump inspection tool. +# +# Usage: +# ./cdac-dump-inspect.ps1 descriptor Print contract descriptor +# ./cdac-dump-inspect.ps1 threads List managed threads +# ./cdac-dump-inspect.ps1 stacks Print managed stack traces + +param( + [Parameter(Position = 0)] + [ValidateSet("descriptor", "threads", "stacks")] + [string]$Command, + + [Parameter(Position = 1)] + [string]$DumpPath, + + [switch]$Release +) + +$ErrorActionPreference = "Stop" +$scriptDir = $PSScriptRoot +$repoRoot = Resolve-Path (Join-Path $scriptDir "../../../../..") +$dotnetDir = Join-Path $repoRoot ".dotnet" +$dotnetExe = if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) { "dotnet.exe" } else { "dotnet" } +$dotnet = Join-Path $dotnetDir $dotnetExe +$projFile = Join-Path $scriptDir "cdac-dump-inspect.csproj" +$config = if ($Release) { "Release" } else { "Debug" } + +if (-not $Command -or -not $DumpPath) { + Write-Host "Usage: ./cdac-dump-inspect.ps1 " + Write-Host "" + Write-Host "Commands:" + Write-Host " descriptor Print the raw contract descriptor (contracts, types, globals)" + Write-Host " threads List managed threads" + Write-Host " stacks Print managed stack traces for all threads" + Write-Host "" + Write-Host "Options:" + Write-Host " -Release Build in Release configuration (default: Debug)" + exit 1 +} + +if (-not (Test-Path $DumpPath)) { + Write-Error "Dump not found: $DumpPath" + exit 1 +} + +# Build +Write-Host "Building cdac-dump-inspect ($config)..." -ForegroundColor DarkGray +& $dotnet build $projFile -c $config --nologo -v minimal +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Run +& $dotnet run --project $projFile -c $config --no-build -- $Command $DumpPath +exit $LASTEXITCODE From 29c06a560889a24f6b01f71476a78aab627b72fd Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 16 Apr 2026 22:47:25 -0400 Subject: [PATCH 2/5] Fix GetThreadContext delegate return value The GetTargetThreadContextDelegate follows HRESULT convention where 0 means success. Returning buffer.Length (912) caused TryGetThreadContext to treat every call as a failure, breaking stack walking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/managed/cdac/scripts/cdac-dump-inspect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.cs b/src/native/managed/cdac/scripts/cdac-dump-inspect.cs index 216110e88c2d83..e87d7e7aa0f392 100644 --- a/src/native/managed/cdac/scripts/cdac-dump-inspect.cs +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.cs @@ -92,7 +92,7 @@ static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) dt.DataReader.Read, (ulong address, Span buffer) => throw new NotSupportedException("Read-only dump."), (uint threadId, uint contextFlags, Span buffer) => - dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? buffer.Length : -1, + dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1, [CoreCLRContracts.Register], out ContractDescriptorTarget? target)) { From 5532d654f1736f7cb58cd0264d7fd78b0f2fc3b2 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 17 Apr 2026 15:07:44 -0400 Subject: [PATCH 3/5] Address PR review feedback - Add Test-Path check for repo-local SDK in PowerShell wrapper - Fix relative link to contract descriptor doc (off by one level) - Validate DataReader.Read return values for short/failed reads - Add null check for descriptor pointer - Print full exception (ex.ToString()) instead of just ex.Message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/managed/cdac/scripts/README.md | 2 +- .../managed/cdac/scripts/cdac-dump-inspect.cs | 31 ++++++++++++++++--- .../cdac/scripts/cdac-dump-inspect.ps1 | 6 ++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/native/managed/cdac/scripts/README.md b/src/native/managed/cdac/scripts/README.md index 7a4ed08a57ff99..f0e20db3c8552d 100644 --- a/src/native/managed/cdac/scripts/README.md +++ b/src/native/managed/cdac/scripts/README.md @@ -70,4 +70,4 @@ Or run directly with `dotnet run`: - [cDAC overview](../README.md) - [cDAC tests](../tests/README.md) -- [Contract descriptor format](../../../../docs/design/datacontracts/contract-descriptor.md) +- [Contract descriptor format](../../../../../docs/design/datacontracts/contract-descriptor.md) diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.cs b/src/native/managed/cdac/scripts/cdac-dump-inspect.cs index e87d7e7aa0f392..a5c5f5e7cbaeaf 100644 --- a/src/native/managed/cdac/scripts/cdac-dump-inspect.cs +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.cs @@ -49,7 +49,7 @@ } catch (System.Exception ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(ex.ToString()); return 1; } @@ -115,18 +115,30 @@ static void DumpDescriptor(string dumpPath) ulong addr = contractAddr; byte[] magic = new byte[8]; - dt.DataReader.Read(addr, magic); + if (dt.DataReader.Read(addr, magic) != magic.Length) + { + Console.Error.WriteLine($"Failed to read contract descriptor magic at 0x{addr:x}. Dump may be truncated or corrupted."); + return; + } Console.WriteLine($"Magic: {System.Text.Encoding.ASCII.GetString(magic).TrimEnd('\0')}"); addr += 8; Span buf4 = stackalloc byte[4]; - dt.DataReader.Read(addr, buf4); + if (dt.DataReader.Read(addr, buf4) != buf4.Length) + { + Console.Error.WriteLine($"Failed to read contract descriptor flags at 0x{addr:x}. Dump may be truncated or corrupted."); + return; + } uint flags = BitConverter.ToUInt32(buf4); int targetPtrSize = (flags & 0x2) == 0 ? 8 : 4; Console.WriteLine($"Flags: 0x{flags:x} (target pointer size: {targetPtrSize})"); addr += 4; - dt.DataReader.Read(addr, buf4); + if (dt.DataReader.Read(addr, buf4) != buf4.Length) + { + Console.Error.WriteLine($"Failed to read contract descriptor size at 0x{addr:x}. Dump may be truncated or corrupted."); + return; + } uint descriptorSize = BitConverter.ToUInt32(buf4); if (descriptorSize > 10 * 1024 * 1024) { @@ -136,8 +148,17 @@ static void DumpDescriptor(string dumpPath) addr += 4; Span bufPtr = stackalloc byte[ptrSize]; - dt.DataReader.Read(addr, bufPtr); + if (dt.DataReader.Read(addr, bufPtr) != bufPtr.Length) + { + Console.Error.WriteLine($"Failed to read descriptor pointer at 0x{addr:x}. Dump may be truncated or corrupted."); + return; + } ulong descriptorPtr = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); + if (descriptorPtr == 0) + { + Console.Error.WriteLine("Descriptor pointer is null. Dump may be corrupted."); + return; + } byte[] jsonBytes = new byte[descriptorSize]; int jsonRead = dt.DataReader.Read(descriptorPtr, jsonBytes); diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 b/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 index 21b11f526be29b..686f6438ba290b 100644 --- a/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 @@ -26,6 +26,12 @@ $dotnet = Join-Path $dotnetDir $dotnetExe $projFile = Join-Path $scriptDir "cdac-dump-inspect.csproj" $config = if ($Release) { "Release" } else { "Debug" } +if (-not (Test-Path $dotnet)) { + $buildScript = if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) { "build.cmd" } else { "build.sh" } + Write-Error "Repo-local dotnet SDK not found at '$dotnet'. Run '$buildScript' from the repository root to install the local SDK, then retry." + exit 1 +} + if (-not $Command -or -not $DumpPath) { Write-Host "Usage: ./cdac-dump-inspect.ps1 " Write-Host "" From a1caf96ebf84f9191a6a0d87efb5fc73510dba8e Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:23:45 -0400 Subject: [PATCH 4/5] Re-architect cdac-dump-inspect into multi-file structure - Use System.CommandLine for proper CLI argument parsing - Split into separate files: Program.cs, DescriptorCommand.cs, ThreadsCommand.cs, StacksCommand.cs, DumpHelpers.cs, ParsedDescriptor.cs - Add recursive sub-descriptor traversal and merge conflict detection - Resolve indirect globals through pointer data with dereferenced values - Fix read callback to return 0/-1 instead of byte count - Fix write callback to return -1 instead of throwing - Use explicit module allow-list with NativeAOT fallback - Fall back to dotnet on PATH when repo-local SDK is unavailable - Address all PR review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../managed/cdac/scripts/DescriptorCommand.cs | 350 ++++++++++++++++++ .../managed/cdac/scripts/DumpHelpers.cs | 65 ++++ .../managed/cdac/scripts/ParsedDescriptor.cs | 12 + src/native/managed/cdac/scripts/Program.cs | 19 + src/native/managed/cdac/scripts/README.md | 4 +- .../managed/cdac/scripts/StacksCommand.cs | 143 +++++++ .../managed/cdac/scripts/ThreadsCommand.cs | 80 ++++ .../managed/cdac/scripts/cdac-dump-inspect.cs | 345 ----------------- .../cdac/scripts/cdac-dump-inspect.csproj | 1 + .../cdac/scripts/cdac-dump-inspect.ps1 | 16 +- 10 files changed, 683 insertions(+), 352 deletions(-) create mode 100644 src/native/managed/cdac/scripts/DescriptorCommand.cs create mode 100644 src/native/managed/cdac/scripts/DumpHelpers.cs create mode 100644 src/native/managed/cdac/scripts/ParsedDescriptor.cs create mode 100644 src/native/managed/cdac/scripts/Program.cs create mode 100644 src/native/managed/cdac/scripts/StacksCommand.cs create mode 100644 src/native/managed/cdac/scripts/ThreadsCommand.cs delete mode 100644 src/native/managed/cdac/scripts/cdac-dump-inspect.cs diff --git a/src/native/managed/cdac/scripts/DescriptorCommand.cs b/src/native/managed/cdac/scripts/DescriptorCommand.cs new file mode 100644 index 00000000000000..c8a2ab3b396cbd --- /dev/null +++ b/src/native/managed/cdac/scripts/DescriptorCommand.cs @@ -0,0 +1,350 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.Diagnostics.DataContractReader; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.DotNet.Diagnostics.CdacDumpInspect; + +internal sealed class DescriptorCommand : Command +{ + private readonly Argument _dumpPath = new("dump-path") { Description = "Path to a .NET crash dump" }; + + public DescriptorCommand() : base("descriptor", "Print the raw contract descriptor (contracts, types, globals)") + { + Add(_dumpPath); + SetAction(Run); + } + + private int Run(ParseResult parse) + { + string dumpPath = parse.GetValue(_dumpPath)!; + if (!File.Exists(dumpPath)) + { + Console.Error.WriteLine($"Dump not found: {dumpPath}"); + return 1; + } + + try + { + Execute(dumpPath); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex.ToString()); + return 1; + } + + return 0; + } + + private static void Execute(string dumpPath) + { + using DataTarget dt = DataTarget.LoadDump(dumpPath); + ulong contractAddr = DumpHelpers.FindContractDescriptor(dt); + + Console.WriteLine($"Dump: {dumpPath}"); + Console.WriteLine($"Pointer size: {dt.DataReader.PointerSize}"); + + List descriptors = []; + HashSet visited = []; + CollectDescriptors(dt, contractAddr, "Main", descriptors, visited); + + if (descriptors.Count == 0) + { + Console.Error.WriteLine("No descriptors found."); + return; + } + + foreach (ParsedDescriptor pd in descriptors) + { + Console.WriteLine(); + Console.WriteLine($"=== {pd.Name} Descriptor (0x{pd.Address:x}) ==="); + Console.WriteLine($"Version: {pd.Descriptor.Version}"); + Console.WriteLine($"Baseline: {pd.Descriptor.Baseline}"); + PrintDescriptorContents(pd, dt); + } + + if (descriptors.Count > 1) + PrintMergeConflicts(descriptors); + } + + private static void PrintDescriptorContents(ParsedDescriptor pd, DataTarget dt) + { + var descriptor = pd.Descriptor; + + if (descriptor.Contracts is { Count: > 0 }) + { + Console.WriteLine($"\n Contracts ({descriptor.Contracts.Count}):"); + foreach (var kvp in descriptor.Contracts.OrderBy(c => c.Key)) + Console.WriteLine($" {kvp.Key} = {kvp.Value}"); + } + + if (descriptor.Types is { Count: > 0 }) + { + Console.WriteLine($"\n Types ({descriptor.Types.Count}):"); + foreach (var kvp in descriptor.Types.OrderBy(t => t.Key)) + { + string size = kvp.Value.Size is not null ? $" (size: {kvp.Value.Size})" : ""; + int fieldCount = kvp.Value.Fields?.Count ?? 0; + Console.WriteLine($" {kvp.Key}{size} [{fieldCount} fields]"); + if (kvp.Value.Fields is not null) + { + foreach (var field in kvp.Value.Fields.OrderBy(f => f.Key)) + Console.WriteLine($" {field.Key}: offset={field.Value.Offset}, type={field.Value.Type ?? "?"}"); + } + } + } + + if (descriptor.Globals is { Count: > 0 }) + { + Console.WriteLine($"\n Globals ({descriptor.Globals.Count}):"); + foreach (var kvp in descriptor.Globals.OrderBy(g => g.Key)) + PrintGlobalEntry(kvp.Key, kvp.Value, pd.PointerData, dt); + } + + if (descriptor.SubDescriptors is { Count: > 0 }) + { + Console.WriteLine($"\n Sub-descriptor references ({descriptor.SubDescriptors.Count}):"); + foreach (var kvp in descriptor.SubDescriptors.OrderBy(s => s.Key)) + PrintGlobalEntry(kvp.Key, kvp.Value, pd.PointerData, dt); + } + } + + private static void PrintGlobalEntry(string name, ContractDescriptorParser.GlobalDescriptor g, ulong[] pointerData, DataTarget dt) + { + string val = ResolveGlobalValue(g, pointerData); + string prefix = g.Type is not null ? $"type={g.Type}, " : ""; + + if (g.Indirect && g.NumericValue.HasValue) + { + ulong index = g.NumericValue.Value; + if (index < (ulong)pointerData.Length) + { + ulong addr = pointerData[index]; + int ptrSize = dt.DataReader.PointerSize; + Span buf = stackalloc byte[ptrSize]; + if (addr != 0 && dt.DataReader.Read(addr, buf) == buf.Length) + { + ulong pointedValue = ptrSize == 8 ? BitConverter.ToUInt64(buf) : BitConverter.ToUInt32(buf); + Console.WriteLine($" {name}: {prefix}indirect={g.Indirect}, value={val} -> *0x{pointedValue:x}"); + return; + } + } + } + + Console.WriteLine($" {name}: {prefix}indirect={g.Indirect}, value={val}"); + } + + private static string ResolveGlobalValue(ContractDescriptorParser.GlobalDescriptor g, ulong[] pointerData) + { + if (g.Indirect && g.NumericValue.HasValue) + { + ulong index = g.NumericValue.Value; + if (index < (ulong)pointerData.Length) + return $"0x{pointerData[index]:x} (pointer_data[{index}])"; + + return $"invalid index {index}"; + } + + return g.NumericValue.HasValue ? $"0x{g.NumericValue.Value:x}" : g.StringValue ?? "?"; + } + + private static void CollectDescriptors(DataTarget dt, ulong contractAddr, string name, + List results, HashSet visited) + { + if (!visited.Add(contractAddr)) + { + Console.Error.WriteLine($"Warning: cycle detected at 0x{contractAddr:x}, skipping."); + return; + } + + if (!TryReadDescriptor(dt, contractAddr, out var descriptor, out ulong[] pointerData)) + { + Console.Error.WriteLine($"Warning: failed to read descriptor at 0x{contractAddr:x}."); + return; + } + + results.Add(new ParsedDescriptor(name, contractAddr, descriptor, pointerData)); + + if (descriptor.SubDescriptors is null) + return; + + int ptrSize = dt.DataReader.PointerSize; + Span bufPtr = stackalloc byte[ptrSize]; + + foreach (var kvp in descriptor.SubDescriptors) + { + if (!kvp.Value.Indirect || !kvp.Value.NumericValue.HasValue) + continue; + + ulong index = kvp.Value.NumericValue.Value; + if (index >= (ulong)pointerData.Length) + continue; + + ulong subDescPtrAddr = pointerData[index]; + if (subDescPtrAddr == 0) + continue; + + if (dt.DataReader.Read(subDescPtrAddr, bufPtr) != bufPtr.Length) + continue; + + ulong subDescAddr = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); + if (subDescAddr == 0) + { + Console.Error.WriteLine($"Note: Sub-descriptor '{kvp.Key}' pointer is null (not populated at crash time)."); + continue; + } + + CollectDescriptors(dt, subDescAddr, kvp.Key, results, visited); + } + } + + private static bool TryReadDescriptor(DataTarget dt, ulong contractAddr, + out ContractDescriptorParser.ContractDescriptor descriptor, out ulong[] pointerData) + { + descriptor = null!; + pointerData = []; + + int ptrSize = dt.DataReader.PointerSize; + ulong addr = contractAddr; + + // Magic + byte[] magic = new byte[8]; + if (dt.DataReader.Read(addr, magic) != magic.Length) + return false; + addr += 8; + + // Flags + Span buf4 = stackalloc byte[4]; + if (dt.DataReader.Read(addr, buf4) != buf4.Length) + return false; + addr += 4; + + // Descriptor size + if (dt.DataReader.Read(addr, buf4) != buf4.Length) + return false; + uint descriptorSize = BitConverter.ToUInt32(buf4); + if (descriptorSize > 10 * 1024 * 1024) + return false; + addr += 4; + + // Descriptor pointer + Span bufPtr = stackalloc byte[ptrSize]; + if (dt.DataReader.Read(addr, bufPtr) != bufPtr.Length) + return false; + ulong descriptorPtr = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); + if (descriptorPtr == 0) + return false; + addr += (ulong)ptrSize; + + // Pointer data count + if (dt.DataReader.Read(addr, buf4) != buf4.Length) + return false; + uint pointerDataCount = BitConverter.ToUInt32(buf4); + addr += 4; + + // Padding + addr += 4; + + // Pointer data array address + if (dt.DataReader.Read(addr, bufPtr) != bufPtr.Length) + return false; + ulong pointerDataAddr = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); + + // Read pointer data entries + pointerData = new ulong[pointerDataCount]; + for (uint i = 0; i < pointerDataCount; i++) + { + if (dt.DataReader.Read(pointerDataAddr + i * (uint)ptrSize, bufPtr) != bufPtr.Length) + return false; + pointerData[i] = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); + } + + // Read and parse JSON + byte[] jsonBytes = new byte[descriptorSize]; + int jsonRead = dt.DataReader.Read(descriptorPtr, jsonBytes); + descriptor = ContractDescriptorParser.ParseCompact(jsonBytes.AsSpan(0, jsonRead))!; + + return descriptor is not null; + } + + private static void PrintMergeConflicts(List descriptors) + { + List conflicts = []; + + // Check duplicate contracts + var contractSources = new Dictionary>(); + foreach (ParsedDescriptor pd in descriptors) + { + foreach (var kvp in pd.Descriptor.Contracts ?? []) + { + if (!contractSources.TryGetValue(kvp.Key, out var list)) + { + list = []; + contractSources[kvp.Key] = list; + } + list.Add((pd.Name, kvp.Value)); + } + } + foreach (var kvp in contractSources.Where(c => c.Value.Count > 1).OrderBy(c => c.Key)) + { + string details = string.Join(", ", kvp.Value.Select(v => $"{v.Source}={v.Version}")); + conflicts.Add($" Contract '{kvp.Key}': {details}"); + } + + // Check duplicate types + var typeSources = new Dictionary>(); + foreach (ParsedDescriptor pd in descriptors) + { + foreach (var kvp in pd.Descriptor.Types ?? []) + { + if (!typeSources.TryGetValue(kvp.Key, out var list)) + { + list = []; + typeSources[kvp.Key] = list; + } + list.Add(pd.Name); + } + } + foreach (var kvp in typeSources.Where(t => t.Value.Count > 1).OrderBy(t => t.Key)) + { + conflicts.Add($" Type '{kvp.Key}': defined in {string.Join(", ", kvp.Value)}"); + } + + // Check duplicate globals + var globalSources = new Dictionary>(); + foreach (ParsedDescriptor pd in descriptors) + { + foreach (var kvp in pd.Descriptor.Globals ?? []) + { + if (!globalSources.TryGetValue(kvp.Key, out var list)) + { + list = []; + globalSources[kvp.Key] = list; + } + string val = ResolveGlobalValue(kvp.Value, pd.PointerData); + list.Add((pd.Name, val)); + } + } + foreach (var kvp in globalSources.Where(g => g.Value.Count > 1).OrderBy(g => g.Key)) + { + string details = string.Join(", ", kvp.Value.Select(v => $"{v.Source}={v.Value}")); + conflicts.Add($" Global '{kvp.Key}': {details}"); + } + + if (conflicts.Count > 0) + { + Console.WriteLine(); + Console.WriteLine($"=== Merge Conflicts ({conflicts.Count}) ==="); + foreach (string conflict in conflicts) + Console.WriteLine(conflict); + } + else + { + Console.WriteLine(); + Console.WriteLine("=== No Merge Conflicts ==="); + } + } +} diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs new file mode 100644 index 00000000000000..e7df671e6e2f46 --- /dev/null +++ b/src/native/managed/cdac/scripts/DumpHelpers.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.DotNet.Diagnostics.CdacDumpInspect; + +internal static class DumpHelpers +{ + private static readonly string[] s_coreClrModuleNames = ["coreclr.dll", "libcoreclr.so", "libcoreclr.dylib"]; + + public static ulong FindContractDescriptor(DataTarget dt) + { + // First pass: look in known CoreCLR modules. + // Second pass: check all remaining modules (covers NativeAOT where the export is in the app binary). + ulong fallback = 0; + foreach (ModuleInfo module in dt.DataReader.EnumerateModules()) + { + string? fileName = module.FileName; + if (fileName is null) + continue; + + ulong addr = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); + if (addr == 0) + continue; + + if (dt.DataReader.PointerSize == 4) + addr &= 0xFFFF_FFFF; + + int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')); + string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; + if (s_coreClrModuleNames.Contains(name, StringComparer.OrdinalIgnoreCase)) + return addr; + + if (fallback == 0) + fallback = addr; + } + + if (fallback != 0) + return fallback; + + throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export."); + } + + public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) + { + ulong contractAddr = FindContractDescriptor(dt); + + if (!ContractDescriptorTarget.TryCreate( + contractAddr, + (ulong address, Span buffer) => dt.DataReader.Read(address, buffer) == buffer.Length ? 0 : -1, + (ulong address, Span buffer) => -1, + (uint threadId, uint contextFlags, Span buffer) => + dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1, + [CoreCLRContracts.Register], + out ContractDescriptorTarget? target)) + { + throw new InvalidOperationException("Failed to create cDAC target."); + } + + return target; + } +} diff --git a/src/native/managed/cdac/scripts/ParsedDescriptor.cs b/src/native/managed/cdac/scripts/ParsedDescriptor.cs new file mode 100644 index 00000000000000..942ab9d8a9f4ee --- /dev/null +++ b/src/native/managed/cdac/scripts/ParsedDescriptor.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader; + +namespace Microsoft.DotNet.Diagnostics.CdacDumpInspect; + +internal sealed record ParsedDescriptor( + string Name, + ulong Address, + ContractDescriptorParser.ContractDescriptor Descriptor, + ulong[] PointerData); diff --git a/src/native/managed/cdac/scripts/Program.cs b/src/native/managed/cdac/scripts/Program.cs new file mode 100644 index 00000000000000..54d7057c5840c4 --- /dev/null +++ b/src/native/managed/cdac/scripts/Program.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; + +namespace Microsoft.DotNet.Diagnostics.CdacDumpInspect; + +internal sealed class Program +{ + public static async Task Main(string[] args) + { + RootCommand rootCommand = new("Inspect .NET crash dumps using the cDAC (contract-based Data Access) reader."); + rootCommand.Add(new DescriptorCommand()); + rootCommand.Add(new ThreadsCommand()); + rootCommand.Add(new StacksCommand()); + + return await rootCommand.Parse(args).InvokeAsync().ConfigureAwait(true); + } +} diff --git a/src/native/managed/cdac/scripts/README.md b/src/native/managed/cdac/scripts/README.md index f0e20db3c8552d..38e33868819453 100644 --- a/src/native/managed/cdac/scripts/README.md +++ b/src/native/managed/cdac/scripts/README.md @@ -8,7 +8,7 @@ A command-line tool that opens a .NET process dump with [ClrMD](https://github.c ### Prerequisites -- The repo's local .dotnet SDK (built via `build.cmd`/`build.sh`) +- A .NET SDK — the script prefers the repo-local `.dotnet` SDK (installed by `build.cmd`/`build.sh`) but falls back to any `dotnet` on your PATH - A .NET crash dump (Windows minidump, Linux coredump, or macOS coredump) from a runtime that includes the `DotNetRuntimeContractDescriptor` export ### Quick Start @@ -36,7 +36,7 @@ Or run directly with `dotnet run`: | Command | Description | |---------|-------------| -| `descriptor` | Print the full contract descriptor: version, baseline, contracts with versions, types with fields, and globals | +| `descriptor` | Print the full contract descriptor: version, baseline, contracts with versions, types with fields, globals, and sub-descriptors. Detects merge conflicts across descriptors. | | `threads` | List all managed threads with OS ID, thread state, and address | | `stacks` | Walk the managed stack for each thread, showing instruction pointers and method descriptors | diff --git a/src/native/managed/cdac/scripts/StacksCommand.cs b/src/native/managed/cdac/scripts/StacksCommand.cs new file mode 100644 index 00000000000000..af66b98964158c --- /dev/null +++ b/src/native/managed/cdac/scripts/StacksCommand.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.Diagnostics.DataContractReader; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.Runtime; + +using Exception = System.Exception; + +namespace Microsoft.DotNet.Diagnostics.CdacDumpInspect; + +internal sealed class StacksCommand : Command +{ + private readonly Argument _dumpPath = new("dump-path") { Description = "Path to a .NET crash dump" }; + + public StacksCommand() : base("stacks", "Print managed stack traces for all threads") + { + Add(_dumpPath); + SetAction(Run); + } + + private int Run(ParseResult parse) + { + string dumpPath = parse.GetValue(_dumpPath)!; + if (!File.Exists(dumpPath)) + { + Console.Error.WriteLine($"Dump not found: {dumpPath}"); + return 1; + } + + try + { + Execute(dumpPath); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex.ToString()); + return 1; + } + + return 0; + } + + private static void Execute(string dumpPath) + { + using DataTarget dt = DataTarget.LoadDump(dumpPath); + var cdac = DumpHelpers.CreateCdacTarget(dt); + + Console.WriteLine($"Dump: {dumpPath}\n"); + + IThread threadContract = cdac.Contracts.GetContract(); + IStackWalk stackWalk = cdac.Contracts.GetContract(); + IRuntimeTypeSystem rts = cdac.Contracts.GetContract(); + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + int idx = 0; + HashSet visited = []; + TargetPointer threadAddr = storeData.FirstThread; + while (threadAddr != TargetPointer.Null) + { + if (!visited.Add(threadAddr.Value)) + { + Console.WriteLine($"Cycle detected in thread list at {threadAddr}"); + break; + } + + ThreadData td; + try + { + td = threadContract.GetThreadData(threadAddr); + } + catch (Exception ex) + { + Console.WriteLine($"Thread {idx} ({threadAddr}): Error - {ex.Message}\n"); + break; + } + + Console.WriteLine($"Thread {idx} (OS ID: 0x{td.OSId:x}):"); + + try + { + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(td)) + { + try + { + TargetPointer ip = stackWalk.GetInstructionPointer(frame); + TargetPointer mdPtr = stackWalk.GetMethodDescPtr(frame); + string frameName; + + if (mdPtr != TargetPointer.Null) + { + try + { + MethodDescHandle mdHandle = rts.GetMethodDescHandle(mdPtr); + if (rts.IsNoMetadataMethod(mdHandle, out string methodName)) + { + frameName = methodName; + } + else + { + TargetPointer mt = rts.GetMethodTable(mdHandle); + frameName = $"MD@0x{mdPtr.Value:x} (MT: 0x{mt.Value:x})"; + } + } + catch + { + frameName = $"MethodDesc@0x{mdPtr.Value:x}"; + } + } + else + { + TargetPointer frameAddr = stackWalk.GetFrameAddress(frame); + if (frameAddr != TargetPointer.Null) + { + try { frameName = $"[{stackWalk.GetFrameName(frameAddr)}]"; } + catch { frameName = $"[InternalFrame@0x{frameAddr.Value:x}]"; } + } + else + { + frameName = "[Native Frame]"; + } + } + + Console.WriteLine($" 0x{ip.Value:x16} {frameName}"); + } + catch (Exception ex) + { + Console.WriteLine($" "); + } + } + } + catch (Exception ex) + { + Console.WriteLine($" Stack walk failed: {ex.Message}"); + } + + Console.WriteLine(); + threadAddr = td.NextThread; + idx++; + } + } +} diff --git a/src/native/managed/cdac/scripts/ThreadsCommand.cs b/src/native/managed/cdac/scripts/ThreadsCommand.cs new file mode 100644 index 00000000000000..6afbc60360a7cb --- /dev/null +++ b/src/native/managed/cdac/scripts/ThreadsCommand.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.Diagnostics.DataContractReader; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.Runtime; + +using Exception = System.Exception; + +namespace Microsoft.DotNet.Diagnostics.CdacDumpInspect; + +internal sealed class ThreadsCommand : Command +{ + private readonly Argument _dumpPath = new("dump-path") { Description = "Path to a .NET crash dump" }; + + public ThreadsCommand() : base("threads", "List managed threads") + { + Add(_dumpPath); + SetAction(Run); + } + + private int Run(ParseResult parse) + { + string dumpPath = parse.GetValue(_dumpPath)!; + if (!File.Exists(dumpPath)) + { + Console.Error.WriteLine($"Dump not found: {dumpPath}"); + return 1; + } + + try + { + Execute(dumpPath); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex.ToString()); + return 1; + } + + return 0; + } + + private static void Execute(string dumpPath) + { + using DataTarget dt = DataTarget.LoadDump(dumpPath); + var cdac = DumpHelpers.CreateCdacTarget(dt); + + Console.WriteLine($"Dump: {dumpPath}\n"); + + IThread threadContract = cdac.Contracts.GetContract(); + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + Console.WriteLine($"Thread count: {storeData.ThreadCount}\n"); + + int idx = 0; + HashSet visited = []; + TargetPointer threadAddr = storeData.FirstThread; + while (threadAddr != TargetPointer.Null) + { + if (!visited.Add(threadAddr.Value)) + { + Console.WriteLine($"Cycle detected in thread list at {threadAddr}"); + break; + } + try + { + ThreadData td = threadContract.GetThreadData(threadAddr); + Console.WriteLine($"Thread {idx}: OS ID=0x{td.OSId:x}, State=0x{(uint)td.State:x}, Addr={threadAddr}"); + threadAddr = td.NextThread; + } + catch (Exception ex) + { + Console.WriteLine($"Thread {idx}: Error reading at {threadAddr} - {ex.Message}"); + break; + } + idx++; + } + } +} diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.cs b/src/native/managed/cdac/scripts/cdac-dump-inspect.cs deleted file mode 100644 index a5c5f5e7cbaeaf..00000000000000 --- a/src/native/managed/cdac/scripts/cdac-dump-inspect.cs +++ /dev/null @@ -1,345 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Usage: -// dotnet run -- descriptor Print the raw contract descriptor -// dotnet run -- threads List managed threads -// dotnet run -- stacks Print managed stack traces - -using Microsoft.Diagnostics.Runtime; -using Microsoft.Diagnostics.DataContractReader; -using Microsoft.Diagnostics.DataContractReader.Contracts; - -if (args.Length < 2) -{ - Console.WriteLine("Usage: cdac-dump-inspect "); - Console.WriteLine("Commands:"); - Console.WriteLine(" descriptor Print the raw contract descriptor (contracts, types, globals)"); - Console.WriteLine(" threads List managed threads"); - Console.WriteLine(" stacks Print managed stack traces for all threads"); - return 1; -} - -string command = args[0]; -string dumpPath = args[1]; - -if (!File.Exists(dumpPath)) -{ - Console.Error.WriteLine($"Dump not found: {dumpPath}"); - return 1; -} - -try -{ - switch (command) - { - case "descriptor": - DumpDescriptor(dumpPath); - break; - case "threads": - DumpThreads(dumpPath); - break; - case "stacks": - DumpStacks(dumpPath); - break; - default: - Console.Error.WriteLine($"Unknown command: {command}"); - return 1; - } -} -catch (System.Exception ex) -{ - Console.Error.WriteLine(ex.ToString()); - return 1; -} - -return 0; - -// --------------------------------------------------------------------------- - -static ulong FindContractDescriptor(DataTarget dt) -{ - foreach (ModuleInfo module in dt.DataReader.EnumerateModules()) - { - string? fileName = module.FileName; - if (fileName is null) - continue; - - int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')); - string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; - if (!name.Contains("coreclr", StringComparison.OrdinalIgnoreCase) || - name.Contains("dac", StringComparison.OrdinalIgnoreCase)) - continue; - - ulong addr = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); - if (addr != 0) - { - if (dt.DataReader.PointerSize == 4) - addr &= 0xFFFF_FFFF; - return addr; - } - } - - throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export."); -} - -static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) -{ - ulong contractAddr = FindContractDescriptor(dt); - - if (!ContractDescriptorTarget.TryCreate( - contractAddr, - dt.DataReader.Read, - (ulong address, Span buffer) => throw new NotSupportedException("Read-only dump."), - (uint threadId, uint contextFlags, Span buffer) => - dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1, - [CoreCLRContracts.Register], - out ContractDescriptorTarget? target)) - { - throw new InvalidOperationException("Failed to create cDAC target."); - } - - return target; -} - -static void DumpDescriptor(string dumpPath) -{ - using DataTarget dt = DataTarget.LoadDump(dumpPath); - ulong contractAddr = FindContractDescriptor(dt); - int ptrSize = dt.DataReader.PointerSize; - - Console.WriteLine($"Dump: {dumpPath}"); - Console.WriteLine($"Pointer size: {ptrSize}"); - Console.WriteLine($"Contract descriptor at: 0x{contractAddr:x}"); - - ulong addr = contractAddr; - - byte[] magic = new byte[8]; - if (dt.DataReader.Read(addr, magic) != magic.Length) - { - Console.Error.WriteLine($"Failed to read contract descriptor magic at 0x{addr:x}. Dump may be truncated or corrupted."); - return; - } - Console.WriteLine($"Magic: {System.Text.Encoding.ASCII.GetString(magic).TrimEnd('\0')}"); - addr += 8; - - Span buf4 = stackalloc byte[4]; - if (dt.DataReader.Read(addr, buf4) != buf4.Length) - { - Console.Error.WriteLine($"Failed to read contract descriptor flags at 0x{addr:x}. Dump may be truncated or corrupted."); - return; - } - uint flags = BitConverter.ToUInt32(buf4); - int targetPtrSize = (flags & 0x2) == 0 ? 8 : 4; - Console.WriteLine($"Flags: 0x{flags:x} (target pointer size: {targetPtrSize})"); - addr += 4; - - if (dt.DataReader.Read(addr, buf4) != buf4.Length) - { - Console.Error.WriteLine($"Failed to read contract descriptor size at 0x{addr:x}. Dump may be truncated or corrupted."); - return; - } - uint descriptorSize = BitConverter.ToUInt32(buf4); - if (descriptorSize > 10 * 1024 * 1024) - { - Console.Error.WriteLine($"Descriptor size {descriptorSize} exceeds 10MB limit. Dump may be corrupted."); - return; - } - addr += 4; - - Span bufPtr = stackalloc byte[ptrSize]; - if (dt.DataReader.Read(addr, bufPtr) != bufPtr.Length) - { - Console.Error.WriteLine($"Failed to read descriptor pointer at 0x{addr:x}. Dump may be truncated or corrupted."); - return; - } - ulong descriptorPtr = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); - if (descriptorPtr == 0) - { - Console.Error.WriteLine("Descriptor pointer is null. Dump may be corrupted."); - return; - } - - byte[] jsonBytes = new byte[descriptorSize]; - int jsonRead = dt.DataReader.Read(descriptorPtr, jsonBytes); - - var descriptor = ContractDescriptorParser.ParseCompact(jsonBytes.AsSpan(0, jsonRead)); - if (descriptor is null) - { - Console.WriteLine("Failed to parse contract descriptor JSON."); - return; - } - - Console.WriteLine($"Version: {descriptor.Version}"); - Console.WriteLine($"Baseline: {descriptor.Baseline}"); - - if (descriptor.Contracts is { Count: > 0 }) - { - Console.WriteLine($"\nContracts ({descriptor.Contracts.Count}):"); - foreach (var kvp in descriptor.Contracts.OrderBy(c => c.Key)) - Console.WriteLine($" {kvp.Key} = {kvp.Value}"); - } - - if (descriptor.Types is { Count: > 0 }) - { - Console.WriteLine($"\nTypes ({descriptor.Types.Count}):"); - foreach (var kvp in descriptor.Types.OrderBy(t => t.Key)) - { - string size = kvp.Value.Size is not null ? $" (size: {kvp.Value.Size})" : ""; - int fieldCount = kvp.Value.Fields?.Count ?? 0; - Console.WriteLine($" {kvp.Key}{size} [{fieldCount} fields]"); - if (kvp.Value.Fields is not null) - { - foreach (var field in kvp.Value.Fields.OrderBy(f => f.Key)) - Console.WriteLine($" {field.Key}: offset={field.Value.Offset}, type={field.Value.Type ?? "?"}"); - } - } - } - - if (descriptor.Globals is { Count: > 0 }) - { - Console.WriteLine($"\nGlobals ({descriptor.Globals.Count}):"); - foreach (var kvp in descriptor.Globals.OrderBy(g => g.Key)) - { - var g = kvp.Value; - string val = g.NumericValue.HasValue ? $"0x{g.NumericValue.Value:x}" : g.StringValue ?? "?"; - string prefix = g.Type is not null ? $"type={g.Type}" : $"indirect={g.Indirect}"; - Console.WriteLine($" {kvp.Key}: {prefix}, value={val}"); - } - } -} - -static void DumpThreads(string dumpPath) -{ - using DataTarget dt = DataTarget.LoadDump(dumpPath); - var cdac = CreateCdacTarget(dt); - - Console.WriteLine($"Dump: {dumpPath}\n"); - - IThread threadContract = cdac.Contracts.GetContract(); - ThreadStoreData storeData = threadContract.GetThreadStoreData(); - Console.WriteLine($"Thread count: {storeData.ThreadCount}\n"); - - int idx = 0; - HashSet visited = []; - TargetPointer threadAddr = storeData.FirstThread; - while (threadAddr != TargetPointer.Null) - { - if (!visited.Add(threadAddr.Value)) - { - Console.WriteLine($"Cycle detected in thread list at {threadAddr}"); - break; - } - try - { - ThreadData td = threadContract.GetThreadData(threadAddr); - Console.WriteLine($"Thread {idx}: OS ID=0x{td.OSId:x}, State=0x{(uint)td.State:x}, Addr={threadAddr}"); - threadAddr = td.NextThread; - } - catch (System.Exception ex) - { - Console.WriteLine($"Thread {idx}: Error reading at {threadAddr} - {ex.Message}"); - break; - } - idx++; - } -} - -static void DumpStacks(string dumpPath) -{ - using DataTarget dt = DataTarget.LoadDump(dumpPath); - var cdac = CreateCdacTarget(dt); - - Console.WriteLine($"Dump: {dumpPath}\n"); - - IThread threadContract = cdac.Contracts.GetContract(); - IStackWalk stackWalk = cdac.Contracts.GetContract(); - IRuntimeTypeSystem rts = cdac.Contracts.GetContract(); - ThreadStoreData storeData = threadContract.GetThreadStoreData(); - - int idx = 0; - HashSet visited = []; - TargetPointer threadAddr = storeData.FirstThread; - while (threadAddr != TargetPointer.Null) - { - if (!visited.Add(threadAddr.Value)) - { - Console.WriteLine($"Cycle detected in thread list at {threadAddr}"); - break; - } - - ThreadData td; - try - { - td = threadContract.GetThreadData(threadAddr); - } - catch (System.Exception ex) - { - Console.WriteLine($"Thread {idx} ({threadAddr}): Error - {ex.Message}\n"); - break; - } - - Console.WriteLine($"Thread {idx} (OS ID: 0x{td.OSId:x}):"); - - try - { - foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(td)) - { - try - { - TargetPointer ip = stackWalk.GetInstructionPointer(frame); - TargetPointer mdPtr = stackWalk.GetMethodDescPtr(frame); - string frameName; - - if (mdPtr != TargetPointer.Null) - { - try - { - MethodDescHandle mdHandle = rts.GetMethodDescHandle(mdPtr); - if (rts.IsNoMetadataMethod(mdHandle, out string methodName)) - { - frameName = methodName; - } - else - { - TargetPointer mt = rts.GetMethodTable(mdHandle); - frameName = $"MD@0x{mdPtr.Value:x} (MT: 0x{mt.Value:x})"; - } - } - catch - { - frameName = $"MethodDesc@0x{mdPtr.Value:x}"; - } - } - else - { - TargetPointer frameAddr = stackWalk.GetFrameAddress(frame); - if (frameAddr != TargetPointer.Null) - { - try { frameName = $"[{stackWalk.GetFrameName(frameAddr)}]"; } - catch { frameName = $"[InternalFrame@0x{frameAddr.Value:x}]"; } - } - else - { - frameName = "[Native Frame]"; - } - } - - Console.WriteLine($" 0x{ip.Value:x16} {frameName}"); - } - catch (System.Exception ex) - { - Console.WriteLine($" "); - } - } - } - catch (System.Exception ex) - { - Console.WriteLine($" Stack walk failed: {ex.Message}"); - } - - Console.WriteLine(); - threadAddr = td.NextThread; - idx++; - } -} diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.csproj b/src/native/managed/cdac/scripts/cdac-dump-inspect.csproj index 521b9ed0fc76ae..38346dc1a08094 100644 --- a/src/native/managed/cdac/scripts/cdac-dump-inspect.csproj +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.csproj @@ -12,5 +12,6 @@ + \ No newline at end of file diff --git a/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 b/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 index 686f6438ba290b..d7960294fef60a 100644 --- a/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 +++ b/src/native/managed/cdac/scripts/cdac-dump-inspect.ps1 @@ -22,14 +22,20 @@ $scriptDir = $PSScriptRoot $repoRoot = Resolve-Path (Join-Path $scriptDir "../../../../..") $dotnetDir = Join-Path $repoRoot ".dotnet" $dotnetExe = if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) { "dotnet.exe" } else { "dotnet" } -$dotnet = Join-Path $dotnetDir $dotnetExe $projFile = Join-Path $scriptDir "cdac-dump-inspect.csproj" $config = if ($Release) { "Release" } else { "Debug" } -if (-not (Test-Path $dotnet)) { - $buildScript = if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) { "build.cmd" } else { "build.sh" } - Write-Error "Repo-local dotnet SDK not found at '$dotnet'. Run '$buildScript' from the repository root to install the local SDK, then retry." - exit 1 +# Prefer repo-local SDK, fall back to dotnet on PATH. +$localDotnet = Join-Path $dotnetDir $dotnetExe +if (Test-Path $localDotnet) { + $dotnet = $localDotnet +} +else { + $dotnet = Get-Command $dotnetExe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source + if (-not $dotnet) { + Write-Error "dotnet SDK not found. Either run 'build.cmd'/'build.sh' to install the repo-local SDK, or ensure 'dotnet' is on your PATH." + exit 1 + } } if (-not $Command -or -not $DumpPath) { From 3146ecdff003bcdde2e3d3804fa869b59519b77e Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:17:16 -0400 Subject: [PATCH 5/5] Address PR review feedback - Check export before FileName to handle modules with null FileName - Validate magic bytes (DNCCDAC) in TryReadDescriptor - Cap pointerDataCount at 1024 and check null pointerDataAddr - Use ulong arithmetic for pointer data offset to prevent overflow - Validate full JSON read and catch parse exceptions gracefully - Remove misleading pointer dereference from indirect global display Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../managed/cdac/scripts/DescriptorCommand.cs | 50 +++++++++---------- .../managed/cdac/scripts/DumpHelpers.cs | 16 +++--- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/native/managed/cdac/scripts/DescriptorCommand.cs b/src/native/managed/cdac/scripts/DescriptorCommand.cs index c8a2ab3b396cbd..a9e850906b6f74 100644 --- a/src/native/managed/cdac/scripts/DescriptorCommand.cs +++ b/src/native/managed/cdac/scripts/DescriptorCommand.cs @@ -63,14 +63,14 @@ private static void Execute(string dumpPath) Console.WriteLine($"=== {pd.Name} Descriptor (0x{pd.Address:x}) ==="); Console.WriteLine($"Version: {pd.Descriptor.Version}"); Console.WriteLine($"Baseline: {pd.Descriptor.Baseline}"); - PrintDescriptorContents(pd, dt); + PrintDescriptorContents(pd); } if (descriptors.Count > 1) PrintMergeConflicts(descriptors); } - private static void PrintDescriptorContents(ParsedDescriptor pd, DataTarget dt) + private static void PrintDescriptorContents(ParsedDescriptor pd) { var descriptor = pd.Descriptor; @@ -101,39 +101,21 @@ private static void PrintDescriptorContents(ParsedDescriptor pd, DataTarget dt) { Console.WriteLine($"\n Globals ({descriptor.Globals.Count}):"); foreach (var kvp in descriptor.Globals.OrderBy(g => g.Key)) - PrintGlobalEntry(kvp.Key, kvp.Value, pd.PointerData, dt); + PrintGlobalEntry(kvp.Key, kvp.Value, pd.PointerData); } if (descriptor.SubDescriptors is { Count: > 0 }) { Console.WriteLine($"\n Sub-descriptor references ({descriptor.SubDescriptors.Count}):"); foreach (var kvp in descriptor.SubDescriptors.OrderBy(s => s.Key)) - PrintGlobalEntry(kvp.Key, kvp.Value, pd.PointerData, dt); + PrintGlobalEntry(kvp.Key, kvp.Value, pd.PointerData); } } - private static void PrintGlobalEntry(string name, ContractDescriptorParser.GlobalDescriptor g, ulong[] pointerData, DataTarget dt) + private static void PrintGlobalEntry(string name, ContractDescriptorParser.GlobalDescriptor g, ulong[] pointerData) { string val = ResolveGlobalValue(g, pointerData); string prefix = g.Type is not null ? $"type={g.Type}, " : ""; - - if (g.Indirect && g.NumericValue.HasValue) - { - ulong index = g.NumericValue.Value; - if (index < (ulong)pointerData.Length) - { - ulong addr = pointerData[index]; - int ptrSize = dt.DataReader.PointerSize; - Span buf = stackalloc byte[ptrSize]; - if (addr != 0 && dt.DataReader.Read(addr, buf) == buf.Length) - { - ulong pointedValue = ptrSize == 8 ? BitConverter.ToUInt64(buf) : BitConverter.ToUInt32(buf); - Console.WriteLine($" {name}: {prefix}indirect={g.Indirect}, value={val} -> *0x{pointedValue:x}"); - return; - } - } - } - Console.WriteLine($" {name}: {prefix}indirect={g.Indirect}, value={val}"); } @@ -214,6 +196,10 @@ private static bool TryReadDescriptor(DataTarget dt, ulong contractAddr, byte[] magic = new byte[8]; if (dt.DataReader.Read(addr, magic) != magic.Length) return false; + ReadOnlySpan magicLE = "DNCCDAC\0"u8; + ReadOnlySpan magicBE = "\0CADCCND"u8; + if (!magic.AsSpan().SequenceEqual(magicLE) && !magic.AsSpan().SequenceEqual(magicBE)) + return false; addr += 8; // Flags @@ -243,6 +229,8 @@ private static bool TryReadDescriptor(DataTarget dt, ulong contractAddr, if (dt.DataReader.Read(addr, buf4) != buf4.Length) return false; uint pointerDataCount = BitConverter.ToUInt32(buf4); + if (pointerDataCount > 1024) + return false; addr += 4; // Padding @@ -252,12 +240,14 @@ private static bool TryReadDescriptor(DataTarget dt, ulong contractAddr, if (dt.DataReader.Read(addr, bufPtr) != bufPtr.Length) return false; ulong pointerDataAddr = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); + if (pointerDataCount > 0 && pointerDataAddr == 0) + return false; // Read pointer data entries pointerData = new ulong[pointerDataCount]; for (uint i = 0; i < pointerDataCount; i++) { - if (dt.DataReader.Read(pointerDataAddr + i * (uint)ptrSize, bufPtr) != bufPtr.Length) + if (dt.DataReader.Read(pointerDataAddr + (ulong)i * (ulong)ptrSize, bufPtr) != bufPtr.Length) return false; pointerData[i] = ptrSize == 8 ? BitConverter.ToUInt64(bufPtr) : BitConverter.ToUInt32(bufPtr); } @@ -265,7 +255,17 @@ private static bool TryReadDescriptor(DataTarget dt, ulong contractAddr, // Read and parse JSON byte[] jsonBytes = new byte[descriptorSize]; int jsonRead = dt.DataReader.Read(descriptorPtr, jsonBytes); - descriptor = ContractDescriptorParser.ParseCompact(jsonBytes.AsSpan(0, jsonRead))!; + if (jsonRead != (int)descriptorSize) + return false; + + try + { + descriptor = ContractDescriptorParser.ParseCompact(jsonBytes)!; + } + catch + { + return false; + } return descriptor is not null; } diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs index e7df671e6e2f46..fb1bb0acdf967c 100644 --- a/src/native/managed/cdac/scripts/DumpHelpers.cs +++ b/src/native/managed/cdac/scripts/DumpHelpers.cs @@ -18,10 +18,6 @@ public static ulong FindContractDescriptor(DataTarget dt) ulong fallback = 0; foreach (ModuleInfo module in dt.DataReader.EnumerateModules()) { - string? fileName = module.FileName; - if (fileName is null) - continue; - ulong addr = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); if (addr == 0) continue; @@ -29,10 +25,14 @@ public static ulong FindContractDescriptor(DataTarget dt) if (dt.DataReader.PointerSize == 4) addr &= 0xFFFF_FFFF; - int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')); - string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; - if (s_coreClrModuleNames.Contains(name, StringComparer.OrdinalIgnoreCase)) - return addr; + string? fileName = module.FileName; + if (fileName is not null) + { + int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')); + string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; + if (s_coreClrModuleNames.Contains(name, StringComparer.OrdinalIgnoreCase)) + return addr; + } if (fallback == 0) fallback = addr;