Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9e06fe4
add cdac dump-test infrastructure
max-charlamb Feb 18, 2026
20a18ae
centralize debuggee props
max-charlamb Feb 18, 2026
03d470d
add CI pipeline
max-charlamb Feb 18, 2026
d06c90f
address comments
max-charlamb Feb 18, 2026
7bf223f
move pipeline inside of runtime-diagnostics
max-charlamb Feb 18, 2026
8295470
comments
max-charlamb Feb 18, 2026
eb8ad7d
update yaml for cross-platform dump testing
max-charlamb Feb 18, 2026
f19b808
add more tests
max-charlamb Feb 18, 2026
5c06718
refactor skip logic
max-charlamb Feb 18, 2026
0727d57
refactor more
max-charlamb Feb 18, 2026
da8011b
update
max-charlamb Feb 18, 2026
7686c17
refactor
max-charlamb Feb 18, 2026
b4ad6ee
run stages in parallel
max-charlamb Feb 18, 2026
d02012e
Fix cross-platform cDAC dump test artifact download
steveisok Feb 19, 2026
b5ba445
Standardize cDAC dump artifacts on tar.gz format
steveisok Feb 19, 2026
3dc06d1
publish test results seperately
max-charlamb Feb 19, 2026
c8519a4
Merge branch 'cdac-dumptests' of https://github.com/max-charlamb/runt…
max-charlamb Feb 19, 2026
ab80618
add support for running from CI dumps
max-charlamb Feb 19, 2026
3f16942
fix windows paths on unix machines
max-charlamb Feb 19, 2026
06cc2bd
add more platforms
max-charlamb Feb 19, 2026
ef9ff5f
address comments
max-charlamb Feb 19, 2026
dd69e3c
only build on avaialble ADO queues
max-charlamb Feb 19, 2026
6056ed8
fix cDAC Thread object from requiring the UEWatsonBucketTrackerBucket…
max-charlamb Feb 19, 2026
056af5a
remove more skips
max-charlamb Feb 19, 2026
265d112
add serverGC test
max-charlamb Feb 19, 2026
d1f203c
try to fix arm64 build
max-charlamb Feb 19, 2026
642be8b
try using heap dumps
max-charlamb Feb 19, 2026
2bb3bd1
don't use osx_arm64
max-charlamb Feb 19, 2026
eb5fb49
change back to full dumps
max-charlamb Feb 19, 2026
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
7 changes: 6 additions & 1 deletion eng/Subsets.props
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
<AllSubsetsExpansion>$(AllSubsetsExpansion)+clr.paltests+clr.paltestlist+clr.hosts+clr.jit+clr.alljits+clr.alljitscommunity+clr.spmi+clr.corelib+clr.nativecorelib+clr.tools+clr.toolstests+clr.packages</AllSubsetsExpansion>
<AllSubsetsExpansion Condition="$([MSBuild]::IsOsPlatform(Windows))">$(AllSubsetsExpansion)+linuxdac+alpinedac</AllSubsetsExpansion>
<AllSubsetsExpansion>$(AllSubsetsExpansion)+mono.runtime+provision.emsdk+mono.aotcross+mono.corelib+mono.manifests+mono.packages+mono.tools+mono.wasmruntime+mono.wasiruntime+mono.wasmworkload+mono.mscordbi+mono.workloads</AllSubsetsExpansion>
<AllSubsetsExpansion>$(AllSubsetsExpansion)+tools.illink+tools.cdac+tools.illinktests+tools.cdactests</AllSubsetsExpansion>
<AllSubsetsExpansion>$(AllSubsetsExpansion)+tools.illink+tools.cdac+tools.illinktests+tools.cdactests+tools.cdacdumptests</AllSubsetsExpansion>
<AllSubsetsExpansion>$(AllSubsetsExpansion)+host.native+host.pkg+host.tools+host.pretest+host.tests</AllSubsetsExpansion>
<AllSubsetsExpansion>$(AllSubsetsExpansion)+libs.native+libs.sfx+libs.oob+libs.pretest+libs.tests</AllSubsetsExpansion>
<AllSubsetsExpansion>$(AllSubsetsExpansion)+packs.product+packs.installers+packs.tests</AllSubsetsExpansion>
Expand Down Expand Up @@ -253,6 +253,7 @@
<SubsetName Include="Tools.Cdac" Description="Diagnostic data contract reader and related projects." />
<SubsetName Include="Tools.ILLinkTests" OnDemand="true" Description="Unit tests for the tools.illink subset." />
<SubsetName Include="Tools.CdacTests" OnDemand="true" Description="Unit tests for the diagnostic data contract reader." />
<SubsetName Include="Tools.CdacDumpTests" OnDemand="true" Description="Dump-based integration tests for the diagnostic data contract reader." />
<SubsetName Include="Tools.ILAsm" OnDemand="true" Description="Build only the managed ilasm tool." />

