Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -429,6 +432,7 @@ Contracts used:
private readonly Dictionary<TargetPointer, MethodTable_1> _methodTables;

internal TargetPointer FreeObjectMethodTablePointer {get; }
internal TargetPointer ContinuationMethodTablePointer {get; }

public TypeHandle GetTypeHandle(TargetPointer typeHandlePointer)
{
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArrayBase>::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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ IRuntimeTypeSystem IContractFactory<IRuntimeTypeSystem>.CreateContract(Target ta
{
TargetPointer targetPointer = target.ReadGlobalPointer(Constants.Globals.FreeObjectMethodTable);
TargetPointer freeObjectMethodTable = target.ReadPointer(targetPointer);
TargetPointer continuationMethodTablePointer = target.ReadGlobalPointer(Constants.Globals.ContinuationMethodTable);
TargetPointer continuationMethodTable = target.ReadPointer(continuationMethodTablePointer);
ulong methodDescAlignment = target.ReadGlobal<ulong>(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),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -396,17 +397,19 @@ 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;
_methodValidation.SetMethodTableQueries(new NonValidatedMethodTableQueries(this));
}

internal TargetPointer FreeObjectMethodTablePointer => _freeObjectMethodTablePointer;
internal TargetPointer ContinuationMethodTablePointer => _continuationMethodTablePointer;

internal ulong MethodDescAlignment => _methodDescAlignment;

Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,6 +71,7 @@ internal TargetPointer CanonMT
}

internal readonly bool ValidateReadable() => ValidateDataReadable<MethodTable>(_target, Address);
internal TargetPointer ParentMethodTable => _target.ReadPointer(Address + (ulong)_type.Fields[nameof(ParentMethodTable)].Offset);
}

internal struct NonValidatedEEClass
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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);
Expand Down
168 changes: 168 additions & 0 deletions src/native/managed/cdac/tests/DumpTests/AsyncContinuationDumpTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Enable Roslyn async2 codegen so the JIT creates continuation MethodTables -->
<Features>$(Features);runtime-async=on</Features>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<NoWarn>$(NoWarn);CA2007;CA2252</NoWarn>
<!-- Full dump needed so that module metadata is available for type lookup -->
<DumpTypes>Full</DumpTypes>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
internal static class Program
{
internal static async Task<int> 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<int> OuterAsync(int value)
{
return await InnerAsync(value);
}

private static void Main()
{
OuterAsync(41).GetAwaiter().GetResult();
}
}
Loading
Loading