From f91d2dbe61a74095867aec86055d4e3cd04f1b32 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:27:55 -0400 Subject: [PATCH 1/6] [cDAC] Bail R2R MethodDesc lookup for odd entry points on AMD64/x86 Mirror the legacy DAC's fast path in ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage (src/coreclr/vm/readytoruninfo.cpp:371-376): on AMD64 and x86 a normal method entry point is always 2+ byte aligned, but a funclet can start at an odd address. The legacy DAC bails without probing the EntryPointToMethodDescMap PtrHashMap for these. The cDAC was probing the hashmap for odd entry points. That hash lands in bucket slots the producer-side DAC walker (which has the fast-path bail) never touches and therefore never enumerates into a triage minidump. The consumer-side cDAC probe then faults reading the not-in-dump bucket page, throws VirtualReadException, and aborts the whole stack walk a single frame past a SoftwareExceptionFrame. Manifests as SOS '!ClrStack' producing only: [SoftwareExceptionFrame: ...] Stack Walk failed. Reported stack incomplete. on net11 macOS triage dumps where cDAC defaults on (dotnet/diagnostics PR #5874). Diagnosed via dotnet/diagnostics issue #. Validated on the failing CI dump (SOS.ReflectionTest.Triage.dmp from public build 1483142, macOS x64): pre-fix dies after one frame; with fix produces the full eight-frame managed stack with file/line numbers, matching the legacy DAC's output. --- .../ExecutionManagerCore.ReadyToRunJitManager.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index 3b0a49add09c3b..3f93cc37f16f4b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -188,7 +188,7 @@ private uint GetR2RGCInfoVersion(Data.ReadyToRunInfo r2rInfo) { >= 21 => 5, >= 11 => 4, - < 11 => 3, + < 11 => 3, }; } @@ -274,6 +274,13 @@ private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInf TargetCodePointer startAddress = imageBase + function.BeginAddress; TargetPointer entryPoint = CodePointerUtils.AddressFromCodePointer(startAddress, Target); + RuntimeInfoArchitecture arch = Target.Contracts.RuntimeInfo.GetTargetArchitecture(); + if ((arch == RuntimeInfoArchitecture.X64 || arch == RuntimeInfoArchitecture.X86) + && (entryPoint.Value & 0x1) != 0) + { + return TargetPointer.Null; + } + TargetPointer methodDesc = _hashMap.GetValue(r2rInfo.EntryPointToMethodDescMap, entryPoint); if (methodDesc == (ulong)HashMapLookup.SpecialKeys.InvalidEntry) return TargetPointer.Null; From ec96aaeaaafe05a0af086d551af537e0016375fb Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Sat, 27 Jun 2026 21:26:34 -0400 Subject: [PATCH 2/6] [cDAC] Register IRuntimeInfo in ExecutionManager R2R unit tests The odd-entry-point bail added to GetMethodDescForRuntimeFunction calls Target.Contracts.RuntimeInfo.GetTargetArchitecture(), but the test target built by CreateTarget only registered IExecutionManager and a mock IPlatformMetadata. Every R2R test then threw NotImplementedException ('Contract IRuntimeInfo is not supported by the target'). Register a mock IRuntimeInfo that reports X64/X86 based on the test architecture's bitness, keeping the suite green while exercising the new x86/x64 branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index a8666ca5f5a5b9..d968c088df25e3 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -45,12 +45,16 @@ public class ExecutionManagerTests private static Target CreateTarget(MockExecutionManagerBuilder emBuilder) { var arch = emBuilder.Builder.TargetTestHelpers.Arch; + var runtimeInfo = new Mock(); + runtimeInfo.Setup(r => r.GetTargetArchitecture()) + .Returns(arch.Is64Bit ? RuntimeInfoArchitecture.X64 : RuntimeInfoArchitecture.X86); return new TestPlaceholderTarget.Builder(arch) .UseReader(emBuilder.Builder.GetMemoryContext().ReadFromTarget) .AddTypes(CreateContractTypes(emBuilder)) .AddGlobals(emBuilder.Globals) .AddContract(version: emBuilder.Version) .AddMockContract(Mock.Of()) + .AddMockContract(runtimeInfo) .Build(); } From 573842e958d3401490d421be0b4cd4db014bf4f7 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Sat, 27 Jun 2026 21:30:30 -0400 Subject: [PATCH 3/6] comment --- .../ExecutionManagerCore.ReadyToRunJitManager.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index 3f93cc37f16f4b..6d2b9f1b4db4fc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -274,6 +274,17 @@ private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInf TargetCodePointer startAddress = imageBase + function.BeginAddress; TargetPointer entryPoint = CodePointerUtils.AddressFromCodePointer(startAddress, Target); + // Mirror the legacy DAC fast path in ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage + // (src/coreclr/vm/readytoruninfo.cpp). + // + // This is not just an optimization for the cDAC: the createdump path takes the same + // bail when enumerating memory for a triage minidump, so the bucket page an odd-address + // probe would land in is never captured. Without this bail the cDAC would fault reading + // that not-in-dump page and abort the stack walk (see dotnet/diagnostics#5910). + // + // Only x86/x64 need this: on arm64 all code (incl. funclets) is 4-byte aligned so an + // entry point is never odd, and on arm32 an odd low bit is the Thumb indicator rather + // than a funclet marker. RuntimeInfoArchitecture arch = Target.Contracts.RuntimeInfo.GetTargetArchitecture(); if ((arch == RuntimeInfoArchitecture.X64 || arch == RuntimeInfoArchitecture.X86) && (entryPoint.Value & 0x1) != 0) From ff55472d2e1620a366a18011c74e584061f832e3 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Sat, 27 Jun 2026 23:40:31 -0400 Subject: [PATCH 4/6] update based on comments --- .../ExecutionManagerCore.ReadyToRunJitManager.cs | 14 +++----------- .../ExecutionManager/ExecutionManagerTests.cs | 4 ---- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index 6d2b9f1b4db4fc..9f2b700782df6c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -274,20 +274,12 @@ private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInf TargetCodePointer startAddress = imageBase + function.BeginAddress; TargetPointer entryPoint = CodePointerUtils.AddressFromCodePointer(startAddress, Target); - // Mirror the legacy DAC fast path in ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage - // (src/coreclr/vm/readytoruninfo.cpp). + // Method entryPoint are 4-byte aligned on all architectures. We can exit early for misaligned addresses. // // This is not just an optimization for the cDAC: the createdump path takes the same // bail when enumerating memory for a triage minidump, so the bucket page an odd-address - // probe would land in is never captured. Without this bail the cDAC would fault reading - // that not-in-dump page and abort the stack walk (see dotnet/diagnostics#5910). - // - // Only x86/x64 need this: on arm64 all code (incl. funclets) is 4-byte aligned so an - // entry point is never odd, and on arm32 an odd low bit is the Thumb indicator rather - // than a funclet marker. - RuntimeInfoArchitecture arch = Target.Contracts.RuntimeInfo.GetTargetArchitecture(); - if ((arch == RuntimeInfoArchitecture.X64 || arch == RuntimeInfoArchitecture.X86) - && (entryPoint.Value & 0x1) != 0) + // probe would land in is never captured. + if ((entryPoint.Value & 0x1) != 0) { return TargetPointer.Null; } diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index d968c088df25e3..a8666ca5f5a5b9 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -45,16 +45,12 @@ public class ExecutionManagerTests private static Target CreateTarget(MockExecutionManagerBuilder emBuilder) { var arch = emBuilder.Builder.TargetTestHelpers.Arch; - var runtimeInfo = new Mock(); - runtimeInfo.Setup(r => r.GetTargetArchitecture()) - .Returns(arch.Is64Bit ? RuntimeInfoArchitecture.X64 : RuntimeInfoArchitecture.X86); return new TestPlaceholderTarget.Builder(arch) .UseReader(emBuilder.Builder.GetMemoryContext().ReadFromTarget) .AddTypes(CreateContractTypes(emBuilder)) .AddGlobals(emBuilder.Globals) .AddContract(version: emBuilder.Version) .AddMockContract(Mock.Of()) - .AddMockContract(runtimeInfo) .Build(); } From 74d2ec6c88542b161523931846c11cd392fb906c Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Sun, 28 Jun 2026 00:28:47 -0400 Subject: [PATCH 5/6] Move R2R odd-entry-point bail to non-DAC builds; drop it from cDAC Per review feedback, fix the root cause instead of mirroring the bail in the cDAC. The odd-entry-point check in ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage is a perf optimization that skips a guaranteed-miss hashmap lookup for funclet addresses. In DAC builds it had the side effect of skipping the probe during createdump's memory enumeration, so the bucket pages were never captured into triage minidumps and the cDAC consumer faulted reading them. Gate the optimization with !DACCESS_COMPILE so the DAC performs the lookup and enumerates those pages, and remove the cDAC bail entirely so it reads naturally. Also delete the inaccurate 'PtrHashMap can't handle odd pointers' comment - the map handles odd keys fine, they are simply never present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/readytoruninfo.cpp | 9 +++++++-- .../ExecutionManagerCore.ReadyToRunJitManager.cs | 10 ---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/coreclr/vm/readytoruninfo.cpp b/src/coreclr/vm/readytoruninfo.cpp index 95762df95cd9cc..4d57671a2d9288 100644 --- a/src/coreclr/vm/readytoruninfo.cpp +++ b/src/coreclr/vm/readytoruninfo.cpp @@ -368,9 +368,14 @@ PTR_MethodDesc ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage(PCODE ent } CONTRACTL_END; -#if defined(TARGET_AMD64) || defined(TARGET_X86) +#if (defined(TARGET_AMD64) || defined(TARGET_X86)) && !defined(DACCESS_COMPILE) // A normal method entry point is always 8 byte aligned, but a funclet can start at an odd address. - // Since PtrHashMap can't handle odd pointers, check for this case and return NULL. + // The map only contains true method entry points, so a lookup for an odd (funclet) address is + // always a miss. Skip the guaranteed-miss lookup as a performance optimization. + // + // This is intentionally limited to non-DAC builds. The DAC must perform the lookup so that the + // hashmap bucket pages it touches are enumerated into triage minidumps; otherwise a consumer + // (such as the cDAC) faults when it later probes those not-in-dump pages. See dotnet/diagnostics#5910. if ((entryPoint & 0x1) != 0) return NULL; #endif diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index 9f2b700782df6c..c88ee8eab2ced1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -274,16 +274,6 @@ private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInf TargetCodePointer startAddress = imageBase + function.BeginAddress; TargetPointer entryPoint = CodePointerUtils.AddressFromCodePointer(startAddress, Target); - // Method entryPoint are 4-byte aligned on all architectures. We can exit early for misaligned addresses. - // - // This is not just an optimization for the cDAC: the createdump path takes the same - // bail when enumerating memory for a triage minidump, so the bucket page an odd-address - // probe would land in is never captured. - if ((entryPoint.Value & 0x1) != 0) - { - return TargetPointer.Null; - } - TargetPointer methodDesc = _hashMap.GetValue(r2rInfo.EntryPointToMethodDescMap, entryPoint); if (methodDesc == (ulong)HashMapLookup.SpecialKeys.InvalidEntry) return TargetPointer.Null; From 8e5b6e8bdc3a63469577c1fdf385d9bead411d5c Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Sun, 28 Jun 2026 00:39:17 -0400 Subject: [PATCH 6/6] Revert cDAC ReadyToRunJitManager to main (no change needed) The fix lives entirely in the runtime: gating the odd-entry-point optimization with !DACCESS_COMPILE makes the DAC enumerate the bucket pages into triage dumps, so the cDAC needs no change and reads them naturally. Restore the file to its main state, including the unrelated switch-expression whitespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ExecutionManagerCore.ReadyToRunJitManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index c88ee8eab2ced1..3b0a49add09c3b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -188,7 +188,7 @@ private uint GetR2RGCInfoVersion(Data.ReadyToRunInfo r2rInfo) { >= 21 => 5, >= 11 => 4, - < 11 => 3, + < 11 => 3, }; }