-
Notifications
You must be signed in to change notification settings - Fork 5.3k
[cDAC] Add infrastructure to run cDAC tests using CLRMD and dumps #124564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9e06fe4
20a18ae
03d470d
d06c90f
7bf223f
8295470
eb8ad7d
f19b808
5c06718
0727d57
da8011b
7686c17
b4ad6ee
d02012e
b5ba445
3dc06d1
c8519a4
ab80618
3f16942
06cc2bd
ef9ff5f
dd69e3c
6056ed8
056af5a
265d112
d1f203c
642be8b
2bb3bd1
eb5fb49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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: | ||||||||||
|
|
@@ -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) | ||||||||||
| 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: | ||||||||||
|
||||||||||
| parameters: | |
| parameters: | |
| # NOTE: Underscores in this artifact name are intentional to match the | |
| # producing job's artifact naming and existing CdacDumps_* conventions. |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
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).
| continueOnError: true |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
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).
| continueOnError: true |
| 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; | ||||||
|
||||||
| string name = lastSep >= 0 ? fileName.Substring(lastSep + 1) : fileName; | |
| string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
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.
| GC.SuppressFinalize(this); |
max-charlamb marked this conversation as resolved.
Show resolved
Hide resolved
| 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> |
There was a problem hiding this comment.
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: