diff --git a/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml b/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml new file mode 100644 index 00000000000000..436153bc7cc8ef --- /dev/null +++ b/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml @@ -0,0 +1,45 @@ +# prepare-cdac-stress-helix-steps.yml - Steps for preparing cDAC stress test Helix payloads. +# +# Used by CdacDumpTests stage in runtime-diagnostics.yml. +# Handles: building stress test debuggees, preparing Helix payload, finding testhost. + +steps: +- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) msbuild + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj + /t:BuildDebuggeesOnly + /p:Configuration=$(_BuildConfig) + /p:TargetArchitecture=$(archType) + -bl:$(Build.SourcesDirectory)/artifacts/log/BuildStressDebuggees.binlog + displayName: 'Build Stress Debuggees' + +- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) build + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj + /p:PrepareHelixPayload=true + /p:Configuration=$(_BuildConfig) + /p:HelixPayloadDir=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac-stress + -bl:$(Build.SourcesDirectory)/artifacts/log/StressTestPayload.binlog + displayName: 'Prepare Stress Test Helix Payload' + +- pwsh: | + $testhostDir = Get-ChildItem -Directory -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-*-$(archType)" | Select-Object -First 1 -ExpandProperty FullName + if (-not $testhostDir) { + Write-Error "No testhost directory found" + exit 1 + } + Write-Host "TestHost root: $testhostDir" + Write-Host "##vso[task.setvariable variable=StressTestHostRootDir]$testhostDir" + + $queue = switch ("$(osGroup)_$(archType)") { + "windows_x64" { "$(helix_windows_x64)" } + "windows_x86" { "$(helix_windows_x64)" } + "windows_arm64" { "$(helix_windows_arm64)" } + "linux_x64" { "$(helix_linux_x64_oldest)" } + "linux_arm64" { "$(helix_linux_arm64_oldest)" } + "linux_arm" { "$(helix_linux_arm32_oldest)" } + "osx_x64" { "$(helix_macos_x64)" } + "osx_arm64" { "$(helix_macos_arm64)" } + default { Write-Error "Unsupported platform: $(osGroup)_$(archType)"; exit 1 } + } + Write-Host "Helix queue: $queue" + Write-Host "##vso[task.setvariable variable=CdacStressHelixQueue]$queue" + displayName: 'Find Stress TestHost and Helix Queue' diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index f6a3f9215fdd0b..01bb6723bebab8 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -267,7 +267,7 @@ extends: shouldContinueOnError: true jobParameters: nameSuffix: CdacDumpTest - buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 + buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests+tools.cdacstresstests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 timeoutInMinutes: 180 postBuildSteps: - template: /eng/pipelines/cdac/prepare-cdac-helix-steps.yml @@ -286,6 +286,16 @@ extends: displayName: 'Publish Dump Artifacts' condition: and(always(), ne(variables['Agent.JobStatus'], 'Succeeded')) continueOnError: true + # cDAC Stress Tests — run GC stress verification on the same Checked build + - template: /eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml + - template: /eng/pipelines/common/templates/runtimes/send-to-helix-inner-step.yml + parameters: + displayName: 'Send cDAC Stress Tests to Helix' + sendParams: $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj /t:Test /p:TargetOS=$(osGroup) /p:TargetArchitecture=$(archType) /p:HelixTargetQueues="$(CdacStressHelixQueue)" /p:TestHostPayload=$(StressTestHostRootDir) /p:StressTestsPayload=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac-stress /bl:$(Build.SourcesDirectory)/artifacts/log/SendStressToHelix.binlog + environment: + _Creator: dotnet-bot + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + NUGET_PACKAGES: $(Build.SourcesDirectory)$(dir).packages - pwsh: | if ("$(Agent.JobStatus)" -ne "Succeeded") { Write-Error "One or more cDAC dump test failures were detected. Failing the job." diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs deleted file mode 100644 index fa72eb606fad75..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ /dev/null @@ -1,109 +0,0 @@ -// 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.DataContractReader.Contracts.GCInfoHelpers; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; - -internal class GcScanner -{ - private readonly Target _target; - private readonly IExecutionManager _eman; - private readonly IGCInfo _gcInfo; - - internal GcScanner(Target target) - { - _target = target; - _eman = target.Contracts.ExecutionManager; - _gcInfo = target.Contracts.GCInfo; - } - - public bool EnumGcRefs( - IPlatformAgnosticContext context, - CodeBlockHandle cbh, - CodeManagerFlags flags, - GcScanContext scanContext) - { - TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); - _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); - - if (_eman.IsFilterFunclet(cbh)) - flags |= CodeManagerFlags.NoReportUntracked; - - IGCInfoHandle handle = _gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); - if (handle is not IGCInfoDecoder decoder) - return false; - - uint stackBaseRegister = decoder.StackBaseRegister; - - // Lazily compute the caller SP for GC_CALLER_SP_REL slots. - // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. - TargetPointer? callerSP = null; - - return decoder.EnumerateLiveSlots( - (uint)relativeOffset.Value, - flags, - (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => - { - GcScanFlags scanFlags = GcScanFlags.None; - if ((gcFlags & 0x1) != 0) // GC_SLOT_INTERIOR - scanFlags |= GcScanFlags.GC_CALL_INTERIOR; - if ((gcFlags & 0x2) != 0) // GC_SLOT_PINNED - scanFlags |= GcScanFlags.GC_CALL_PINNED; - - if (isRegister) - { - TargetPointer regValue = ReadRegisterValue(context, (int)registerNumber); - GcScanSlotLocation loc = new((int)registerNumber, 0, false); - scanContext.GCEnumCallback(regValue, scanFlags, loc); - } - else - { - int spReg = context.StackPointerRegister; - int reg = spBase switch - { - 1 => spReg, // GC_SP_REL → SP register number - 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → frame base register - 0 => -(spReg + 1), // GC_CALLER_SP_REL → -(SP + 1) - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - TargetPointer baseAddr = spBase switch - { - 1 => context.StackPointer, // GC_SP_REL - 2 => ReadRegisterValue(context, (int)stackBaseRegister), // GC_FRAMEREG_REL - 0 => GetCallerSP(context, ref callerSP), // GC_CALLER_SP_REL - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - - TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); - GcScanSlotLocation loc = new(reg, spOffset, true); - scanContext.GCEnumCallback(addr, scanFlags, loc); - } - }); - } - - /// - /// Compute the caller's SP by unwinding the current context one frame. - /// Cached in to avoid repeated unwinds for the same frame. - /// - private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) - { - if (cached is null) - { - IPlatformAgnosticContext callerContext = context.Clone(); - callerContext.Unwind(_target); - cached = callerContext.StackPointer; - } - return cached.Value; - } - - private static TargetPointer ReadRegisterValue(IPlatformAgnosticContext context, int registerNumber) - { - if (!context.TryReadRegister(registerNumber, out TargetNUInt value)) - throw new ArgumentOutOfRangeException(nameof(registerNumber), $"Register number {registerNumber} not found"); - - return new TargetPointer(value.Value); - } - -} diff --git a/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs new file mode 100644 index 00000000000000..5da773f7828f7b --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Runs each debuggee app under corerun with DOTNET_CdacStress=0x51 and asserts +/// that the cDAC stack reference verification achieves 100% pass rate. +/// +/// +/// Prerequisites: +/// - Build CoreCLR native + cDAC: build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +/// - Generate core_root: src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +/// - Build debuggees: dotnet build this test project +/// +/// The tests use CORE_ROOT env var if set, otherwise default to the standard artifacts path. +/// +public class BasicStressTests : CdacStressTestBase +{ + public BasicStressTests(ITestOutputHelper output) : base(output) { } + + public static IEnumerable Debuggees => + [ + ["BasicAlloc"], + ["DeepStack"], + ["Generics"], + ["MultiThread"], + ["Comprehensive"], + ["ExceptionHandling"], + ["StructScenarios"], + ["DynamicMethods"], + ]; + + public static IEnumerable WindowsOnlyDebuggees => + [ + ["PInvoke"], + ]; + + [Theory] + [MemberData(nameof(Debuggees))] + public void GCStress_AllVerificationsPass(string debuggeeName) + { + CdacStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } + + [Theory] + [MemberData(nameof(WindowsOnlyDebuggees))] + public void GCStress_WindowsOnly_AllVerificationsPass(string debuggeeName) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new SkipTestException("P/Invoke debuggee uses kernel32.dll (Windows only)"); + + CdacStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs new file mode 100644 index 00000000000000..dd2222741ebed3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs @@ -0,0 +1,275 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Parses the cdac stress results log file written by the native cdacstress.cpp hook. +/// +internal sealed partial class CdacStressResults +{ + public int TotalVerifications { get; private set; } + public int Passed { get; private set; } + public int Failed { get; private set; } + public int Skipped { get; private set; } + public int RtDiffs { get; private set; } + public string LogFilePath { get; private set; } = string.Empty; + public List FailureDetails { get; } = []; + public List SkipDetails { get; } = []; + public List FailedVerifications { get; } = []; + + [GeneratedRegex(@"^\[PASS\]")] + private static partial Regex PassPattern(); + + [GeneratedRegex(@"^\[FAIL\]")] + private static partial Regex FailPattern(); + + [GeneratedRegex(@"^\[SKIP\]")] + private static partial Regex SkipPattern(); + + [GeneratedRegex(@"^Total verifications:\s*(\d+)")] + private static partial Regex TotalPattern(); + + [GeneratedRegex(@"\[RT_DIFF\]")] + private static partial Regex RtDiffPattern(); + + [GeneratedRegex(@"\[FRAME_DIFF\]\s+Source=0x(\w+)\s+\(([^)]+)\):\s+(\w+)=(\d+)\s+(\w+)=(\d+)")] + private static partial Regex FrameDiffPattern(); + + [GeneratedRegex(@"\[FRAME_(\w+)_ONLY\]\s+Source=0x(\w+)\s+\(([^)]+)\):\s+\w+=(\d+)")] + private static partial Regex FrameOnlyPattern(); + + [GeneratedRegex(@"\[(cDAC|DAC|RT)_ONLY\]\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)")] + private static partial Regex RefOnlyPattern(); + + [GeneratedRegex(@"cDAC \[\d+\]:\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)\s+Src=(.+)")] + private static partial Regex CdacRefPattern(); + + [GeneratedRegex(@"RT\s+\[\d+\]:\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)")] + private static partial Regex RtRefPattern(); + + public static CdacStressResults Parse(string logFilePath) + { + if (!File.Exists(logFilePath)) + throw new FileNotFoundException($"GC stress results log not found: {logFilePath}"); + + var results = new CdacStressResults { LogFilePath = logFilePath }; + FailedVerification? currentFailure = null; + FrameDiff? currentFrame = null; + + foreach (string line in File.ReadLines(logFilePath)) + { + string trimmed = line.Trim(); + + if (PassPattern().IsMatch(trimmed)) + { + currentFailure = null; + currentFrame = null; + results.Passed++; + } + else if (FailPattern().IsMatch(trimmed)) + { + results.Failed++; + results.FailureDetails.Add(trimmed); + currentFailure = new FailedVerification { Header = trimmed }; + results.FailedVerifications.Add(currentFailure); + currentFrame = null; + } + else if (SkipPattern().IsMatch(trimmed)) + { + currentFailure = null; + currentFrame = null; + results.Skipped++; + results.SkipDetails.Add(trimmed); + } + else if (RtDiffPattern().IsMatch(trimmed)) + { + results.RtDiffs++; + } + else if (currentFailure is not null) + { + // Parse structured per-frame output + Match frameDiff = FrameDiffPattern().Match(trimmed); + if (frameDiff.Success) + { + currentFrame = new FrameDiff + { + Source = ulong.Parse(frameDiff.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + MethodName = frameDiff.Groups[2].Value, + CdacCount = int.Parse(frameDiff.Groups[4].Value), + DacCount = int.Parse(frameDiff.Groups[6].Value), + Kind = FrameDiffKind.Different, + }; + currentFailure.FrameDiffs.Add(currentFrame); + continue; + } + + Match frameOnly = FrameOnlyPattern().Match(trimmed); + if (frameOnly.Success) + { + string ownerLabel = frameOnly.Groups[1].Value; + currentFrame = new FrameDiff + { + Source = ulong.Parse(frameOnly.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + MethodName = frameOnly.Groups[3].Value, + Kind = ownerLabel == "cDAC" ? FrameDiffKind.CdacOnly : FrameDiffKind.DacOnly, + }; + int count = int.Parse(frameOnly.Groups[4].Value); + if (currentFrame.Kind == FrameDiffKind.CdacOnly) + currentFrame.CdacCount = count; + else + currentFrame.DacCount = count; + currentFailure.FrameDiffs.Add(currentFrame); + continue; + } + + Match refOnly = RefOnlyPattern().Match(trimmed); + if (refOnly.Success && currentFrame is not null) + { + var r = new StackRef + { + Address = ulong.Parse(refOnly.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(refOnly.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(refOnly.Groups[4].Value, System.Globalization.NumberStyles.HexNumber), + }; + currentFrame.UnmatchedRefs.Add(($"{refOnly.Groups[1].Value}_ONLY", r)); + continue; + } + + // Parse flat cDAC/RT ref lines (for cDAC-vs-RT comparison) + Match cdacRef = CdacRefPattern().Match(trimmed); + if (cdacRef.Success) + { + currentFailure.CdacRefs.Add(new StackRef + { + Address = ulong.Parse(cdacRef.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(cdacRef.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(cdacRef.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + }); + continue; + } + + Match rtRef = RtRefPattern().Match(trimmed); + if (rtRef.Success) + { + currentFailure.RtRefs.Add(new StackRef + { + Address = ulong.Parse(rtRef.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(rtRef.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(rtRef.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + }); + continue; + } + + // Parse [STACK_TRACE] frame lines: #N MethodName (cDAC=X DAC=Y) + if (trimmed.StartsWith("#") && trimmed.Contains("(cDAC=")) + { + currentFailure.StackTrace.Add(trimmed); + } + } + + Match totalMatch = TotalPattern().Match(trimmed); + if (totalMatch.Success) + { + results.TotalVerifications = int.Parse(totalMatch.Groups[1].Value); + } + } + + if (results.TotalVerifications == 0) + { + results.TotalVerifications = results.Passed + results.Failed + results.Skipped; + } + + return results; + } + + public override string ToString() => + $"Total={TotalVerifications}, Passed={Passed}, Failed={Failed}, Skipped={Skipped}, RtDiffs={RtDiffs}"; + + /// + /// Formats the first N failed verifications using the structured per-frame data + /// logged by the native code. No re-analysis needed — just presents what was logged. + /// + public string AnalyzeFailures(int maxFailures = 3) + { + var sb = new System.Text.StringBuilder(); + + foreach (var failure in FailedVerifications.Take(maxFailures)) + { + sb.AppendLine(failure.Header); + + if (failure.FrameDiffs.Count > 0) + { + sb.AppendLine(" Per-frame diff (cDAC vs DAC):"); + foreach (var frame in failure.FrameDiffs) + { + string kindLabel = frame.Kind switch + { + FrameDiffKind.Different => $"cDAC={frame.CdacCount} DAC={frame.DacCount}", + FrameDiffKind.CdacOnly => $"cDAC={frame.CdacCount} (cDAC-only frame)", + FrameDiffKind.DacOnly => $"DAC={frame.DacCount} (DAC-only frame)", + _ => "unknown", + }; + sb.AppendLine($" {frame.MethodName}: {kindLabel}"); + foreach (var (label, r) in frame.UnmatchedRefs) + sb.AppendLine($" [{label}] Addr=0x{r.Address:X} Obj=0x{r.Object:X} Flags=0x{r.Flags:X}"); + } + } + + if (failure.CdacRefs.Count > 0 || failure.RtRefs.Count > 0) + { + sb.AppendLine($" cDAC vs RT: cDAC={failure.CdacRefs.Count} RT={failure.RtRefs.Count}"); + } + + if (failure.StackTrace.Count > 0) + { + sb.AppendLine(" Stack trace:"); + foreach (string frame in failure.StackTrace) + sb.AppendLine($" {frame}"); + } + + sb.AppendLine(); + } + + return sb.ToString(); + } +} + +internal struct StackRef +{ + public ulong Address; + public ulong Object; + public uint Flags; +} + +internal enum FrameDiffKind +{ + Different, + CdacOnly, + DacOnly, +} + +internal sealed class FrameDiff +{ + public ulong Source { get; set; } + public string MethodName { get; set; } = ""; + public int CdacCount { get; set; } + public int DacCount { get; set; } + public FrameDiffKind Kind { get; set; } + public List<(string Label, StackRef Ref)> UnmatchedRefs { get; } = []; +} + +internal sealed class FailedVerification +{ + public string Header { get; set; } = ""; + public List FrameDiffs { get; } = []; + public List CdacRefs { get; } = []; + public List RtRefs { get; } = []; + public List StackTrace { get; } = []; +} diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs new file mode 100644 index 00000000000000..8a3eb52219ba38 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs @@ -0,0 +1,237 @@ +// 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.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Base class for cDAC stress tests. Runs a debuggee app under corerun +/// with DOTNET_CdacStress=0x51 and parses the verification results. +/// +public abstract class CdacStressTestBase +{ + private readonly ITestOutputHelper _output; + + protected CdacStressTestBase(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Runs the named debuggee under GC stress and returns the parsed results. + /// + internal CdacStressResults RunGCStress(string debuggeeName, int timeoutSeconds = 300) + { + string coreRoot = GetCoreRoot(); + string corerun = GetCoreRunPath(coreRoot); + string debuggeeDll = GetDebuggeePath(debuggeeName); + string logFile = Path.Combine(Path.GetTempPath(), $"cdac-gcstress-{debuggeeName}-{Guid.NewGuid():N}.txt"); + + _output.WriteLine($"Running GC stress: {debuggeeName}"); + _output.WriteLine($" corerun: {corerun}"); + _output.WriteLine($" debuggee: {debuggeeDll}"); + _output.WriteLine($" log: {logFile}"); + + var psi = new ProcessStartInfo + { + FileName = corerun, + Arguments = $"\"{debuggeeDll}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Environment["CORE_ROOT"] = coreRoot; + // Default to 0x51 (ALLOC + REFS + USE_DAC) for three-way comparison. + // Override via outer DOTNET_CdacStress env var if needed. + psi.Environment["DOTNET_CdacStress"] = + Environment.GetEnvironmentVariable("DOTNET_CdacStress") ?? "0x51"; + psi.Environment["DOTNET_CdacStressFailFast"] = "0"; + psi.Environment["DOTNET_CdacStressLogFile"] = logFile; + psi.Environment["DOTNET_CdacStressStep"] = "1"; + psi.Environment["DOTNET_ContinueOnAssert"] = "1"; + + using var process = Process.Start(psi)!; + + // Read both stdout and stderr asynchronously to avoid deadlock + // when pipe buffers fill, and to allow WaitForExit timeout to work. + string stderr = ""; + string stdout = ""; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is not null) + stderr += e.Data + Environment.NewLine; + }; + process.OutputDataReceived += (_, e) => + { + if (e.Data is not null) + stdout += e.Data + Environment.NewLine; + }; + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + bool exited = process.WaitForExit(timeoutSeconds * 1000); + if (!exited) + { + process.Kill(entireProcessTree: true); + Assert.Fail($"GC stress test '{debuggeeName}' timed out after {timeoutSeconds}s"); + } + + _output.WriteLine($" exit code: {process.ExitCode}"); + if (!string.IsNullOrWhiteSpace(stdout)) + _output.WriteLine($" stdout: {stdout.TrimEnd()}"); + if (!string.IsNullOrWhiteSpace(stderr)) + _output.WriteLine($" stderr: {stderr.TrimEnd()}"); + + Assert.True(process.ExitCode == 100, + $"GC stress test '{debuggeeName}' exited with {process.ExitCode} (expected 100).\nstdout: {stdout}\nstderr: {stderr}"); + + Assert.True(File.Exists(logFile), + $"GC stress results log not created: {logFile}"); + + CdacStressResults results = CdacStressResults.Parse(logFile); + + _output.WriteLine($" results: {results}"); + + return results; + } + + /// + /// Asserts that GC stress verification produced 100% pass rate with no failures or skips. + /// + internal static void AssertAllPassed(CdacStressResults results, string debuggeeName) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + if (results.Failed > 0) + { + string analysis = results.AnalyzeFailures(maxFailures: 3); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Failed} failure(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n\n{analysis}"); + } + + if (results.Skipped > 0) + { + string details = string.Join("\n", results.SkipDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Skipped} skip(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + } + + /// + /// Asserts that GC stress verification produced a pass rate at or above the given threshold. + /// Useful for instruction-level stress where a small number of failures may occur + /// due to known limitations. + /// + internal static void AssertHighPassRate(CdacStressResults results, string debuggeeName, double minPassRate) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + double passRate = (double)results.Passed / results.TotalVerifications; + if (passRate < minPassRate) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' pass rate {passRate:P2} is below " + + $"{minPassRate:P1} threshold. {results.Failed} failure(s) out of " + + $"{results.TotalVerifications} verifications.\n{details}"); + } + } + + private static string GetCoreRoot() + { + // Check environment variable first + string? coreRoot = Environment.GetEnvironmentVariable("CORE_ROOT"); + if (!string.IsNullOrEmpty(coreRoot) && Directory.Exists(coreRoot)) + return coreRoot; + + // Default path based on repo layout + string repoRoot = FindRepoRoot(); + string rid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" + : "linux"; + string arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); + coreRoot = Path.Combine(repoRoot, "artifacts", "tests", "coreclr", $"{rid}.{arch}.Checked", "Tests", "Core_Root"); + + if (!Directory.Exists(coreRoot)) + throw new DirectoryNotFoundException( + $"Core_Root not found at '{coreRoot}'. " + + "Set the CORE_ROOT environment variable or run 'src/tests/build.cmd Checked generatelayoutonly'."); + + return coreRoot; + } + + private static string GetCoreRunPath(string coreRoot) + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; + string path = Path.Combine(coreRoot, exe); + Assert.True(File.Exists(path), $"corerun not found at '{path}'"); + + return path; + } + + private static string GetDebuggeePath(string debuggeeName) + { + // On Helix, debuggees are in the work item payload's debuggees/ directory. + // The test assembly is in /tests/, so AppContext.BaseDirectory is there. + // The debuggees are siblings at /debuggees//. + string? helixPayload = Environment.GetEnvironmentVariable("HELIX_WORKITEM_PAYLOAD"); + if (!string.IsNullOrEmpty(helixPayload)) + { + string helixDebuggeesDir = Path.Combine(helixPayload, "debuggees", debuggeeName); + if (Directory.Exists(helixDebuggeesDir)) + { + foreach (string dir in Directory.GetDirectories(helixDebuggeesDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + } + } + + // Local development: debuggees are built to artifacts/bin/StressTests// + string repoRoot = FindRepoRoot(); + string binDir = Path.Combine(repoRoot, "artifacts", "bin", "StressTests", debuggeeName); + + if (!Directory.Exists(binDir)) + throw new DirectoryNotFoundException( + $"Debuggee '{debuggeeName}' not found at '{binDir}'. Build the StressTests project first."); + + // Find the dll in any Release/ subdirectory + foreach (string dir in Directory.GetDirectories(binDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + + throw new FileNotFoundException($"Could not find {debuggeeName}.dll under '{binDir}'"); + } + + private static string FindRepoRoot() + { + string? dir = AppContext.BaseDirectory; + while (dir is not null) + { + if (File.Exists(Path.Combine(dir, "global.json"))) + return dir; + dir = Path.GetDirectoryName(dir); + } + + throw new InvalidOperationException("Could not find repo root (global.json)"); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs new file mode 100644 index 00000000000000..c98679aea54ac2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs @@ -0,0 +1,43 @@ +// 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.Runtime.CompilerServices; + +/// +/// Exercises deep recursion with live GC references at each frame level. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedWithMultipleRefs(int depth) + { + object a = new object(); + string b = $"depth-{depth}"; + int[] c = new int[depth + 1]; + if (depth > 0) + NestedWithMultipleRefs(depth - 1); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + NestedCall(10); + NestedWithMultipleRefs(8); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs new file mode 100644 index 00000000000000..865d338e1e935f --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs @@ -0,0 +1,149 @@ +// 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.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +/// +/// Exercises the MetaSig (non-GCRefMap) path by creating and invoking +/// DynamicMethod (LCG) methods. These methods use StoredSigMethodDesc +/// and don't have pre-computed GCRefMaps, forcing PromoteCallerStack +/// to walk the signature via MetaSig. +/// +/// Scenarios: +/// - Simple object parameter (GcTypeKind.Ref) +/// - Multiple object parameters +/// - Byref parameter (GcTypeKind.Interior) +/// - Mixed ref and primitive parameters +/// - Method with 'this' (instance delegate) +/// - Method returning object (tests return type parsing) +/// +internal static class Program +{ + static int Main() + { + for (int i = 0; i < 50; i++) + { + SimpleObjectParam(); + MultipleObjectParams(); + MixedParams(); + ObjectReturn(); + KeepAliveInDynamic(); + } + return 100; + } + + // ===== Scenario 1: Single object parameter ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void SimpleObjectParam() + { + // Create: void DynMethod(object o) + DynamicMethod dm = new("DynSimple", typeof(void), new[] { typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + Action del = dm.CreateDelegate>(); + object live = new object(); + del(live); + GC.KeepAlive(live); + } + + // ===== Scenario 2: Multiple object parameters ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultipleObjectParams() + { + // Create: void DynMulti(object a, string b, int[] c) + DynamicMethod dm = new("DynMulti", typeof(void), + new[] { typeof(object), typeof(string), typeof(int[]) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object a = new object(); + string b = "hello"; + int[] c = new[] { 1, 2, 3 }; + del(a, b, c); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + // ===== Scenario 3: Mixed ref and primitive parameters ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void MixedParams() + { + // Create: void DynMixed(object o, int x, string s, long y) + DynamicMethod dm = new("DynMixed", typeof(void), + new[] { typeof(object), typeof(int), typeof(string), typeof(long) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object o = new object(); + string s = "world"; + del(o, 42, s, 999L); + GC.KeepAlive(o); + GC.KeepAlive(s); + } + + // ===== Scenario 4: Object return type ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void ObjectReturn() + { + // Create: object DynReturn(object o) + DynamicMethod dm = new("DynReturn", typeof(object), new[] { typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object input = new object(); + object result = del(input); + GC.KeepAlive(result); + GC.KeepAlive(input); + } + + // ===== Scenario 5: Multiple allocations inside dynamic method ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void KeepAliveInDynamic() + { + // Create: void DynAlloc(object a, object b, object c, object d) + DynamicMethod dm = new("DynAlloc", typeof(void), + new[] { typeof(object), typeof(object), typeof(object), typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object a = new object(); + object b = "str"; + object c = new int[] { 1 }; + object d = new byte[16]; + del(a, b, c, d); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + GC.KeepAlive(d); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs new file mode 100644 index 00000000000000..4bd0a12fe6d145 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs @@ -0,0 +1,143 @@ +// 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.Runtime.CompilerServices; + +/// +/// Exercises exception handling: try/catch/finally funclets, nested exceptions, +/// filter funclets, and rethrow. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + object inside = new object(); + ThrowHelper(); + GC.KeepAlive(inside); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowHelper() + { + throw new InvalidOperationException("test exception"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + object c = new object(); + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + finally + { + object d = new object(); + GC.KeepAlive(d); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs new file mode 100644 index 00000000000000..54b7060c040f5a --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs @@ -0,0 +1,81 @@ +// 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.Collections.Generic; +using System.Runtime.CompilerServices; + +/// +/// Exercises generic method instantiations and interface dispatch. +/// +internal static class Program +{ + interface IKeepAlive + { + object GetRef(); + } + + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void GenericScenario() + { + var o = GenericAlloc(); + var l = GenericAlloc>(); + var s = GenericAlloc(); + GC.KeepAlive(o); + GC.KeepAlive(l); + GC.KeepAlive(s); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => + { + GC.KeepAlive(captured); + return new object(); + }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + GenericScenario(); + InterfaceDispatchScenario(); + DelegateScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs new file mode 100644 index 00000000000000..0eea731a6bd313 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs @@ -0,0 +1,53 @@ +// 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.Runtime.CompilerServices; +using System.Threading; + +/// +/// Exercises concurrent threads with GC references, exercising multi-threaded +/// stack walks and GC ref enumeration. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThreadWork(int id) + { + object threadLocal = new object(); + string threadName = $"thread-{id}"; + NestedCall(5); + GC.KeepAlive(threadLocal); + GC.KeepAlive(threadName); + } + + static int Main() + { + for (int iteration = 0; iteration < 2; iteration++) + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + ready.Set(); + go.Wait(); + ThreadWork(1); + }); + t.Start(); + ready.Wait(); + go.Set(); + ThreadWork(0); + t.Join(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs new file mode 100644 index 00000000000000..83aece921baaea --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs @@ -0,0 +1,74 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +/// +/// Exercises P/Invoke transitions with GC references before and after native calls, +/// and pinned GC handles. +/// +internal static class Program +{ + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PInvokeScenario() + { + object before = new object(); + uint tid = GetCurrentThreadId(); + object after = new object(); + GC.KeepAlive(before); + GC.KeepAlive(after); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + struct LargeStruct + { + public object A, B, C, D; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new object(); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + PInvokeScenario(); + PinnedScenario(); + StructWithRefsScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs new file mode 100644 index 00000000000000..9067337495def2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs @@ -0,0 +1,157 @@ +// 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.Runtime.CompilerServices; + +/// +/// Exercises struct-related GC scanning scenarios that stress the MetaSig path: +/// - Value type 'this' (interior pointer for struct instance methods) +/// - Small struct returns (retbuf detection precision) +/// - Struct parameters containing embedded GC references +/// +internal static class Program +{ + static int Main() + { + for (int i = 0; i < 100; i++) + { + ValueTypeThisScenario(); + SmallStructReturnScenario(); + StructWithRefsScenario(); + InterfaceDispatchScenario(); + } + return 100; + } + + // ===== Scenario 1: Value type 'this' ===== + // When a struct instance method is called through interface dispatch, + // 'this' is an interior pointer (pointing into the boxed struct, past + // the MethodTable pointer). The GC needs GC_CALL_INTERIOR to handle it. + + interface IKeepAlive + { + object GetRef(); + } + + struct StructWithRef : IKeepAlive + { + public object Field; + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => Field; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ValueTypeThisScenario() + { + IKeepAlive s = new StructWithRef { Field = new object() }; + object r = s.GetRef(); + GC.KeepAlive(r); + GC.KeepAlive(s); + } + + // ===== Scenario 2: Small struct returns ===== + // Methods returning small structs (1/2/4/8 bytes, power-of-2) do NOT need + // a return buffer on AMD64 Windows — the value is returned in RAX. + // Conservative HasRetBuffArg=true shifts all parameter offsets by 1 slot. + + struct SmallResult + { + public int Value; + } + + struct TinyResult + { + public byte Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static SmallResult MakeSmallResult(object keepAlive) + { + GC.KeepAlive(keepAlive); + return new SmallResult { Value = 42 }; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static TinyResult MakeTinyResult(object keepAlive) + { + GC.KeepAlive(keepAlive); + return new TinyResult { Value = 1 }; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void SmallStructReturnScenario() + { + object live = new object(); + SmallResult sr = MakeSmallResult(live); + TinyResult tr = MakeTinyResult(live); + GC.KeepAlive(sr); + GC.KeepAlive(tr); + GC.KeepAlive(live); + } + + // ===== Scenario 3: Struct parameters with embedded GC refs ===== + // Value type parameters containing object references require GCDesc + // scanning to find the embedded refs. Without this, the refs inside + // the struct are invisible to the GC. + + struct Holder + { + public object Ref1; + public string Ref2; + public int[] Array; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ProcessHolder(Holder h) + { + GC.KeepAlive(h.Ref1); + GC.KeepAlive(h.Ref2); + GC.KeepAlive(h.Array); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + Holder h = new Holder + { + Ref1 = new object(), + Ref2 = "hello", + Array = new int[] { 1, 2, 3 }, + }; + ProcessHolder(h); + GC.KeepAlive(h.Ref1); + } + + // ===== Scenario 4: Interface dispatch with generics ===== + // Shared generic methods going through stub dispatch combine + // RequiresInstArg with value type 'this'. + + interface IGenericOp + { + T Get(); + } + + struct GenericStruct : IGenericOp + { + public T Value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public T Get() => Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IGenericOp g = new GenericStruct { Value = new object() }; + object r = g.Get(); + GC.KeepAlive(r); + GC.KeepAlive(g); + + IGenericOp gs = new GenericStruct { Value = "test" }; + string s = gs.Get(); + GC.KeepAlive(s); + GC.KeepAlive(gs); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj b/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj new file mode 100644 index 00000000000000..d6bd3aa5a13459 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj @@ -0,0 +1,21 @@ + + + true + $(NetCoreAppToolCurrent) + enable + true + + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/StressTests/README.md b/src/native/managed/cdac/tests/StressTests/README.md index c5bcde5675b3f0..97a676edfa2892 100644 --- a/src/native/managed/cdac/tests/StressTests/README.md +++ b/src/native/managed/cdac/tests/StressTests/README.md @@ -1,18 +1,39 @@ # cDAC Stress Tests -This folder contains stress tests that verify the cDAC's stack reference -enumeration against the runtime's GC root scanning. The tests run managed -debuggee applications under `corerun` with cDAC stress flags enabled, -triggering verification at allocation points, GC points, or instruction-level -GC stress points. +Integration tests that verify the cDAC's stack reference enumeration matches the runtime's +GC root scanning under GC stress conditions. -## Quick Start +## How It Works + +Each test runs a debuggee console app under `corerun` with `DOTNET_CdacStress=0x51`, which enables: +- **0x01**: Allocation-point verification (triggers at every managed allocation) +- **0x10**: GC reference comparison (compares cDAC stack refs against runtime refs) +- **0x40**: Legacy DAC comparison (three-way: cDAC vs DAC vs runtime) + +The native `cdacstress.cpp` hook writes structured per-frame comparison results to a log file. +On failure, it shows per-frame diffs with resolved method names, making it easy to identify +which frame and method has mismatched GC references. + +Pass/fail semantics: +- **[PASS]**: cDAC matches DAC (may include `[RT_DIFF]` annotation if RT differs) +- **[FAIL]**: cDAC does NOT match DAC +- **[SKIP]**: cDAC GetStackReferences failed (e.g., during EH) + +## Prerequisites + +Build the runtime with the cDAC stress hook enabled: ```powershell -# Prerequisites: build CoreCLR Checked and generate core_root -# build.cmd clr+libs -rc Checked -lc Release -# src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +# From repo root +.\build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +.\src\tests\build.cmd Checked generatelayoutonly -SkipRestorePackages /p:LibrariesConfiguration=Release +``` +## Running Tests + +### Using RunStressTests.ps1 + +```powershell # Run all debuggees (allocation-point verification, no GCStress) ./RunStressTests.ps1 -SkipBuild @@ -21,88 +42,60 @@ GC stress points. # Run with instruction-level GCStress (slower, more thorough) ./RunStressTests.ps1 -SkipBuild -CdacStress 0x14 -GCStress 0x4 - -# Full comparison including walk parity and DAC cross-check -./RunStressTests.ps1 -SkipBuild -CdacStress 0x74 -GCStress 0x4 ``` -## How It Works - -### DOTNET_CdacStress Flags - -The `DOTNET_CdacStress` environment variable is a bitmask that controls -**where** and **what** the runtime verifies: +### Using dotnet test (xUnit) -| Bit | Flag | Description | -|-----|------|-------------| -| 0x1 | ALLOC | Verify at managed allocation points | -| 0x2 | GC | Verify at GC collection points | -| 0x4 | INSTR | Verify at instruction-level GC stress points (requires `DOTNET_GCStress`) | -| 0x10 | REFS | Compare GC stack references (cDAC vs runtime) | -| 0x20 | WALK | Compare stack walk frame ordering (cDAC vs DAC) | -| 0x40 | USE_DAC | Also compare GC refs against the legacy DAC | -| 0x100 | UNIQUE | Only verify each instruction pointer once | - -Common combinations: -- `0x11` — ALLOC + REFS (fast, default) -- `0x14` — INSTR + REFS (thorough, requires `DOTNET_GCStress=0x4`) -- `0x31` — ALLOC + REFS + WALK (fast with walk parity check) -- `0x74` — INSTR + REFS + WALK + USE_DAC (full comparison) - -### Verification Flow +```powershell +# Build and run all stress tests +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests -At each stress point, the native hook (`cdacstress.cpp`) in the runtime: +# Run a specific debuggee +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests --filter "FullyQualifiedName~BasicAlloc" -1. Suspends the current thread's context -2. Calls the cDAC's `GetStackReferences` to enumerate GC roots -3. Compares against the runtime's own GC root enumeration -4. Optionally compares against the legacy DAC's enumeration -5. Optionally compares stack walk frame ordering -6. Logs `[PASS]` or `[FAIL]` per verification point +# Set CORE_ROOT manually if needed +$env:CORE_ROOT = "path\to\Core_Root" +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests +``` -The script collects these results and reports aggregate pass/fail counts. +## Adding a New Debuggee -## Debuggees +1. Create a folder under `Debuggees/` with a `.csproj` and `Program.cs` +2. The `.csproj` just needs: `` + (inherits OutputType=Exe and TFM from `Directory.Build.props`) +3. `Main()` must return `100` on success +4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` on methods to prevent inlining +5. Use `GC.KeepAlive()` to ensure objects are live at GC stress points +6. Add the debuggee name to `BasicStressTests.Debuggees` -Each debuggee is a standalone console application under `Debuggees/`: +## Debuggee Catalog | Debuggee | Scenarios | |----------|-----------| -| **BasicAlloc** | Object allocation, strings, arrays, many live refs | -| **Comprehensive** | All-in-one: allocations, deep stacks, exceptions, generics, P/Invoke, threading | - -All debuggees return exit code 100 on success. - -### Adding a New Debuggee - -1. Create a new folder under `Debuggees/` (e.g., `Debuggees/MyScenario/`) -2. Add a minimal `.csproj`: - ```xml - - ``` - The `Directory.Build.props` provides all common settings. -3. Add a `Program.cs` with a `Main()` that returns 100 -4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` and `GC.KeepAlive()` - to prevent the JIT from optimizing away allocations and references +| **BasicAlloc** | Objects, strings, arrays, many live refs | +| **ExceptionHandling** | try/catch/finally funclets, nested exceptions, filter funclets, rethrow | +| **DeepStack** | Deep recursion with live refs at each frame | +| **Generics** | Generic method instantiations, interface dispatch, delegates | +| **PInvoke** | P/Invoke transitions, pinned GC handles, struct with object refs | +| **MultiThread** | Concurrent threads with synchronized GC stress | +| **Comprehensive** | All-in-one: every scenario in a single run | +| **StructScenarios** | Struct returns, by-ref params | +| **DynamicMethods** | DynamicMethod / IL emit | + +## Architecture -The script auto-discovers all debuggees by scanning for `.csproj` files. - -## Script Parameters - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `-Configuration` | `Checked` | Runtime build configuration | -| `-CdacStress` | `0x11` | Hex bitmask for `DOTNET_CdacStress` | -| `-GCStress` | _(empty)_ | Hex value for `DOTNET_GCStress` (e.g., `0x4`) | -| `-Debuggee` | _(all)_ | Which debuggee(s) to run | -| `-SkipBuild` | off | Skip CoreCLR/cDAC build step | -| `-SkipBaseline` | off | Skip baseline (no-stress) verification | - -## Expected Results - -Most runs achieve >99.5% pass rate. A small number of failures (~0.2%) -are expected due to the ScanFrameRoots gap — the cDAC does not yet enumerate -GC roots from explicit frame stub data (e.g., `StubDispatchFrame`, -`PInvokeCalliFrame`). These are tracked in [known-issues.md](known-issues.md). - -Walk parity (`WALK` flag) should show 0 mismatches. +``` +CdacStressTestBase.RunGCStress(debuggeeName) + │ + ├── Locate core_root/corerun (CORE_ROOT env or default path) + ├── Locate debuggee DLL (artifacts/bin/StressTests//...) + ├── Start Process: corerun + │ Environment: + │ DOTNET_CdacStress=0x51 + │ DOTNET_CdacStressStep=1 + │ DOTNET_CdacStressLogFile= + │ DOTNET_ContinueOnAssert=1 + ├── Wait for exit (timeout: 300s) + ├── Parse results log → CdacStressResults + └── Assert: exit=100, zero failures +``` diff --git a/src/native/managed/cdac/tests/StressTests/StressTests.targets b/src/native/managed/cdac/tests/StressTests/StressTests.targets new file mode 100644 index 00000000000000..b88b4132e0d4a3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/StressTests.targets @@ -0,0 +1,70 @@ + + + + $(MSBuildThisFileDirectory)Debuggees\ + Release + + + + + + + + + + + + + + + + + + + + + <_HelixTestsDir>$([MSBuild]::NormalizeDirectory('$(HelixPayloadDir)', 'tests')) + <_HelixDebuggeesDir>$([MSBuild]::NormalizeDirectory('$(HelixPayloadDir)', 'debuggees')) + + + + + <_TestOutput Include="$(OutputPath)**\*" /> + + + + + + <_XunitConsoleFiles Include="$([System.IO.Path]::GetDirectoryName('$(XunitConsoleNetCoreAppPath)'))\*" /> + + + + + + + + + + + <_DebuggeeOutputDir>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'StressTests', '$(DebuggeeName)', '$(DebuggeeConfiguration)')) + + + <_DebuggeeFiles Include="$(_DebuggeeOutputDir)**\*" /> + + + + diff --git a/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj new file mode 100644 index 00000000000000..9deecf71c49ca4 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj @@ -0,0 +1,76 @@ + + + + + + msbuild + true + true + true + $(_Creator) + true + $(BUILD_BUILDNUMBER) + test/cdac/stresstests/ + pr/dotnet/runtime/cdac-stress-tests + 00:30:00 + + + + + + %(Identity) + + + + + + + + + + + + + + + @(HelixPreCommand) + + + + + + <_StressTestCommand>%25HELIX_CORRELATION_PAYLOAD%25\dotnet.exe exec --runtimeconfig %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.runtimeconfig.json --depsfile %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.deps.json %25HELIX_WORKITEM_PAYLOAD%25\tests\xunit.console.dll %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.dll -xml testResults.xml -nologo + + + <_StressTestCommand>$HELIX_CORRELATION_PAYLOAD/dotnet exec --runtimeconfig $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.runtimeconfig.json --depsfile $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.deps.json $HELIX_WORKITEM_PAYLOAD/tests/xunit.console.dll $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.dll -xml testResults.xml -nologo + + + + + $(StressTestsPayload) + $(_StressTestCommand) + $(WorkItemTimeout) + + + + + diff --git a/src/native/managed/cdac/tests/StressTests/known-issues.md b/src/native/managed/cdac/tests/StressTests/known-issues.md index 6445d255b67362..51815fca6a5100 100644 --- a/src/native/managed/cdac/tests/StressTests/known-issues.md +++ b/src/native/managed/cdac/tests/StressTests/known-issues.md @@ -1,57 +1,128 @@ # cDAC Stack Reference Walking — Known Issues This document tracks known gaps between the cDAC's stack reference enumeration -and the legacy DAC's `GetStackReferences`. +and the legacy DAC / runtime's GC stack scanning. ## Current Test Results -Using `DOTNET_CdacStress` with cDAC-vs-DAC comparison: - -| Mode | Non-EH debuggees (6) | ExceptionHandling | -|------|-----------------------|-------------------| -| INSTR (0x4 + GCStress=0x4, step=10) | 0 failures | 0-2 failures | -| ALLOC+UNIQUE (0x101) | 0 failures | 4 failures | -| Walk comparison (0x20, IP+SP) | 0 mismatches | N/A | - -## Known Issue: cDAC Cannot Unwind Through Native Frames - -**Severity**: Low — only affects live-process stress testing during active -exception first-pass dispatch. Does not affect dump analysis where the thread -is suspended with a consistent Frame chain. - -**Pattern**: `cDAC < DAC` (cDAC reports 4 refs, DAC reports 10-13). -ExceptionHandling debuggee only, 4 deterministic occurrences per run. - -**Root cause**: The cDAC's `AMD64Unwinder.Unwind` (and equivalents for other -architectures) can only unwind **managed** frames — it checks -`ExecutionManager.GetCodeBlockHandle(IP)` first and returns false if the IP -is not in a managed code range. This means it cannot unwind through native -runtime frames (allocation helpers, EH dispatch code, etc.). - -When the allocation stress point fires during exception first-pass dispatch: - -1. The thread's `m_pFrame` is `FRAME_TOP` (no explicit Frames in the chain - because the InlinedCallFrame/SoftwareExceptionFrame have been popped or - not yet pushed at that point in the EH dispatch sequence) -2. The initial IP is in native code (allocation helper) -3. The cDAC attempts to unwind through native frames but - `GetCodeBlockHandle` returns null for native IPs → unwind fails -4. With no Frames and no ability to unwind, the walk stops early - -The legacy DAC's `DacStackReferenceWalker::WalkStack` succeeds because -`StackWalkFrames` calls `VirtualUnwindToFirstManagedCallFrame` which uses -OS-level unwind (`RtlVirtualUnwind` on Windows, `PAL_VirtualUnwind` on Unix) -that can unwind ANY native frame using PE `.pdata`/`.xdata` sections. - -**Possible fixes**: -1. **Ensure Frames are always available** — change the runtime to keep - an explicit Frame pushed during allocation points within EH dispatch. - The cDAC cannot do OS-level native unwind (it operates on dumps where - `RtlVirtualUnwind` is not available). The Frame chain is the only - mechanism the cDAC has for transitioning through native code to reach - managed frames. If `m_pFrame = FRAME_TOP` when the IP is native, the - cDAC cannot proceed. -2. **Accept as known limitation** — these failures only occur during - live-process stress testing at a narrow window during EH first-pass - dispatch. In dumps, the exception state is frozen and the Frame chain - is consistent. +### Unit tests: 1374/1374 pass + +### ALLOC+WALK+USE_DAC (0x61) — Stack walk frame comparison +**7/7 debuggees: 100% clean (zero WALK_FAIL) when tested** + +### ALLOC+REFS+USE_DAC (0x51) — Three-way GC ref comparison + +| Debuggee | Result | Notes | +|----------|--------|-------| +| BasicAlloc | 0 failures | | +| Comprehensive | 0 failures | | +| DeepStack | 0 failures | | +| Generics | 0 failures | | +| MultiThread | 0 failures | | +| PInvoke | 0 failures | Windows only | +| DynamicMethods | 0 failures | | +| StructScenarios | 0 failures | | +| ExceptionHandling | 0 failures | Fixed via ExecutionAborted | + +## Issue 1: ELEMENT_TYPE_INTERNAL in PromoteCallerStack (instruction-level stress only) + +**Affected**: Explicit Frames whose method signature contains `ELEMENT_TYPE_INTERNAL` (0x21) +**Frequency**: ~3 per 25K verifications (0.01%) +**Root cause**: IDENTIFIED — follow-up fix needed + +**Where it happens**: `FrameIterator.PromoteCallerStack()` in +`src/native/managed/cdac/.../Contracts/StackWalk/FrameHandling/FrameIterator.cs` +(around line 604). This is the fallback path used when a Frame's GCRefMap is +unavailable and we must decode the method signature to determine which caller +arguments are GC references. + +**Pattern**: The DAC reports 1 ref from an explicit Frame that the cDAC fails to scan. +The `PromoteCallerStack` fallback decodes the method signature using +`System.Reflection.Metadata.SignatureDecoder`, which only handles standard ECMA-335 +type codes. Runtime-internal signatures (generated for IL stubs, marshalling stubs, +unsafe accessors, etc.) may contain `ELEMENT_TYPE_INTERNAL` (0x21), which encodes a +raw pointer-sized `TypeHandle` directly in the signature blob. The SRM decoder doesn't +recognize this type code and throws `BadImageFormatException`. + +``` +System.BadImageFormatException: Unexpected SignatureTypeCode: (0x21). + at SignatureDecoder`2.DecodeType(BlobReader&, Boolean, Int32) + at SignatureDecoder`2.DecodeGenericTypeInstance(BlobReader&) + at FrameIterator.PromoteCallerStack(...) + at FrameIterator.GcScanRoots(...) +``` + +The exception is caught by the per-frame exception handler in `WalkStackReferences()` +(`StackWalk_1.cs`, around line 245), which silently swallows it and continues the +walk — causing the Frame's GC refs to be unreported. + +**How the DAC handles it**: The native DAC uses `MetaSig` + `ArgIterator` +(`frames.cpp:1520-1596`) instead of the SRM decoder. `MetaSig` natively understands +`ELEMENT_TYPE_INTERNAL` — it reads the embedded TypeHandle pointer and follows it to +determine the actual type for GC classification. + +**How the Legacy cDAC handles it**: `SigFormat.cs` (line 157-175) already handles +`ELEMENT_TYPE_INTERNAL` by reading the pointer-sized TypeHandle, resolving it via +`RuntimeTypeSystem.GetTypeHandle()`, and checking `GetSignatureCorElementType()`. + +**Current workaround**: A `catch (BadImageFormatException)` in `PromoteCallerStack` +returns without reporting refs for the frame. + +**Follow-up fix**: Replace the SRM `SignatureDecoder` usage with a custom signature +walker that: +1. Pre-processes the signature bytes, handling `ELEMENT_TYPE_INTERNAL` (0x21) by + reading the pointer-sized TypeHandle and resolving through `RuntimeTypeSystem` + (following the pattern in `SigFormat.cs:157-175`) +2. Delegates standard ECMA-335 type codes to the existing `GcSignatureTypeProvider` +3. Handles `ELEMENT_TYPE_CMOD_INTERNAL` (0x22) similarly if encountered + +## IsFirst not preserved for skipped frames (FIXED) + +Previously ~4 per 25K failures at instruction-level stress. The cDAC's +`AdvanceIsFirst` was updating `IsFirst` for `SW_SKIPPED_FRAME` based on the +Frame's resumable attribute, but the native walker does NOT modify `isFirst` +in the `SFITER_SKIPPED_FRAME_FUNCTION` path (stackwalk.cpp:2086-2128). Fixed +by making `AdvanceIsFirst` skip the `IsFirst` update for `SW_SKIPPED_FRAME`. + +## EH ThrowHelper (FIXED) + +Previously 8-9 failures per run. Fixed by detecting `SoftwareExceptionFrame` +and `FaultingExceptionFrame` as interrupted frames and setting `ExecutionAborted` +flag, matching native `CrawlFrame::GetCodeManagerFlags`. + +## Allocation-level stress results + +At allocation-level stress (`DOTNET_CdacStress=0x51`, the default): +- All 9 debuggees pass 100% (0 failures across ~45K total verifications) + +## Instruction-level stress results + +At instruction-level stress (`DOTNET_GCStress=0x4 + DOTNET_CdacStress=0x54`): +- Comprehensive: 25,512 pass / 3 fail (99.988%) + - 0 FRAME_DIFF (fixed via IsFirst skipped-frame preservation) + - 3 FRAME_DAC_ONLY (ELEMENT_TYPE_INTERNAL in PromoteCallerStack — follow-up) + +## Future work + +- Investigate the GcInfo safe-point bitmap decoding difference for QCall frames +- Replace `fprintf`-based stress logging in `cdacstress.cpp` with a more + structured mechanism (e.g., ETW events or StressLog) for better tooling + integration and reduced I/O overhead during stress runs. + +## Log Format + +The stress log uses structured per-frame output with method name resolution: + +``` +[PASS] Thread=0x... IP=0x... cDAC=N DAC=N RT=M +[FAIL] Thread=0x... IP=0x... cDAC=N DAC=M RT=M + [COMPARE cDAC-vs-DAC] + [FRAME_DIFF] Source=0x... (MethodName): cDAC=X DAC=Y + [cDAC_ONLY] Addr=0x... Obj=0x... Flags=0x... + [DAC_ONLY] Addr=0x... Obj=0x... Flags=0x... + [FRAME_cDAC_ONLY] Source=0x... (MethodName): cDAC=X + [FRAME_DAC_ONLY] Source=0x... (): DAC=Y + [RT_DIFF] cDAC=N RT=M (cDAC matches DAC but differs from RT) + [STACK_TRACE] (cDAC=N DAC=M RT=M) + #i MethodName (cDAC=X DAC=Y) [<-- MISMATCH] +```