From 3792f6bc2becb96d86ea601177d017e10f2061df Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 26 Feb 2026 14:06:55 -0500 Subject: [PATCH 1/3] add support for IsContinuation --- .../design/datacontracts/RuntimeTypeSystem.md | 4 + .../vm/datadescriptor/datadescriptor.inc | 1 + .../Contracts/IRuntimeTypeSystem.cs | 2 + .../Constants.cs | 1 + .../Contracts/RuntimeTypeSystemFactory.cs | 7 +- .../Contracts/RuntimeTypeSystem_1.cs | 8 +- .../TypeValidation.cs | 17 ++- .../managed/cdac/tests/MethodTableTests.cs | 138 ++++++++++++++++++ .../MockDescriptors.RuntimeTypeSystem.cs | 22 +++ 9 files changed, 196 insertions(+), 4 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 928e646f6500d1..742b5996786351 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -54,6 +54,8 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool IsString(TypeHandle typeHandle); // True if the MethodTable represents a type that contains managed references public virtual bool ContainsGCPointers(TypeHandle typeHandle); + // True if the MethodTable represents a continuation type used by the async continuation feature + public virtual bool IsContinuation(TypeHandle typeHandle); public virtual bool IsDynamicStatics(TypeHandle typeHandle); public virtual ushort GetNumInterfaces(TypeHandle typeHandle); @@ -370,6 +372,7 @@ The contract depends on the following globals | Global name | Meaning | | --- | --- | +| `ContinuationMethodTable` | A pointer to the address of the base `Continuation` `MethodTable`, or null if no continuations have been created | `FreeObjectMethodTablePointer` | A pointer to the address of a `MethodTable` used by the GC to indicate reclaimed memory | `StaticsPointerMask` | For masking out a bit of DynamicStaticsInfo pointer fields | `ArrayBaseSize` | The base size of an array object; used to compute multidimensional array rank from `MethodTable::BaseSize` @@ -429,6 +432,7 @@ Contracts used: private readonly Dictionary _methodTables; internal TargetPointer FreeObjectMethodTablePointer {get; } + internal TargetPointer ContinuationMethodTablePointer {get; } public TypeHandle GetTypeHandle(TargetPointer typeHandlePointer) { diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 696620a4d54984..e0568d09894673 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1205,6 +1205,7 @@ CDAC_GLOBAL(MaxClrNotificationArgs, uint32, MAX_CLR_NOTIFICATION_ARGS) CDAC_GLOBAL(FieldOffsetBigRVA, uint32, FIELD_OFFSET_BIG_RVA) CDAC_GLOBAL_POINTER(ClrNotificationArguments, &::g_clrNotificationArguments) CDAC_GLOBAL_POINTER(ArrayBoundsZero, cdac_data::ArrayBoundsZero) +CDAC_GLOBAL_POINTER(ContinuationMethodTable, &::g_pContinuationClassIfSubTypeCreated) CDAC_GLOBAL_POINTER(ExceptionMethodTable, &::g_pExceptionClass) CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &::g_pFreeObjectMethodTable) CDAC_GLOBAL_POINTER(ObjectMethodTable, &::g_pObjectClass) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index f318e70e528f04..4e575a3b1c425f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -105,6 +105,8 @@ public interface IRuntimeTypeSystem : IContract bool IsString(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable represents a type that contains managed references bool ContainsGCPointers(TypeHandle typeHandle) => throw new NotImplementedException(); + // True if the MethodTable represents a continuation type used by the async continuation feature + bool IsContinuation(TypeHandle typeHandle) => throw new NotImplementedException(); bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException(); ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 93af46d5981bfc..48bdb6f590c813 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -24,6 +24,7 @@ public static class Globals public const string ObjectToMethodTableUnmask = nameof(ObjectToMethodTableUnmask); public const string SOSBreakingChangeVersion = nameof(SOSBreakingChangeVersion); + public const string ContinuationMethodTable = nameof(ContinuationMethodTable); public const string ExceptionMethodTable = nameof(ExceptionMethodTable); public const string FreeObjectMethodTable = nameof(FreeObjectMethodTable); public const string ObjectMethodTable = nameof(ObjectMethodTable); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs index 510eb605555bb4..cc9b3d8da0fdcf 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs @@ -11,10 +11,15 @@ IRuntimeTypeSystem IContractFactory.CreateContract(Target ta { TargetPointer targetPointer = target.ReadGlobalPointer(Constants.Globals.FreeObjectMethodTable); TargetPointer freeObjectMethodTable = target.ReadPointer(targetPointer); + TargetPointer continuationMethodTable = TargetPointer.Null; + if (target.TryReadGlobalPointer(Constants.Globals.ContinuationMethodTable, out TargetPointer? continuationMethodTableGlobal)) + { + continuationMethodTable = target.ReadPointer(continuationMethodTableGlobal.Value); + } ulong methodDescAlignment = target.ReadGlobal(Constants.Globals.MethodDescAlignment); return version switch { - 1 => new RuntimeTypeSystem_1(target, new RuntimeTypeSystemHelpers.TypeValidation(target), new RuntimeTypeSystemHelpers.MethodValidation(target, methodDescAlignment), freeObjectMethodTable, methodDescAlignment), + 1 => new RuntimeTypeSystem_1(target, new RuntimeTypeSystemHelpers.TypeValidation(target, continuationMethodTable), new RuntimeTypeSystemHelpers.MethodValidation(target, methodDescAlignment), freeObjectMethodTable, continuationMethodTable, methodDescAlignment), _ => default(RuntimeTypeSystem), }; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index df23454a53528a..160fa9c19024d7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -18,6 +18,7 @@ internal partial struct RuntimeTypeSystem_1 : IRuntimeTypeSystem private const int TYPE_MASK_OFFSET = 27; // offset of type in field desc flags2 private readonly Target _target; private readonly TargetPointer _freeObjectMethodTablePointer; + private readonly TargetPointer _continuationMethodTablePointer; private readonly ulong _methodDescAlignment; private readonly TypeValidation _typeValidation; private readonly MethodValidation _methodValidation; @@ -396,10 +397,11 @@ private StoredSigMethodDesc(Target target, TargetPointer methodDescPointer) } } - internal RuntimeTypeSystem_1(Target target, TypeValidation typeValidation, MethodValidation methodValidation, TargetPointer freeObjectMethodTablePointer, ulong methodDescAlignment) + internal RuntimeTypeSystem_1(Target target, TypeValidation typeValidation, MethodValidation methodValidation, TargetPointer freeObjectMethodTablePointer, TargetPointer continuationMethodTablePointer, ulong methodDescAlignment) { _target = target; _freeObjectMethodTablePointer = freeObjectMethodTablePointer; + _continuationMethodTablePointer = continuationMethodTablePointer; _methodDescAlignment = methodDescAlignment; _typeValidation = typeValidation; _methodValidation = methodValidation; @@ -407,6 +409,7 @@ internal RuntimeTypeSystem_1(Target target, TypeValidation typeValidation, Metho } internal TargetPointer FreeObjectMethodTablePointer => _freeObjectMethodTablePointer; + internal TargetPointer ContinuationMethodTablePointer => _continuationMethodTablePointer; internal ulong MethodDescAlignment => _methodDescAlignment; @@ -526,6 +529,9 @@ private Data.EEClass GetClassData(TypeHandle typeHandle) public bool IsString(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsString; public bool ContainsGCPointers(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; + public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() + && _continuationMethodTablePointer != TargetPointer.Null + && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; public bool IsDynamicStatics(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? (ushort)0 : _methodTables[typeHandle.Address].NumInterfaces; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs index 8c53ac0bfe07d8..7a7dc1ab50d89c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs @@ -9,10 +9,12 @@ namespace Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers; internal sealed class TypeValidation { private readonly Target _target; + private readonly TargetPointer _continuationMethodTablePointer; - internal TypeValidation(Target target) + internal TypeValidation(Target target, TargetPointer continuationMethodTablePointer) { _target = target; + _continuationMethodTablePointer = continuationMethodTablePointer; } // This doesn't need as many properties as MethodTable because we don't want to be operating on @@ -69,6 +71,7 @@ internal TargetPointer CanonMT } internal readonly bool ValidateReadable() => ValidateDataReadable(_target, Address); + internal TargetPointer ParentMethodTable => _target.ReadPointer(Address + (ulong)_type.Fields[nameof(ParentMethodTable)].Offset); } internal struct NonValidatedEEClass @@ -164,7 +167,7 @@ private bool ValidateThrowing(NonValidatedMethodTable methodTable) { return true; } - if (methodTable.Flags.HasInstantiation || methodTable.Flags.IsArray) + if (methodTable.Flags.HasInstantiation || methodTable.Flags.IsArray || IsContinuation(methodTable)) { NonValidatedMethodTable methodTableFromClass = GetMethodTableData(_target, methodTablePtrFromClass); if (!methodTableFromClass.ValidateReadable()) @@ -224,6 +227,16 @@ private TargetPointer GetClassThrowing(NonValidatedMethodTable methodTable) } } + // NOTE: The continuation check is duplicated here and in RuntimeTypeSystem_1.IsContinuation. + // TypeValidation runs before the MethodTable is added to the RuntimeTypeSystem's cache, so we + // cannot call into RuntimeTypeSystem_1 — the type handle does not exist yet. Instead we + // duplicate the check using the raw ParentMethodTable read from target memory. + private bool IsContinuation(NonValidatedMethodTable methodTable) + { + return _continuationMethodTablePointer != TargetPointer.Null + && methodTable.ParentMethodTable == _continuationMethodTablePointer; + } + internal bool TryValidateMethodTablePointer(TargetPointer methodTablePointer) { NonValidatedMethodTable nonvalidatedMethodTable = GetMethodTableData(_target, methodTablePointer); diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 7966d2470fccd7..2dcb7e843c5421 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -278,6 +278,46 @@ public unsafe void GetMethodTableDataReturnsEInvalidArgWhenEEClassPartiallyReada }); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsContinuationReturnsTrueForContinuationType(MockTarget.Architecture arch) + { + TargetPointer continuationInstanceMethodTablePtr = default; + RTSContractHelper(arch, + (rtsBuilder) => + { + TargetTestHelpers targetTestHelpers = rtsBuilder.Builder.TargetTestHelpers; + TargetPointer systemObjectMethodTablePtr = AddSystemObjectMethodTable(rtsBuilder).MethodTable; + + // Create the base Continuation class (parent is System.Object) + TargetPointer continuationBaseEEClassPtr = rtsBuilder.AddEEClass("Continuation", attr: 0, numMethods: 0, numNonVirtualSlots: 0); + TargetPointer continuationBaseMethodTablePtr = rtsBuilder.AddMethodTable("Continuation", + mtflags: default, mtflags2: default, baseSize: targetTestHelpers.ObjectBaseSize, + module: TargetPointer.Null, parentMethodTable: systemObjectMethodTablePtr, numInterfaces: 0, numVirtuals: 3); + rtsBuilder.SetEEClassAndCanonMTRefs(continuationBaseEEClassPtr, continuationBaseMethodTablePtr); + + // Set the global to point to the base continuation MT + rtsBuilder.SetContinuationMethodTable(continuationBaseMethodTablePtr); + + // Create a derived continuation instance MT (shares EEClass with the base, parent is the base continuation MT) + TargetPointer continuationInstanceEEClassPtr = rtsBuilder.AddEEClass("ContinuationInstance", attr: 0, numMethods: 0, numNonVirtualSlots: 0); + continuationInstanceMethodTablePtr = rtsBuilder.AddMethodTable("ContinuationInstance", + mtflags: default, mtflags2: default, baseSize: targetTestHelpers.ObjectBaseSize, + module: TargetPointer.Null, parentMethodTable: continuationBaseMethodTablePtr, numInterfaces: 0, numVirtuals: 3); + // Continuation instances share the EEClass with the base, similar to arrays + rtsBuilder.SetEEClassAndCanonMTRefs(continuationInstanceEEClassPtr, continuationInstanceMethodTablePtr); + }, + (target) => + { + Contracts.IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + Assert.NotNull(rts); + Contracts.TypeHandle continuationTypeHandle = rts.GetTypeHandle(continuationInstanceMethodTablePtr); + Assert.True(rts.IsContinuation(continuationTypeHandle)); + Assert.False(rts.IsFreeObjectMethodTable(continuationTypeHandle)); + Assert.False(rts.IsString(continuationTypeHandle)); + }); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public unsafe void GetMethodTableDataReturnsEInvalidArgWhenMethodTablePartiallyReadable(MockTarget.Architecture arch) @@ -384,4 +424,102 @@ public void ValidateMultidimArrayRank(MockTarget.Architecture arch) Assert.Equal(1u, rank1); }); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsContinuationReturnsFalseForRegularType(MockTarget.Architecture arch) + { + TargetPointer systemObjectMethodTablePtr = default; + RTSContractHelper(arch, + (rtsBuilder) => + { + systemObjectMethodTablePtr = AddSystemObjectMethodTable(rtsBuilder).MethodTable; + }, + (target) => + { + Contracts.IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + Assert.NotNull(rts); + Contracts.TypeHandle objectTypeHandle = rts.GetTypeHandle(systemObjectMethodTablePtr); + Assert.False(rts.IsContinuation(objectTypeHandle)); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsContinuationReturnsFalseWhenGlobalIsNull(MockTarget.Architecture arch) + { + TargetPointer systemObjectMethodTablePtr = default; + TargetPointer childMethodTablePtr = default; + RTSContractHelper(arch, + (rtsBuilder) => + { + TargetTestHelpers targetTestHelpers = rtsBuilder.Builder.TargetTestHelpers; + systemObjectMethodTablePtr = AddSystemObjectMethodTable(rtsBuilder).MethodTable; + + // Don't set the continuation global (it remains null) + // Create a child type with System.Object as parent + TargetPointer childEEClassPtr = rtsBuilder.AddEEClass("ChildType", attr: 0, numMethods: 0, numNonVirtualSlots: 0); + childMethodTablePtr = rtsBuilder.AddMethodTable("ChildType", + mtflags: default, mtflags2: default, baseSize: targetTestHelpers.ObjectBaseSize, + module: TargetPointer.Null, parentMethodTable: systemObjectMethodTablePtr, numInterfaces: 0, numVirtuals: 3); + rtsBuilder.SetEEClassAndCanonMTRefs(childEEClassPtr, childMethodTablePtr); + }, + (target) => + { + Contracts.IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + Assert.NotNull(rts); + + // System.Object has ParentMethodTable == Null, same as the null continuation global. + // Verify the null guard prevents a false positive match. + Contracts.TypeHandle objectTypeHandle = rts.GetTypeHandle(systemObjectMethodTablePtr); + Assert.False(rts.IsContinuation(objectTypeHandle)); + + Contracts.TypeHandle childTypeHandle = rts.GetTypeHandle(childMethodTablePtr); + Assert.False(rts.IsContinuation(childTypeHandle)); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ValidateContinuationMethodTablePointer(MockTarget.Architecture arch) + { + TargetPointer continuationInstanceMethodTablePtr = default; + RTSContractHelper(arch, + (rtsBuilder) => + { + TargetTestHelpers targetTestHelpers = rtsBuilder.Builder.TargetTestHelpers; + TargetPointer systemObjectMethodTablePtr = AddSystemObjectMethodTable(rtsBuilder).MethodTable; + + // Create the base Continuation class + TargetPointer continuationBaseEEClassPtr = rtsBuilder.AddEEClass("Continuation", attr: 0, numMethods: 0, numNonVirtualSlots: 0); + TargetPointer continuationBaseMethodTablePtr = rtsBuilder.AddMethodTable("Continuation", + mtflags: default, mtflags2: default, baseSize: targetTestHelpers.ObjectBaseSize, + module: TargetPointer.Null, parentMethodTable: systemObjectMethodTablePtr, numInterfaces: 0, numVirtuals: 3); + rtsBuilder.SetEEClassAndCanonMTRefs(continuationBaseEEClassPtr, continuationBaseMethodTablePtr); + rtsBuilder.SetContinuationMethodTable(continuationBaseMethodTablePtr); + + // Create a derived continuation instance + // Continuation instances share the EEClass with the singleton sub-continuation class + TargetPointer sharedEEClassPtr = rtsBuilder.AddEEClass("SubContinuation", attr: 0, numMethods: 0, numNonVirtualSlots: 0); + TargetPointer sharedCanonMTPtr = rtsBuilder.AddMethodTable("SubContinuationCanon", + mtflags: default, mtflags2: default, baseSize: targetTestHelpers.ObjectBaseSize, + module: TargetPointer.Null, parentMethodTable: continuationBaseMethodTablePtr, numInterfaces: 0, numVirtuals: 3); + rtsBuilder.SetEEClassAndCanonMTRefs(sharedEEClassPtr, sharedCanonMTPtr); + + // The actual continuation instance MT points to the shared EEClass via CanonMT + continuationInstanceMethodTablePtr = rtsBuilder.AddMethodTable("ContinuationInstance", + mtflags: default, mtflags2: default, baseSize: targetTestHelpers.ObjectBaseSize, + module: TargetPointer.Null, parentMethodTable: continuationBaseMethodTablePtr, numInterfaces: 0, numVirtuals: 3); + rtsBuilder.SetMethodTableCanonMT(continuationInstanceMethodTablePtr, sharedCanonMTPtr); + }, + (target) => + { + Contracts.IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + Assert.NotNull(rts); + // Validation should succeed - the MT→CanonMT→EEClass→MT roundtrip is handled for continuations + Contracts.TypeHandle continuationTypeHandle = rts.GetTypeHandle(continuationInstanceMethodTablePtr); + Assert.Equal(continuationInstanceMethodTablePtr.Value, continuationTypeHandle.Address.Value); + Assert.True(rts.IsContinuation(continuationTypeHandle)); + }); + } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index 5d827c7f68c4f4..2b9de8e1b31782 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -12,6 +12,7 @@ internal partial class MockDescriptors public class RuntimeTypeSystem { internal const ulong TestFreeObjectMethodTableGlobalAddress = 0x00000000_7a0000a0; + internal const ulong TestContinuationMethodTableGlobalAddress = 0x00000000_7a0000b0; private const ulong DefaultAllocationRangeStart = 0x00000000_4a000000; private const ulong DefaultAllocationRangeEnd = 0x00000000_4b000000; @@ -85,6 +86,7 @@ public class RuntimeTypeSystem internal MockMemorySpace.BumpAllocator TypeSystemAllocator { get; } internal TargetPointer FreeObjectMethodTableAddress { get; private set; } + internal TargetPointer ContinuationMethodTableAddress { get; private set; } public RuntimeTypeSystem(MockMemorySpace.Builder builder) : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd)) @@ -101,6 +103,7 @@ public RuntimeTypeSystem(MockMemorySpace.Builder builder, (ulong Start, ulong En Globals = [ (nameof(Constants.Globals.FreeObjectMethodTable), TestFreeObjectMethodTableGlobalAddress), + (nameof(Constants.Globals.ContinuationMethodTable), TestContinuationMethodTableGlobalAddress), (nameof(Constants.Globals.MethodDescAlignment), GetMethodDescAlignment(Builder.TargetTestHelpers)), (nameof(Constants.Globals.ArrayBaseSize), Builder.TargetTestHelpers.ArrayBaseBaseSize), ]; @@ -109,6 +112,7 @@ public RuntimeTypeSystem(MockMemorySpace.Builder builder, (ulong Start, ulong En private void AddGlobalPointers() { AddFreeObjectMethodTable(); + AddContinuationMethodTableGlobal(); } private void AddFreeObjectMethodTable() @@ -124,6 +128,24 @@ private void AddFreeObjectMethodTable() Builder.AddHeapFragment(globalAddr); } + private void AddContinuationMethodTableGlobal() + { + // Initially the continuation method table global points to null (no continuations created yet) + TargetTestHelpers targetTestHelpers = Builder.TargetTestHelpers; + MockMemorySpace.HeapFragment globalAddr = new() { Name = "Address of Continuation Method Table", Address = TestContinuationMethodTableGlobalAddress, Data = new byte[targetTestHelpers.PointerSize] }; + targetTestHelpers.WritePointer(globalAddr.Data, TargetPointer.Null); + Builder.AddHeapFragment(globalAddr); + ContinuationMethodTableAddress = TargetPointer.Null; + } + + internal void SetContinuationMethodTable(TargetPointer continuationMethodTable) + { + TargetTestHelpers targetTestHelpers = Builder.TargetTestHelpers; + Span globalAddrBytes = Builder.BorrowAddressRange(TestContinuationMethodTableGlobalAddress, targetTestHelpers.PointerSize); + targetTestHelpers.WritePointer(globalAddrBytes, continuationMethodTable); + ContinuationMethodTableAddress = continuationMethodTable; + } + // set the eeClass MethodTable pointer to the canonMT and the canonMT's EEClass pointer to the eeClass internal void SetEEClassAndCanonMTRefs(TargetPointer eeClass, TargetPointer canonMT) { From 37222f213a4c407f7e4037356d55e1248b2822ea Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 26 Feb 2026 14:41:21 -0500 Subject: [PATCH 2/3] don't require backcompat --- .../Contracts/RuntimeTypeSystemFactory.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs index cc9b3d8da0fdcf..9c62c256421fd8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystemFactory.cs @@ -11,11 +11,8 @@ IRuntimeTypeSystem IContractFactory.CreateContract(Target ta { TargetPointer targetPointer = target.ReadGlobalPointer(Constants.Globals.FreeObjectMethodTable); TargetPointer freeObjectMethodTable = target.ReadPointer(targetPointer); - TargetPointer continuationMethodTable = TargetPointer.Null; - if (target.TryReadGlobalPointer(Constants.Globals.ContinuationMethodTable, out TargetPointer? continuationMethodTableGlobal)) - { - continuationMethodTable = target.ReadPointer(continuationMethodTableGlobal.Value); - } + TargetPointer continuationMethodTablePointer = target.ReadGlobalPointer(Constants.Globals.ContinuationMethodTable); + TargetPointer continuationMethodTable = target.ReadPointer(continuationMethodTablePointer); ulong methodDescAlignment = target.ReadGlobal(Constants.Globals.MethodDescAlignment); return version switch { From 93df3d504a320c5e5bb1ba4a3e9740c7552da31e Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 26 Feb 2026 18:12:58 -0500 Subject: [PATCH 3/3] add dumptest for IsContinuation --- .../DumpTests/AsyncContinuationDumpTests.cs | 168 ++++++++++++++++++ .../AsyncContinuation.csproj | 10 ++ .../Debuggees/AsyncContinuation/Program.cs | 39 ++++ 3 files changed, 217 insertions(+) create mode 100644 src/native/managed/cdac/tests/DumpTests/AsyncContinuationDumpTests.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/AsyncContinuation.csproj create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/Program.cs diff --git a/src/native/managed/cdac/tests/DumpTests/AsyncContinuationDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/AsyncContinuationDumpTests.cs new file mode 100644 index 00000000000000..29a9560735ab46 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/AsyncContinuationDumpTests.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for async continuation support in the RuntimeTypeSystem contract. +/// Uses the AsyncContinuation debuggee dump, which runs an async2 method to trigger +/// continuation MethodTable creation. +/// +public class AsyncContinuationDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "AsyncContinuation"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Continuation support is not available in .NET 10")] + public void ContinuationMethodTable_IsNonNull(TestConfiguration config) + { + InitializeDumpTest(config); + + TargetPointer continuationMTGlobal = Target.ReadGlobalPointer("ContinuationMethodTable"); + TargetPointer continuationMT = Target.ReadPointer(continuationMTGlobal); + Assert.NotEqual(TargetPointer.Null, continuationMT); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Continuation support is not available in .NET 10")] + public void ContinuationBaseClass_IsNotContinuation(TestConfiguration config) + { + InitializeDumpTest(config); + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + // The ContinuationMethodTable global points to the Continuation base class itself. + // IsContinuation checks if a type's parent is the Continuation base class, + // so the base class itself is NOT considered a continuation (its parent is Object). + TargetPointer continuationMTGlobal = Target.ReadGlobalPointer("ContinuationMethodTable"); + TargetPointer continuationMT = Target.ReadPointer(continuationMTGlobal); + Assert.NotEqual(TargetPointer.Null, continuationMT); + + TypeHandle handle = rts.GetTypeHandle(continuationMT); + Assert.False(rts.IsContinuation(handle)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Continuation support is not available in .NET 10")] + public void ObjectMethodTable_IsNotContinuation(TestConfiguration config) + { + InitializeDumpTest(config); + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + + TargetPointer objectMTGlobal = Target.ReadGlobalPointer("ObjectMethodTable"); + TargetPointer objectMT = Target.ReadPointer(objectMTGlobal); + TypeHandle objectHandle = rts.GetTypeHandle(objectMT); + Assert.False(rts.IsContinuation(objectHandle)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Continuation support is not available in .NET 10")] + public void ThreadLocalContinuation_IsContinuation(TestConfiguration config) + { + InitializeDumpTest(config); + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + ILoader loader = Target.Contracts.Loader; + IThread threadContract = Target.Contracts.Thread; + IEcmaMetadata ecmaMetadata = Target.Contracts.EcmaMetadata; + + // 1. Locate the AsyncDispatcherInfo type in System.Private.CoreLib. + TargetPointer systemAssembly = loader.GetSystemAssembly(); + ModuleHandle coreLibModule = loader.GetModuleHandleFromAssemblyPtr(systemAssembly); + TypeHandle asyncDispatcherInfoHandle = rts.GetTypeByNameAndModule( + "AsyncDispatcherInfo", + "System.Runtime.CompilerServices", + coreLibModule); + Assert.True(asyncDispatcherInfoHandle.Address != 0, + "Could not find AsyncDispatcherInfo type in CoreLib"); + + // 2. Find the t_current field's offset within the non-GC thread statics block. + // Walk the FieldDescList to find the ThreadStatic field named "t_current". + System.Reflection.Metadata.MetadataReader? md = ecmaMetadata.GetMetadata(coreLibModule); + Assert.NotNull(md); + + TargetPointer fieldDescList = rts.GetFieldDescList(asyncDispatcherInfoHandle); + ushort numStaticFields = rts.GetNumStaticFields(asyncDispatcherInfoHandle); + ushort numThreadStaticFields = rts.GetNumThreadStaticFields(asyncDispatcherInfoHandle); + ushort numInstanceFields = rts.GetNumInstanceFields(asyncDispatcherInfoHandle); + + // FieldDescList has instance fields first, then static fields. + // Thread-static fields are among the static fields. + uint tCurrentOffset = 0; + bool foundField = false; + int totalFields = numInstanceFields + numStaticFields; + uint fieldDescSize = Target.GetTypeInfo(DataType.FieldDesc).Size!.Value; + + for (int i = numInstanceFields; i < totalFields; i++) + { + TargetPointer fieldDesc = fieldDescList + (ulong)(i * (int)fieldDescSize); + if (!rts.IsFieldDescThreadStatic(fieldDesc)) + continue; + + uint memberDef = rts.GetFieldDescMemberDef(fieldDesc); + var fieldDefHandle = (FieldDefinitionHandle)MetadataTokens.Handle((int)memberDef); + var fieldDef = md.GetFieldDefinition(fieldDefHandle); + string fieldName = md.GetString(fieldDef.Name); + + if (fieldName == "t_current") + { + tCurrentOffset = rts.GetFieldDescOffset(fieldDesc, fieldDef); + foundField = true; + break; + } + } + + Assert.True(foundField, $"Could not find t_current field. numStatic={numStaticFields} numThreadStatic={numThreadStaticFields} numInstance={numInstanceFields}"); + + // 3. Walk all threads and read t_current at the discovered offset. + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadPtr = threadStore.FirstThread; + + ulong continuationAddress = 0; + while (threadPtr != TargetPointer.Null) + { + ThreadData threadData = threadContract.GetThreadData(threadPtr); + + TargetPointer nonGCBase = rts.GetNonGCThreadStaticsBasePointer( + asyncDispatcherInfoHandle, threadPtr); + + if (nonGCBase != TargetPointer.Null) + { + TargetPointer tCurrent = Target.ReadPointer(nonGCBase + tCurrentOffset); + + if (tCurrent != TargetPointer.Null) + { + // AsyncDispatcherInfo layout: + // offset 0: AsyncDispatcherInfo* Next + // offset PointerSize: Continuation? NextContinuation (object reference) + TargetPointer nextContinuation = Target.ReadPointer( + tCurrent.Value + (ulong)Target.PointerSize); + + if (nextContinuation != TargetPointer.Null) + { + continuationAddress = nextContinuation.Value; + break; + } + } + } + + threadPtr = threadData.NextThread; + } + + Assert.NotEqual(0UL, continuationAddress); + + // 4. Verify the object's MethodTable is a continuation subtype via the cDAC. + TargetPointer objMT = Target.Contracts.Object.GetMethodTableAddress( + new TargetPointer(continuationAddress)); + TypeHandle handle = rts.GetTypeHandle(objMT); + Assert.True(rts.IsContinuation(handle)); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/AsyncContinuation.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/AsyncContinuation.csproj new file mode 100644 index 00000000000000..f2c96fe50c786d --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/AsyncContinuation.csproj @@ -0,0 +1,10 @@ + + + + $(Features);runtime-async=on + true + $(NoWarn);CA2007;CA2252 + + Full + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/Program.cs new file mode 100644 index 00000000000000..c9e22fe6b76512 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/Program.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; + +/// +/// Debuggee for cDAC dump test — exercises async continuations. +/// Uses the runtime-async=on compiler feature to generate async2 methods, +/// which cause the JIT/runtime to create continuation MethodTables +/// (setting g_pContinuationClassIfSubTypeCreated). +/// +/// FailFast is called from within the resumed inner async method +/// while DispatchContinuations is still on the call stack, ensuring +/// AsyncDispatcherInfo.t_current points to a live continuation chain. +/// +internal static class Program +{ + internal static async Task InnerAsync(int value) + { + await Task.Delay(1); + + // Crash while still inside Resume — t_current is set on this thread + // and NextContinuation points to OuterAsync's continuation. + Environment.FailFast("cDAC dump test: AsyncContinuation debuggee intentional crash"); + + return value + 1; + } + + internal static async Task OuterAsync(int value) + { + return await InnerAsync(value); + } + + private static void Main() + { + OuterAsync(41).GetAwaiter().GetResult(); + } +}