<!-- Host -->
Expand Down Expand Up @@ -523,6 +524,10 @@
<ProjectToBuild Include="$(SharedNativeRoot)managed\cdac\tests\Microsoft.Diagnostics.DataContractReader.Tests.csproj" Test="true" Category="tools"/>
</ItemGroup>

<ItemGroup Condition="$(_subset.Contains('+tools.cdacdumptests+'))">
<ProjectToBuild Include="$(SharedNativeRoot)managed\cdac\tests\DumpTests\Microsoft.Diagnostics.DataContractReader.DumpTests.csproj" Test="true" Category="tools"/>
</ItemGroup>

<ItemGroup Condition="$(_subset.Contains('+tools.illink+'))">
<ProjectToBuild Include="$(ToolsProjectRoot)illink\src\linker\Mono.Linker.csproj" Category="tools" />
<ProjectToBuild Include="$(ToolsProjectRoot)illink\src\ILLink.Tasks\ILLink.Tasks.csproj" Category="tools" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
<NewtonsoftJsonVersion>13.0.3</NewtonsoftJsonVersion>
<NewtonsoftJsonBsonVersion>1.0.2</NewtonsoftJsonBsonVersion>
<MoqVersion>4.18.4</MoqVersion>
<MicrosoftDiagnosticsRuntimeVersion>3.1.512801</MicrosoftDiagnosticsRuntimeVersion>
<AwesomeAssertionsVersion>8.0.2</AwesomeAssertionsVersion>
<FsCheckVersion>2.14.3</FsCheckVersion>
<CommandLineParserVersion>2.9.1</CommandLineParserVersion>
Expand Down
86 changes: 86 additions & 0 deletions eng/pipelines/runtime-diagnostics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ parameters:
displayName: Diagnostics Branch
type: string
default: main
- name: cdacDumpPlatforms
displayName: cDAC Dump Platforms
type: object
default:
- windows_x64
- linux_x64
- osx_x64

resources:
repositories:
Expand Down Expand Up @@ -134,3 +141,82 @@ extends:
buildConfiguration: $(_BuildConfig)
continueOnError: true
condition: always()

#
# cDAC Dump Creation — Build runtime, create crash dumps, publish dump artifacts
#
- stage: DumpCreation
dependsOn: []
jobs:
- template: /eng/pipelines/common/platform-matrix.yml
parameters:
jobTemplate: /eng/pipelines/common/global-build-job.yml
buildConfig: release
platforms: ${{ parameters.cdacDumpPlatforms }}
jobParameters:
buildArgs: -s clr+libs+tools.cdac -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig)
Copy link
Member

Choose a reason for hiding this comment

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

I'm concerned this would add a fair amount of load (and potentially instability) if we are running these in every CI. Wasn't sure if that was the intent.

