[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[cDAC] Fix EEClass validation corner case - #124780

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation
Feb 27, 2026
Merged

[cDAC] Fix EEClass validation corner case#124780
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:cdac-fix-eeclass-validation

Conversation

@max-charlamb

@max-charlambmax-charlamb commented Feb 24, 2026

Copy link
Copy Markdown
Member

Looked into the persistent CI failure and think I found the issue. It looks like SOS is calling GetMethodTableData on a random address that happens to pass validation because it has a pointer going back to the MethodTable. However, when we try to read the full EEClass it isn't available and we throw a different error.

This change should make sure the EEClass is validated and readable. Added unit test to verify.

CI Failure
 STDIN: 00:00.374: !runcommand !clrstack
00:00.683: OS Thread Id: 0xb08 (0)
00:00.692: Child SP IP Call Site
00:00.692: 0000002EEDD7E9E0 00007ff99863d280 [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9E0 00007ff8e620021a [InlinedCallFrame: 0000002eedd7e9e0] VarargPInvokeInteropMD.Interop.printf(System.String, ...)
00:00.697: 0000002EEDD7E9B0 00007FF8E620021A ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.745: 0000002EEDD7EAD0 00007FF8E61218B0 VarargPInvokeInteropMD.Program.Main() [/_/src/tests/SOS.UnitTests/Debuggees/VarargPInvokeInteropMD/Program.cs @ 16]
00:00.751: <END_COMMAND_OUTPUT>
00:00.751: 0:000> STDIN: 00:00.752: !runcommand !IP2MD 00007FF8E620021A
00:00.754: MethodDesc: 00007ff8e61e7b38
00:00.754: Method Name: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.754: Class: 00007ff8e61e7ac8
00:00.754: MethodTable: 00007ff8e61e7ac8
00:00.754: mdToken: 0000000006000000
00:00.754: Module: 00007ff8e61e1b00
00:00.754: IsJitted: yes
00:00.754: Current CodeAddr: 00007ff8e6200040
00:00.754: Version History:
00:00.755: ILCodeVersion: 0000000000000000
00:00.755: ReJIT ID: 0
00:00.755: IL Addr: 0000000000000000
00:00.755: CodeAddr: 00007ff8e6200040 (MinOptJitted)
00:00.755: NativeCodeVersion: 0000000000000000
00:00.757: <END_COMMAND_OUTPUT>
00:00.757: 0:000> STDIN: 00:00.757: !runcommand !clru 00007ff8e61e7b38
00:00.758: Normal JIT generated code
00:00.758: ILStubClass.IL_STUB_PInvoke(System.String, Int32, Double, ...)
00:00.758: Begin 00007FF8E6200040, size 279
00:00.759: 00007ff8`e6200040 48894c2408 mov qword ptr [rsp+8],rcx
00:00.761: 00007ff8`e6200045 4889542410 mov qword ptr [rsp+10h],rdx
00:00.762: 00007ff8`e620004a 4c89442418 mov qword ptr [rsp+18h],r8
00:00.763: 00007ff8`e620004f 4c894c2420 mov qword ptr [rsp+20h],r9
00:00.764: 00007ff8`e6200054 55 push rbp
00:00.766: 00007ff8`e6200055 4157 push r15
00:00.767: 00007ff8`e6200057 4156 push r14
00:00.768: 00007ff8`e6200059 4155 push r13
00:00.769: 00007ff8`e620005b 4154 push r12
00:00.770: 00007ff8`e620005d 57 push rdi
00:00.771: 00007ff8`e620005e 56 push rsi
00:00.773: 00007ff8`e620005f 53 push rbx
00:00.774: 00007ff8`e6200060 4881ecd8000000 sub rsp,0D8h
00:00.775: 00007ff8`e6200067 488d6c2420 lea rbp,[rsp+20h]
STDERROR: 00:00.787: Process terminated. Assertion failed.
STDERROR: 00:00.788: cDAC: 80131c49, DAC: 80070057
STDERROR: 00:00.788: at System.Diagnostics.DebugProvider.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Fail(String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, String)
STDERROR: 00:00.788: at System.Diagnostics.Debug.Assert(Boolean, Debug.AssertInterpolatedStringHandler&)
STDERROR: 00:00.788: at Microsoft.Diagnostics.DataContractReader.Legacy.SOSDacImpl.Microsoft.Diagnostics.DataContractReader.Legacy.ISOSDacInterface.GetMethodTableData(ClrDataAddress, DacpMethodTableData*)
STDERROR: 00:00.788: at <Microsoft_Diagnostics_DataContractReader_Legacy_ISOSDacInterface>F7D08DFA63EEFD39A651C932BEE9B168F60916DB84778D32AACF3004D988BD863__InterfaceImplementation.ABI_GetMethodTableData(ComWrappers.ComInterfaceDispatch*, UInt64, DacpMethodTableData*)
}

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a cDAC/legacy DAC HRESULT mismatch when SOS queries GetMethodTableData for a MethodTable whose EEClass pointer relationship superficially validates but whose EEClass memory is not actually readable (observed as a persistent CI failure). The fix makes EEClass readability part of MethodTable validation, and adds a regression test to ensure E_INVALIDARG is returned (matching legacy DAC behavior) instead of CORDBG_E_READVIRTUAL_FAILURE.

Changes:

  • Update MethodTable validation to eagerly construct/read Data.EEClass during validation so unreadable EEClass memory fails validation early.
  • Add a unit test that reproduces the “partially readable EEClass” scenario and asserts GetMethodTableData returns E_INVALIDARG.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.csMake EEClass validation eagerly read all EEClass fields so unreadable EEClass memory causes validation failure (and thus E_INVALIDARG).
src/native/managed/cdac/tests/MethodTableTests.csAdd regression test covering the unreadable/partial EEClass scenario for GetMethodTableData.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
CopilotAI review requested due to automatic review settings February 24, 2026 03:47

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
Comment threadsrc/native/managed/cdac/tests/MethodTableTests.cs Outdated
@max-charlamb
max-charlamb marked this pull request as draft February 24, 2026 03:56
@jkotas

Copy link
Copy Markdown
Member

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

@noahfalk

noahfalk commented Feb 24, 2026

Copy link
Copy Markdown
Member

get module, token and instantiation from type, and lookup the type using those

This sounds like it would be reliable at detecting if the pointer was originally allocated in the debuggee as a MethodTable. It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup. To me it sounds complimentary, but it wouldn't necessarily catch the kinds of issues Max's validation would detect.

As for feasibility, triage dumps today don't contain the EETypeHashTables and there may be other gaps. I'd guess we need to add at least 50 bytes per MethodTable to capture all the data structures the validation algo would need to touch. I wouldn't expect a ton of types in a triage dump (1 per stack frame) so maybe 10s of KB on a 2MB dump? Put a big margin of error on that until someone explores in more detail.

I think we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there. If you think its important we go farther we can, I'm just not sure it will give us much return on the dev time and extra dump memory.

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures

Maybe I'm missing some history. My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time. We check the MethodTable -> EEClass -> MethodTable loop and assume any datastructure satisfying that constraint is valid. I wasn't aware of the history of validation changes you mentioned. Any breadcrumb I should be following?

@jkotas

Copy link
Copy Markdown
Member

My understanding is that DAC's approach to MethodTable validation has been reasonably stable over a long period of time.

I have been personally fighting with it number of times. Mostly in .NET framework days where we run the SOS tests in the inner loop and the non-deterministic failures were a problem. We are not running the SOS tests in the inner loop these days. If we started running them again with high frequency, I expect we would start seeing the instability again.

It wouldn't catch memory corruption to any portion of the data structure that wasn't directly used in the lookup.

For investigation of crash dumps with corrupted data structures, this sort of validation is about as harmful as it is useful. For example, I have investigated a crash a few months ago where the EEClass pointer was corrupted: #119761 (comment) . This validation was not helping with the investigation.

triage dumps

Do we really need this sort of validation for triage dumps? Can the workflows for investigating triage dumps avoid throwing random pointers against DAC APIs and hoping it to return semi-accurate answer? Most SOS commands do not work well in triage dumps. I do not think we would lose much if we stopped doing this validation in triage dumps.

we'd get a good return on doing a little more validation of the immediate MethodTable/EEClass fields and stopping there.

I expect we will want to investigate creating EEClass/MethodDesc/FieldDesc lazily at some point to further improve startup performance by making CoreCLR w/ R2R characteristics more similar to NativeAOT. Doubling down on using EEClass/MethodDesc/FieldDesc for validation of random pointers would go against that.

I do not expect that this will be solved in this PR. I wanted to mention this since I do not think the current "design" of these validations is good. Maybe create an issue about this?

@max-charlamb

Copy link
Copy Markdown
MemberAuthor

SOS is calling GetMethodTableData on a random address that happens to pass validation

This is like 123th time we are trying to patch some hole in this validation to fix intermittent failures. The current scheme is going to produce false positives by design.

I am wondering whether we can do better and implement 100% reliable validation: get module, token and instantiation from type, and lookup the type using those. If we get back the type we started with, it is a valid type. If not, it is a random pointer that looks like valid type.

I'm not trying to modify the DAC MethodTable validation, I'm attempting to make the cDAC follow the same scheme to prevent failures in the runtime-diagnostic pipeline.

This error occurs because the cDAC validation logic does not check that the entire method table is readable until after validation occurs. This results in a virtual read exception rather than an argument exception.

@jkotas

Copy link
Copy Markdown
Member

Right, I understand you are trying to reimplement the quirks of the legacy DAC in this PR. My point was that I do not think it is the best forward-looking approach.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 646f1e8 to 7bf5e94CompareFebruary 24, 2026 16:44
@max-charlamb
max-charlamb marked this pull request as ready for review February 24, 2026 17:30
CopilotAI review requested due to automatic review settings February 24, 2026 17:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@noahfalk

Copy link
Copy Markdown
Member

@jkotas - thanks for all the extra info. I read your concerns as being at least as much about having more control over where validation occurs in the workflow and what the UX experience of the validation is. Thus far SOS's approach I'd say is ad-hoc and leans towards eager validation + errors rather than lazy validation + non-blocking warnings. I can see advantages for both in different circumstances but I'm certainly open to changing defaults or giving more control that could be used by sophisticated devs to get the behavior they want. I opened: #124829

In terms of triage dumps, we could certainly skip doing the validation you proposed if the various type hashtables are missing. I don't believe we have any direct info about whether a dump is or isn't a triage dump but we can make decisions based on what memory blocks we find. Depending on the scenario SOS may or may not be in control of what pointers are being analyzed as MethodTables.

@max-charlamb
max-charlambforce-pushed the cdac-fix-eeclass-validation branch from 7bf5e94 to f43b229CompareFebruary 26, 2026 16:52

@noahfalknoahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@max-charlamb
max-charlamb merged commit c69c476 into dotnet:mainFeb 27, 2026
48 of 52 checks passed
@max-charlamb
max-charlamb deleted the cdac-fix-eeclass-validation branch February 27, 2026 15:05
max-charlamb added a commit that referenced this pull request Mar 6, 2026
## Summary
Add IsContinuation to the cDAC RuntimeTypeSystem contract, enabling the
cDAC to identify and validate continuation MethodTables created by the
async continuation feature.
Continuations are dynamically-created MethodTables (similar to arrays)
whose parent is the base `Continuation` class stored in
`g_pContinuationClassIfSubTypeCreated`. Without this change, the cDAC's
MT→EEClass→MT validation roundtrip would reject valid continuation MTs.
Related discussion:
#124780 (comment)
## Changes
- **`datadescriptor.inc`** — Expose
`g_pContinuationClassIfSubTypeCreated` as `ContinuationMethodTable`
global pointer
- **`IRuntimeTypeSystem.cs`** — Add `IsContinuation(TypeHandle)` to the
contract interface
- **`RuntimeTypeSystem_1.cs`** — Implement `IsContinuation` by checking
`ParentMethodTable == continuationMethodTablePointer`
- **`RuntimeTypeSystemFactory.cs`** — Read the continuation MT global
(gracefully handles missing global via `TryReadGlobalPointer`)
- **`TypeValidation.cs`** — Fix MT→EEClass→MT validation to allow
continuations (like arrays/generics)
- **`Constants.cs`** — Add `ContinuationMethodTable` constant name
- **Tests** — 4 test methods (8 cases across architectures): true
positive, true negative, null global, and CanonMT validation
---------
Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com>
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@max-charlamb@jkotas@noahfalk@hoyosjs