[WIP] [cDAC] Adding symbols and using heap dumps#126385
[WIP] [cDAC] Adding symbols and using heap dumps#126385rcj1 wants to merge 6 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the cDAC DumpTests infrastructure to prefer heap dumps (instead of full dumps) and makes dump analysis more reliable by providing ClrMD with local symbol/module search paths (including a self-contained symbol layout for Helix runs).
Changes:
- Switch DumpTests to default to heap dumps (removing per-test
DumpType => "full"overrides) and update debuggee projects to generate heap dumps. - Add symbol/module path discovery in
DumpTestBaseand plumb these paths intoClrMdDumpHost.Open. - Update Helix dump-generation to copy
System.Private.CoreLib.dlland debuggee DLLs into asymbols/folder alongside dumps so analysis can resolve modules without the original testhost layout.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/native/managed/cdac/tests/DumpTests/SyncBlockDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/RCWCleanupListDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/LoaderDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/IXCLRDataValueDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/IXCLRDataFrameDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/IXCLRDataAppDomainDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/ISOSDacInterface13Tests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/ExceptionHandlingInfoDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/EcmaMetadataDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs | Passes additional symbol paths to ClrMD and adds logic to discover symbol/module directories (Helix symbols/ and local artifacts fallback). |
| src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs | Extends dump opening to accept additional symbol paths and configures ClrMD symbol search paths. |
| src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj | Copies CoreLib + debuggee DLLs into a symbols/ tree next to dumps to enable symbol/module resolution on Helix machines. |
| src/native/managed/cdac/tests/DumpTests/ComWrappersDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/CCWDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/AsyncContinuationDumpTests.cs | Removes full-dump override so the test uses default heap dump behavior. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/SyncBlock/SyncBlock.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/StackWalk/StackWalk.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/RCWCleanupList/RCWCleanupList.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/RCW.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/MultiModule.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/LocalVariables/LocalVariables.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/ExceptionHandlingInfo/ExceptionHandlingInfo.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/ComWrappers/ComWrappers.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/CCW/CCW.csproj | Switches debuggee dump generation to heap dumps. |
| src/native/managed/cdac/tests/DumpTests/Debuggees/AsyncContinuation/AsyncContinuation.csproj | Switches debuggee dump generation to heap dumps (and removes prior “full dump needed” comment). |
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
| } | ||
|
|
||
| _host = ClrMdDumpHost.Open(dumpPath); | ||
| _host = ClrMdDumpHost.Open(dumpPath, GetSymbolPaths(debuggeeName, versionDir)); | ||
| ulong contractDescriptor = _host.FindContractDescriptorAddress(); | ||
|
|
There was a problem hiding this comment.
GetSymbolPaths can legitimately return an empty list (e.g., if symbols/ isn't present and the test isn't running from a repo layout). Passing an empty list into ClrMdDumpHost.Open currently results in SetSymbolPath("") (clearing defaults). Consider passing null when no paths are found, or having GetSymbolPaths return null/empty and letting Open skip SetSymbolPath in that case.
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
Switching this debuggee to DumpTypes=Heap means no dumps will be generated for net10.0 (DumpTests.targets explicitly skips heap dumps for net10.0, and Full is no longer requested). Any tests that still intend to run against net10.0 for this debuggee will now be skipped due to missing dumps. If cross-version coverage is still desired, consider keeping Full (or Heap;Full) or adding logic to generate/consume Full dumps for net10 only.
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Heap;Full</DumpTypes> |
| <PropertyGroup> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> | ||
| </PropertyGroup> |
There was a problem hiding this comment.
Switching this debuggee to DumpTypes=Heap means no dumps will be generated for net10.0 (DumpTests.targets skips heap dumps for net10.0, and Full is no longer requested). Tests using this debuggee will therefore never exercise net10.0 unless they explicitly switch back to Full for that version.
| </PropertyGroup> | |
| </PropertyGroup> | |
| <PropertyGroup Condition="'$(TargetFramework)' == 'net10.0'"> | |
| <DumpTypes>Full</DumpTypes> | |
| </PropertyGroup> |
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
Switching this debuggee to DumpTypes=Heap means no dumps will be generated for net10.0 (DumpTests.targets skips heap dumps for net10.0, and Full is no longer requested). If the COM dump tests are expected to run cross-version, this will remove net10.0 coverage.
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Full;Heap</DumpTypes> |
| <!-- This debuggee is intentionally Windows-only (COM interop). --> | ||
| <NoWarn>$(NoWarn);CA1416</NoWarn> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
Switching this debuggee to DumpTypes=Heap means no dumps will be generated for net10.0 (DumpTests.targets skips heap dumps for net10.0, and Full is no longer requested). If RCW tests are expected to run against net10.0 using full dumps, this change will prevent that.
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Full</DumpTypes> |
| <!-- This debuggee is intentionally Windows-only (COM interop). --> | ||
| <NoWarn>$(NoWarn);CA1416</NoWarn> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
Switching this debuggee to DumpTypes=Heap means no dumps will be generated for net10.0 (DumpTests.targets skips heap dumps for net10.0, and Full is no longer requested). If RCW cleanup list tests are expected to run against net10.0 using full dumps, this change will prevent that.
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Heap;Full</DumpTypes> |
| int filled = 0; | ||
| ulong current = address; |
There was a problem hiding this comment.
ReadFromTarget retries by reading from a PE on disk when DataReader.Read returns a short read, but it ignores bytesRead and overwrites the entire buffer from the PE image. This can clobber valid bytes already read from the dump and also loses the opportunity to only backfill the missing tail. Preserve the initial bytesRead and only fill the remaining portion starting at address + bytesRead.
| int filled = 0; | |
| ulong current = address; | |
| int filled = bytesRead; | |
| ulong current = address + (ulong)bytesRead; |
| ulong current = address; | ||
| while (filled < buffer.Length) | ||
| { | ||
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); |
There was a problem hiding this comment.
ReadFromTarget casts (current - info.ImageBase) to int for PEReader.GetSectionData. This can overflow for large RVAs and will produce incorrect reads or exceptions. Add a bounds check (e.g., > int.MaxValue) and fail the PE fallback if it doesn’t fit.
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); | |
| if (current < info.ImageBase) | |
| return -1; | |
| ulong rva = current - info.ImageBase; | |
| if (rva > int.MaxValue) | |
| return -1; | |
| PEMemoryBlock block = peReader.GetSectionData((int)rva); |
| if (info is null || info.FileName is null) | ||
| return -1; | ||
|
|
||
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: false); |
There was a problem hiding this comment.
FindPEImage is called with checkProperties:false. If multiple copies of the same module name exist in the symbol path (very likely with the helix copy step), this can return the wrong PE and cause incorrect metadata reads. Prefer enabling property checks so the located PE matches the dump module’s timestamp/file size.
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: false); | |
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: true); |
| Include="mkdir -p $HELIX_WORKITEM_PAYLOAD/dumps/local/symbols/runtime" /> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" | ||
| Include="cp $HELIX_CORRELATION_PAYLOAD/shared/Microsoft.NETCore.App/*/System.Private.CoreLib.dll $HELIX_WORKITEM_PAYLOAD/dumps/local/symbols/runtime/" /> | ||
| <!-- Unix: copy each debuggee DLL --> |
There was a problem hiding this comment.
On Unix, cp $HELIX_CORRELATION_PAYLOAD/shared/Microsoft.NETCore.App/*/System.Private.CoreLib.dll ... can match multiple versions and the last one copied wins, which can lead to mismatched metadata at test time. Consider copying only the specific shared framework version used by the work item (or preserve per-version subdirectories) to avoid accidental mismatches.
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
DumpTests.targets explicitly skips generating heap dumps for net10.0. With DumpTypes set to Heap only, this debuggee will produce no net10.0 dumps. If cross-version coverage is desired, consider Heap;Full so net10.0 can still generate a full dump.
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Heap;Full</DumpTypes> |
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
DumpTests.targets explicitly skips generating heap dumps for net10.0. With DumpTypes set to Heap only, this debuggee will produce no net10.0 dumps. If these tests are meant to validate behavior across released runtimes, consider Heap;Full so net10.0 still gets a full dump.
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Heap;Full</DumpTypes> |
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
DumpTests.targets explicitly skips generating heap dumps for net10.0. With DumpTypes set to Heap only, this debuggee will produce no net10.0 dumps. If cross-version validation is desired for this scenario, consider Heap;Full so net10.0 still gets a full dump.
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Heap;Full</DumpTypes> |
| <NoWarn>$(NoWarn);CA2007;CA2252</NoWarn> | ||
| <!-- Full dump needed so that module metadata is available for type lookup --> | ||
| <DumpTypes>Full</DumpTypes> | ||
| <DumpTypes>Heap</DumpTypes> |
There was a problem hiding this comment.
DumpTests.targets explicitly skips generating heap dumps for net10.0. With DumpTypes set to Heap only, this debuggee will produce no net10.0 dumps. If the async continuation scenario is expected to run against net10.0, consider Heap;Full (heap for local, full for net10.0).
| <DumpTypes>Heap</DumpTypes> | |
| <DumpTypes>Heap;Full</DumpTypes> |
| using FileStream fs = File.OpenRead(foundFile); | ||
| using PEReader peReader = new PEReader(fs); | ||
|
|
There was a problem hiding this comment.
The PE fallback opens the located module file and constructs a PEReader on every short read. If many reads hit module-backed ranges (common when heap dumps omit module images), this can add significant I/O overhead. Consider caching the resolved PE path/PEReader (or the relevant section bytes) per ModuleInfo to avoid repeated file opens during a single test run.
| // If we couldn't read the full buffer, maybe it's in a PE image | ||
| ModuleInfo? info = GetModuleForAddress(address); | ||
| if (info is null || info.FileName is null) | ||
| return -1; | ||
|
|
||
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: false); | ||
| if (foundFile is null) | ||
| return -1; | ||
|
|
||
| using FileStream fs = File.OpenRead(foundFile); | ||
| using PEReader peReader = new PEReader(fs); | ||
|
|
||
| int filled = 0; | ||
| ulong current = address; | ||
| while (filled < buffer.Length) | ||
| { | ||
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); | ||
| if (block.Length == 0) | ||
| return -1; | ||
|
|
||
| int toCopy = Math.Min(block.Length, buffer.Length - filled); | ||
| unsafe | ||
| { | ||
| new ReadOnlySpan<byte>(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); | ||
| } | ||
| filled += toCopy; | ||
| current += (ulong)toCopy; | ||
| } |
There was a problem hiding this comment.
The PE-file fallback currently applies to any module range (including native modules like coreclr). For native modules, reading from the on-disk image can silently return incorrect data for writable sections (.data/.bss) that differ from the in-memory state, which can lead to incorrect cDAC reads instead of a clean failure. Consider restricting this fallback to managed PE images (e.g., only when the located file has managed metadata) or to sections that are safe to source from disk, and otherwise return failure.
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: false); | ||
| if (foundFile is null) | ||
| return -1; | ||
|
|
||
| using FileStream fs = File.OpenRead(foundFile); | ||
| using PEReader peReader = new PEReader(fs); | ||
|
|
||
| int filled = 0; | ||
| ulong current = address; | ||
| while (filled < buffer.Length) | ||
| { | ||
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); | ||
| if (block.Length == 0) | ||
| return -1; | ||
|
|
||
| int toCopy = Math.Min(block.Length, buffer.Length - filled); | ||
| unsafe | ||
| { | ||
| new ReadOnlySpan<byte>(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); | ||
| } | ||
| filled += toCopy; | ||
| current += (ulong)toCopy; | ||
| } |
There was a problem hiding this comment.
The PEReader-based fallback has several potential throw points (FindPEImage result, File.OpenRead, PEReader.GetSectionData with an out-of-range/overflowed RVA cast, etc.). Right now any of these exceptions will bubble out of ReadFromTarget and fail the test run unexpectedly. Consider validating (address - info.ImageBase) fits in int, and wrapping the fallback path in exception handling that returns a failure code when the PE image can't be used.
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: false); | |
| if (foundFile is null) | |
| return -1; | |
| using FileStream fs = File.OpenRead(foundFile); | |
| using PEReader peReader = new PEReader(fs); | |
| int filled = 0; | |
| ulong current = address; | |
| while (filled < buffer.Length) | |
| { | |
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); | |
| if (block.Length == 0) | |
| return -1; | |
| int toCopy = Math.Min(block.Length, buffer.Length - filled); | |
| unsafe | |
| { | |
| new ReadOnlySpan<byte>(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); | |
| } | |
| filled += toCopy; | |
| current += (ulong)toCopy; | |
| } | |
| try | |
| { | |
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: false); | |
| if (foundFile is null) | |
| return -1; | |
| using FileStream fs = File.OpenRead(foundFile); | |
| using PEReader peReader = new PEReader(fs); | |
| int filled = 0; | |
| ulong current = address; | |
| while (filled < buffer.Length) | |
| { | |
| if (current < info.ImageBase) | |
| return -1; | |
| ulong relativeAddress = current - info.ImageBase; | |
| if (relativeAddress > int.MaxValue) | |
| return -1; | |
| PEMemoryBlock block = peReader.GetSectionData((int)relativeAddress); | |
| if (block.Length == 0) | |
| return -1; | |
| int toCopy = Math.Min(block.Length, buffer.Length - filled); | |
| unsafe | |
| { | |
| new ReadOnlySpan<byte>(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); | |
| } | |
| filled += toCopy; | |
| current += (ulong)toCopy; | |
| } | |
| } | |
| catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or BadImageFormatException or InvalidOperationException or ArgumentException or NotSupportedException) | |
| { | |
| return -1; | |
| } |
| private ModuleInfo? GetModuleForAddress(ulong address) | ||
| { | ||
| foreach (ModuleInfo module in _dataTarget.DataReader.EnumerateModules()) | ||
| { | ||
| if (address >= module.ImageBase && address < module.ImageBase + module.ImageSize) | ||
| return module; | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
GetModuleForAddress enumerates all modules on every failed read. With heap dumps, short reads may be common, which can make this a hot path and add significant overhead. Consider caching the module list (and their ranges) once per dump, and doing a faster lookup (e.g., a sorted range list / binary search) instead of re-enumerating each time.
| artifacts/dumps/cdac/ | ||
| local/ | ||
| dump-info.json | ||
| heap/ | ||
| BasicThreads/BasicThreads.dmp | ||
| full/ | ||
| StackWalk/StackWalk.dmp | ||
| PInvokeStub/PInvokeStub.dmp | ||
| net10.0/ | ||
| dump-info.json | ||
| full/ | ||
| heap/ | ||
| TypeHierarchy/TypeHierarchy.dmp | ||
| ``` |
There was a problem hiding this comment.
The directory layout example still omits the per-R2R-mode subdirectory (r2r/jit), but DumpTestBase and DumpTests.targets construct paths as {version}/{dumpType}/{r2rMode}/{debuggee}/{debuggee}.dmp. Updating the example to include the r2r/jit folder would avoid confusion when locating dumps on disk.
| <ItemGroup> | ||
| <!-- Windows: copy SPC from testhost shared framework --> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" | ||
| Include="echo === Copying symbols ===" /> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" | ||
| Include="mkdir %25HELIX_WORKITEM_PAYLOAD%25\dumps\local\symbols\runtime" /> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" | ||
| Include="for /D %25%25v in (%25HELIX_CORRELATION_PAYLOAD%25\shared\Microsoft.NETCore.App\*) do copy "%25%25v\System.Private.CoreLib.dll" "%25HELIX_WORKITEM_PAYLOAD%25\dumps\local\symbols\runtime\"" /> | ||
| <!-- Windows: copy each debuggee DLL --> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" | ||
| Include="@(_UniqueDebuggee->'mkdir %25HELIX_WORKITEM_PAYLOAD%25\dumps\local\symbols\debuggees\%(Identity)')" /> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" | ||
| Include="@(_UniqueDebuggee->'copy "%25HELIX_WORKITEM_PAYLOAD%25\debuggees\%(Identity)\%(Identity).dll" "%25HELIX_WORKITEM_PAYLOAD%25\dumps\local\symbols\debuggees\%(Identity)\"')" /> | ||
|
|
||
| <!-- Unix: copy SPC from testhost shared framework --> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" | ||
| Include="echo '=== Copying symbols ==='" /> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" | ||
| Include="mkdir -p $HELIX_WORKITEM_PAYLOAD/dumps/local/symbols/runtime" /> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" | ||
| Include="cp $HELIX_CORRELATION_PAYLOAD/shared/Microsoft.NETCore.App/*/System.Private.CoreLib.dll $HELIX_WORKITEM_PAYLOAD/dumps/local/symbols/runtime/" /> | ||
| <!-- Unix: copy each debuggee DLL --> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" | ||
| Include="@(_UniqueDebuggee->'mkdir -p $HELIX_WORKITEM_PAYLOAD/dumps/local/symbols/debuggees/%(Identity)')" /> | ||
| <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" | ||
| Include="@(_UniqueDebuggee->'cp $HELIX_WORKITEM_PAYLOAD/debuggees/%(Identity)/%(Identity).dll $HELIX_WORKITEM_PAYLOAD/dumps/local/symbols/debuggees/%(Identity)/')" /> |
There was a problem hiding this comment.
The new symbol-copy commands assume the shared framework layout/globs always match. On Unix, cp $HELIX_CORRELATION_PAYLOAD/shared/Microsoft.NETCore.App/*/System.Private.CoreLib.dll ... will fail the command if the glob doesn't match (or if multiple matches behave unexpectedly), and on Windows the for /D ... copy will emit errors for directories missing the DLL. Consider adding existence checks / tolerant copy (e.g., conditional copy or || true) so missing/unexpected payload layouts don't cause the Helix work item to fail before tests run.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/native/managed/cdac/tests/DumpTests/LoaderDumpTests.cs:24
- With the
DumpTypeoverride removed, this test class now always uses the defaultheapdumps. Since heap dumps are explicitly skipped / not generated fornet10.0, any Loader tests without[SkipOnVersion("net10.0", ...)](e.g.,Loader_CanGetRootAssembly,Loader_AppDomainHasFriendlyName,Loader_GlobalLoaderAllocatorIsValid) will no longer run againstnet10.0. If cross-version coverage is still intended, consider conditionally selectingfullfornet10.0(or generating both dump types).
public class LoaderDumpTests : DumpTestBase
{
protected override string DebuggeeName => "MultiModule";
[ConditionalTheory]
[MemberData(nameof(TestConfigurations))]
public void Loader_CanGetRootAssembly(TestConfiguration config)
{
InitializeDumpTest(config);
ILoader loader = Target.Contracts.Loader;
| public int ReadFromTarget(ulong address, Span<byte> buffer) | ||
| { | ||
| int bytesRead = _dataTarget.DataReader.Read(address, buffer); | ||
| return bytesRead == buffer.Length ? 0 : -1; | ||
| if (bytesRead == buffer.Length) | ||
| return 0; // success | ||
|
|
||
| // If we couldn't read the full buffer, maybe it's in a PE image | ||
| ModuleInfo? info = GetModuleForAddress(address); | ||
| if (info is null || info.FileName is null) | ||
| { | ||
| Console.WriteLine($"Failed to find module for {buffer.Length} at 0x{address:x8}."); | ||
| return -1; | ||
| } | ||
|
|
||
| string? foundFile = _dataTarget.FileLocator?.FindPEImage(info.FileName, info.IndexTimeStamp, info.IndexFileSize, checkProperties: false); | ||
| if (foundFile is null) | ||
| { | ||
| Console.WriteLine($"Failed to find {info.FileName}"); | ||
| return -1; | ||
| } | ||
|
|
||
| using FileStream fs = File.OpenRead(foundFile); | ||
| using PEReader peReader = new PEReader(fs); | ||
|
|
||
| int filled = 0; | ||
| ulong current = address; | ||
| while (filled < buffer.Length) | ||
| { | ||
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); | ||
| if (block.Length == 0) | ||
| { | ||
| Console.WriteLine($"Failed to read {foundFile} at RVA 0x{(current-info.ImageBase):x8}."); | ||
| return -1; | ||
| } | ||
|
|
||
| int toCopy = Math.Min(block.Length, buffer.Length - filled); | ||
| unsafe | ||
| { | ||
| new ReadOnlySpan<byte>(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); | ||
| } | ||
| filled += toCopy; | ||
| current += (ulong)toCopy; | ||
| } | ||
|
|
||
| return 0; | ||
| } |
There was a problem hiding this comment.
ReadFromTarget overwrites the entire buffer from the PE image when the dump read is short, but DataReader.Read can return a partial read (non-zero) rather than an all-or-nothing result. In that case, this code discards valid bytes already read from the dump and may return incorrect data to the cDAC reader. Consider only falling back to PE data when bytesRead == 0, or preserve the first bytesRead bytes and fill only the remaining range from the PE image (and ensure the request doesn’t cross the module boundary).
| // If we couldn't read the full buffer, maybe it's in a PE image | ||
| ModuleInfo? info = GetModuleForAddress(address); | ||
| if (info is null || info.FileName is null) | ||
| { | ||
| Console.WriteLine($"Failed to find module for {buffer.Length} at 0x{address:x8}."); | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
The failure log uses 0x{address:x8}, which truncates addresses on 64-bit targets and can be misleading when diagnosing dump read failures. Consider formatting based on pointer size (e.g., x16 for 64-bit) or using a helper that prints the full ulong address consistently.
we don't need the filelocator
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; |
There was a problem hiding this comment.
using System.Linq; appears unused in this file (e.g., additionalSymbolPaths.ToArray() calls List<T>.ToArray, not LINQ). If warnings are treated as errors (CS8019), this will break the build—please remove the unused using (or replace code with a LINQ call if intended).
| using System.Linq; |
| public int ReadFromTarget(ulong address, Span<byte> buffer) | ||
| { | ||
| int bytesRead = _dataTarget.DataReader.Read(address, buffer); | ||
| return bytesRead == buffer.Length ? 0 : -1; | ||
| if (bytesRead == buffer.Length) | ||
| return 0; // success | ||
|
|
||
| // If we couldn't read the full buffer, maybe it's in a PE image | ||
| ModuleInfo? info = GetModuleForAddress(address); | ||
| if (info is null || info.FileName is null) |
There was a problem hiding this comment.
When DataReader.Read returns a partial read (bytesRead > 0 but < buffer.Length), the fallback path re-reads from the module file starting at the original address and overwrites the already-read bytes. This can corrupt results when the dump contains valid in-memory bytes for the first part (e.g., relocations/patching) but not the rest. Consider preserving the initial bytesRead bytes and only filling the remaining tail from disk (advance current/filled accordingly).
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); | ||
| if (block.Length == 0) | ||
| { | ||
| Console.WriteLine($"Failed to read {foundFile} at RVA 0x{(current-info.ImageBase):x8}."); |
There was a problem hiding this comment.
peReader.GetSectionData((int)(current - info.ImageBase)) truncates a 64-bit RVA to int. If the requested offset exceeds int.MaxValue (or if current < ImageBase due to unexpected module selection), this will overflow and can produce incorrect reads or exceptions. Please validate the range before casting and fail gracefully (or otherwise constrain reads to supported RVA sizes).
| PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); | |
| if (block.Length == 0) | |
| { | |
| Console.WriteLine($"Failed to read {foundFile} at RVA 0x{(current-info.ImageBase):x8}."); | |
| if (current < info.ImageBase) | |
| { | |
| Console.WriteLine($"Failed to read {foundFile}: address 0x{current:x8} is below module base 0x{info.ImageBase:x8}."); | |
| return -1; | |
| } | |
| ulong rva = current - info.ImageBase; | |
| if (rva > int.MaxValue) | |
| { | |
| Console.WriteLine($"Failed to read {foundFile}: RVA 0x{rva:x8} exceeds supported PEReader range."); | |
| return -1; | |
| } | |
| PEMemoryBlock block = peReader.GetSectionData((int)rva); | |
| if (block.Length == 0) | |
| { | |
| Console.WriteLine($"Failed to read {foundFile} at RVA 0x{rva:x8}."); |
| | ServerGC | Server GC mode heap structures | Heap | | ||
| | StackWalk | Deterministic call stack (Main→A→B→C→FailFast) | Full | | ||
| | MultiModule | Multi-assembly metadata resolution | Full | | ||
| | StackWalk | Deterministic call stack (Main→A→B→C→FailFast) | Heap | | ||
| | MultiModule | Multi-assembly metadata resolution | Heap | | ||
| | TypeHierarchy | Type inheritance, method tables | Heap | | ||
| | PInvokeStub | P/Invoke with SetLastError ILStub | Full | | ||
| | VarargPInvoke | Vararg P/Invoke via __arglist (sprintf) | Full | |
There was a problem hiding this comment.
This README now marks StackWalk/MultiModule (and others below) as using Heap dumps, but the later “.NET 10 did not support cDAC in heap dumps… Only full dump tests (RuntimeTypeSystem, Loader, EcmaMetadata) run against net10.0.” note appears inconsistent with these changes (those tests/debuggees are no longer described as Full). Please update that note to reflect which tests still run for net10.0 after switching these debuggees to heap dumps.
For cDAC dump testing, we need metadata for our locally built System.Private.CoreLib for reading certain statics etc., as well as for debuggees for metadata tests. So that we can use heap dumps instead of full dumps, we add the symbol paths when we open the dump in ClrMD.