I think something like this might work out well:

  • For validating the runtime is generating the right contract data, create the dumps during a normal test leg and immediately consume them (no persistence, no cross-plat). Ideally I'd love to see that testing running in the runtime repo CI assuming its pretty low cost/reliable.
  • For validating that cDAC is correctly implemented in inner dev loops and normal CI runs, use MockDataContracts to supply the data rather than dumps. It should execute very fast, its fully deterministic, no dependencies, and can simulate all architectures X all contract versions
  • Infrequently (once a week?) do this cross-arch dump verification. If we did a decent job in the first two bullets then we probably wouldn't see many issues here at all.

nameSuffix: CdacDumpGeneration
timeoutInMinutes: 120
postBuildSteps:
- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) build
$(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj
/p:CIDumpVersionsOnly=true
/p:TargetArchitecture=$(archType)
-bl:$(Build.SourcesDirectory)/artifacts/log/DumpGeneration.binlog
displayName: 'Build Dump Tests and Generate Dumps'
- template: /eng/pipelines/common/upload-artifact-step.yml
parameters:
rootFolder: $(Build.SourcesDirectory)/artifacts/dumps/cdac
includeRootFolder: false
archiveType: tar
archiveExtension: .tar.gz
tarCompression: gz
artifactName: CdacDumps_$(osGroup)_$(archType)
displayName: cDAC Dump Artifacts

#
# cDAC Dump Tests — Download dumps from all platforms, run tests cross-platform
#
- stage: DumpTest
dependsOn:
- DumpCreation
jobs:
- template: /eng/pipelines/common/platform-matrix.yml
parameters:
jobTemplate: /eng/pipelines/common/global-build-job.yml
buildConfig: release
platforms: ${{ parameters.cdacDumpPlatforms }}
jobParameters:
buildArgs: -s tools.cdacdumptests /p:SkipDumpGeneration=true /p:SkipDumpVersions=net10.0
nameSuffix: CdacDumpTests
timeoutInMinutes: 60
postBuildSteps:
# Download and test against dumps from each platform
- ${{ each dumpPlatform in parameters.cdacDumpPlatforms }}:
- template: /eng/pipelines/common/download-artifact-step.yml
parameters:
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The platform naming convention appears inconsistent. The artifact name uses underscores (CdacDumps_${{ dumpPlatform }}), but typically Azure Pipelines artifact names should avoid special characters where possible. Consider using a hyphen instead of underscore for better compatibility, or document why the underscore is intentional. Additionally, ensure that ${{ dumpPlatform }} values like "windows_x64" won't cause issues when used in artifact names - though this appears to be following existing conventions in the repo.

Suggested change
parameters:
parameters:
# NOTE: Underscores in this artifact name are intentional to match the
# producing job's artifact naming and existing CdacDumps_* conventions.

Copilot uses AI. Check for mistakes.
artifactName: CdacDumps_${{ dumpPlatform }}
artifactFileName: CdacDumps_${{ dumpPlatform }}.tar.gz
unpackFolder: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }}
displayName: '${{ dumpPlatform }} Dumps'
- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) test
$(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj
--no-build
--logger "trx;LogFileName=CdacDumpTests_${{ dumpPlatform }}.trx"
--results-directory $(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }}
displayName: 'Run cDAC Dump Tests (${{ dumpPlatform }} dumps)'
continueOnError: true
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The dotnet test step is marked continueOnError: true, so dump-test failures won't fail the job immediately. If these tests are intended to gate the pipeline, remove continueOnError (or capture $LASTEXITCODE and fail at the end).

Suggested change
continueOnError: true

Copilot uses AI. Check for mistakes.
env:
CDAC_DUMP_ROOT: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }}
- task: PublishTestResults@2
displayName: 'Publish Results ($(osGroup)-$(archType) → ${{ dumpPlatform }})'
inputs:
testResultsFormat: VSTest
testResultsFiles: '**/*.trx'
searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }}'
testRunTitle: 'cDAC Dump Tests $(osGroup)-$(archType) → ${{ dumpPlatform }}'
failTaskOnFailedTests: true
publishRunAttachments: true
buildConfiguration: $(_BuildConfig)
continueOnError: true
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

With failTaskOnFailedTests: true, setting continueOnError: true on PublishTestResults@2 can allow failed dump tests to not fail the job. If failures should be enforced, remove continueOnError (or handle failures explicitly).

Suggested change
continueOnError: true

Copilot uses AI. Check for mistakes.
condition: always()
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ public Thread(Target target, TargetPointer address)

// Address of the exception tracker
ExceptionTracker = address + (ulong)type.Fields[nameof(ExceptionTracker)].Offset;
UEWatsonBucketTrackerBuckets = target.ReadPointer(address + (ulong)type.Fields[nameof(UEWatsonBucketTrackerBuckets)].Offset);
// UEWatsonBucketTrackerBuckets does not exist on certain platforms
UEWatsonBucketTrackerBuckets = type.Fields.TryGetValue(nameof(UEWatsonBucketTrackerBuckets), out Target.FieldInfo watsonFieldInfo)
? target.ReadPointer(address + (ulong)watsonFieldInfo.Offset)
: TargetPointer.Null;
ThreadLocalDataPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(ThreadLocalDataPtr)].Offset);
}

Expand Down
104 changes: 104 additions & 0 deletions src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 Microsoft.Diagnostics.Runtime;

namespace Microsoft.Diagnostics.DataContractReader.DumpTests;

/// <summary>
/// Wraps a ClrMD DataTarget to provide the memory read callback and symbol lookup
/// needed to create a <see cref="ContractDescriptorTarget"/> from a crash dump.
/// </summary>
internal sealed class ClrMdDumpHost : IDisposable
{
private static readonly string[] s_runtimeModuleNames =
{
"coreclr.dll",
"libcoreclr.so",
"libcoreclr.dylib",
};

private readonly DataTarget _dataTarget;

public string DumpPath { get; }

private ClrMdDumpHost(string dumpPath, DataTarget dataTarget)
{
DumpPath = dumpPath;
_dataTarget = dataTarget;
}

/// <summary>
/// Open a crash dump and prepare it for cDAC analysis.
/// </summary>
public static ClrMdDumpHost Open(string dumpPath)
{
DataTarget dataTarget = DataTarget.LoadDump(dumpPath);
return new ClrMdDumpHost(dumpPath, dataTarget);
}

/// <summary>
/// Read memory from the dump at the specified address.
/// Returns 0 on success, non-zero on failure.
/// </summary>
public int ReadFromTarget(ulong address, Span<byte> buffer)
{
int bytesRead = _dataTarget.DataReader.Read(address, buffer);
return bytesRead == buffer.Length ? 0 : -1;
}

/// <summary>
/// Get a thread's register context from the dump.
/// Returns 0 on success, non-zero on failure.
/// </summary>
public int GetThreadContext(uint threadId, uint contextFlags, Span<byte> buffer)
{
return _dataTarget.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1;
}

/// <summary>
/// Locate the DotNetRuntimeContractDescriptor symbol address in the dump.
/// Uses ClrMD's built-in export resolution which handles PE, ELF, and Mach-O formats.
/// </summary>
public ulong FindContractDescriptorAddress()
{
foreach (ModuleInfo module in _dataTarget.DataReader.EnumerateModules())
{
string? fileName = module.FileName;
if (fileName is null)
continue;

// Path.GetFileName doesn't handle Windows paths on a Linux/macOS host,
// so split on both separators to extract the file name correctly when
// analyzing cross-platform dumps.
int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\'));
string name = lastSep >= 0 ? fileName.Substring(lastSep + 1) : fileName;
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The string manipulation using Substring is not safe when the file name is exactly at the end of the path. Consider using Path.GetFileName with appropriate handling, or use the newer string slicing syntax [Range] which is more idiomatic in modern C#. The current code assumes lastSep >= 0 is checked before calling Substring, which is correct, but the fallback case fileName when lastSep < 0 means there's no directory separator - you could simplify this to:

string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName;

This uses the range operator which is more modern and clearer.

Suggested change
string name = lastSep >= 0 ? fileName.Substring(lastSep + 1) : fileName;
string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName;

Copilot uses AI. Check for mistakes.
if (!IsRuntimeModule(name))
continue;

ulong address = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor");
if (address != 0)
return address;
}

throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export in any runtime module in the dump.");
}

private static bool IsRuntimeModule(string fileName)
{
foreach (string name in s_runtimeModuleNames)
{
if (fileName.Equals(name, StringComparison.OrdinalIgnoreCase))
return true;
}

return false;
}

public void Dispose()
{
_dataTarget.Dispose();
GC.SuppressFinalize(this);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The GC.SuppressFinalize(this) call is unnecessary here because the class doesn't define a finalizer. This pattern is typically used when a class has both a finalizer and a Dispose method to prevent the finalizer from running after Dispose has been called. Since ClrMdDumpHost doesn't have a finalizer, this line can be removed.

Suggested change
GC.SuppressFinalize(this);

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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;

/// <summary>
/// Debuggee app for cDAC dump tests.
/// Spawns threads with known names, ensures they are all alive, then crashes
/// so a dump is produced for analysis.
/// </summary>
internal static class Program
{
// These constants are referenced by ThreadDumpTests to assert expected values.
public const int SpawnedThreadCount = 5;
public static readonly string[] ThreadNames = new[]
{
"cdac-test-thread-0",
"cdac-test-thread-1",
"cdac-test-thread-2",
"cdac-test-thread-3",
"cdac-test-thread-4",
};

private static void Main()
{
// Barrier ensures all threads are alive and named before we crash.
// participantCount = SpawnedThreadCount + 1 (main thread)
using Barrier barrier = new(SpawnedThreadCount + 1);

Thread[] threads = new Thread[SpawnedThreadCount];
for (int i = 0; i < SpawnedThreadCount; i++)
{
int index = i;
threads[i] = new Thread(() =>
{
// Signal that this thread is alive and wait for all others.
barrier.SignalAndWait();

// Keep the thread alive until the process crashes.
Thread.Sleep(Timeout.Infinite);
})
{
Name = ThreadNames[index],
IsBackground = true,
};
threads[i].Start();
}

// Wait until all spawned threads have reached the barrier.
barrier.SignalAndWait();

// All threads are alive and named. Crash to produce a dump.
Environment.FailFast("cDAC dump test: BasicThreads debuggee intentional crash");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..\'))" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(NetCoreAppToolCurrent);net10.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>$(ArtifactsBinDir)DumpTests\$(MSBuildProjectName)\$(Configuration)\</OutputPath>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<!-- Debuggees intentionally use unsealed types for type hierarchy testing -->
<NoWarn>$(NoWarn);CA1852</NoWarn>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

/// <summary>
/// Debuggee for cDAC dump tests — exercises the Exception contract.
/// Creates a nested exception chain then crashes with FailFast.
/// </summary>
internal static class Program
{
public class DebuggeeException : Exception
{
public DebuggeeException(string message) : base(message) { }
public DebuggeeException(string message, Exception inner) : base(message, inner) { }
}

private static void Main()
{
Exception? caughtException;

// Build a nested exception chain
try
{
try
{
try
{
throw new InvalidOperationException("innermost exception");
}
catch (Exception ex)
{
throw new DebuggeeException("middle exception", ex);
}
}
catch (Exception ex)
{
throw new DebuggeeException("outermost exception", ex);
}
}
catch (Exception ex)
{
caughtException = ex;
}

// Keep the exception chain alive
GC.KeepAlive(caughtException);

Environment.FailFast("cDAC dump test: ExceptionState debuggee intentional crash");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>
Loading
Loading