From bb55a99d4917d1d4528652f43e35ef38a6fff02d Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Fri, 20 Feb 2026 14:45:12 +0000 Subject: [PATCH 1/4] Add cDAC tests and implementation for GetGenerationTable and GetFinalizationFillPointers Implement the four ISOSDacInterface8 methods in cDAC SOSDacImpl: - GetGenerationTable / GetGenerationTableSvr - GetFinalizationFillPointers / GetFinalizationFillPointersSvr Add test infrastructure: - TestPlaceholderTarget.Builder: fluent builder that owns MockMemorySpace, accumulates types/globals, and wires contracts via TestContractRegistry. - GCHeapBuilder + extension methods (AddGCHeapWks/AddGCHeapSvr): configure GC mock data via Action and build directly into the target. - TestContractRegistry: Dictionary> replacement for Mock in the builder path. Add tests: - 3 contract-level tests in GCTests.cs (x4 arch = 12) - 6 SOSDacImpl-level tests in SOSDacInterface8Tests.cs (x4 arch = 24) Add documentation: - README.md files for cdac/, Legacy/, and tests/ directories - Copilot instruction to search for READMEs along the path hierarchy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 + .../README.md | 82 ++++ .../SOSDacImpl.cs | 184 +++++++- src/native/managed/cdac/README.md | 123 +++++ src/native/managed/cdac/tests/GCTests.cs | 111 +++++ .../MockDescriptors/MockDescriptors.GC.cs | 427 ++++++++++++++++++ src/native/managed/cdac/tests/README.md | 136 ++++++ .../cdac/tests/SOSDacInterface8Tests.cs | 240 ++++++++++ .../cdac/tests/TestPlaceholderTarget.cs | 84 +++- 9 files changed, 1381 insertions(+), 8 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md create mode 100644 src/native/managed/cdac/README.md create mode 100644 src/native/managed/cdac/tests/GCTests.cs create mode 100644 src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs create mode 100644 src/native/managed/cdac/tests/README.md create mode 100644 src/native/managed/cdac/tests/SOSDacInterface8Tests.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 11998dfe324006..77c82967694d33 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,6 +10,8 @@ If you make code changes, do not complete without checking the relevant code bui Before completing, use the `code-review` skill to review your code changes. Any issues flagged as errors or warnings should be addressed before completing. +Before making changes to a directory, search for `README.md` files in that directory and its parent directories up to the repository root. Read any you find — they contain conventions, patterns, and architectural context relevant to your work. + If the changes are intended to improve performance, or if they could negatively impact performance, use the `performance-benchmark` skill to validate the impact before completing. You MUST follow all code-formatting and naming conventions defined in [`.editorconfig`](/.editorconfig). diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md new file mode 100644 index 00000000000000..30d1664c82cdc5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md @@ -0,0 +1,82 @@ +# Microsoft.Diagnostics.DataContractReader.Legacy + +This project contains `SOSDacImpl`, which implements the `ISOSDacInterface*` and +`IXCLRDataProcess` COM-style APIs by delegating to the cDAC contract layer. + +## Implementing a new SOSDacImpl method + +When a method currently delegates to `_legacyImpl` (returning `E_NOTIMPL` when null), +replace it with a cDAC implementation following this pattern: + +```csharp +int ISOSDacInterface8.ExampleMethod(uint* pResult) +{ + // 1. Validate pointer arguments before the try block + if (pResult == null) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + // 2. Get the relevant contract and call it + IGC gc = _target.Contracts.GC; + *pResult = gc.SomeMethod(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // 3. Cross-validate with legacy DAC in debug builds +#if DEBUG + if (_legacyImpl8 is not null) + { + uint resultLocal; + int hrLocal = _legacyImpl8.ExampleMethod(&resultLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (hr == HResults.S_OK) + { + Debug.Assert(*pResult == resultLocal); + } + } +#endif + return hr; +} +``` + +### Key conventions + +- **HResult returns**: Methods return `int` HResult codes, not exceptions. + Use `HResults.S_OK`, `HResults.S_FALSE`, `HResults.E_INVALIDARG`, etc. +- **Null pointer checks**: Validate output pointer arguments *before* the try block + and return `E_INVALIDARG`. This matches the native DAC behavior. +- **Exception handling**: Wrap all contract calls in try/catch. The catch converts + exceptions to HResult codes via `ex.HResult`. +- **Debug cross-validation**: In `#if DEBUG`, call the legacy implementation (if + available) and assert the results match. This catches discrepancies during testing. + +### Sized-buffer protocol + +Several `ISOSDacInterface8` methods use a two-call pattern where the caller first +queries the needed buffer size, then calls again with a sufficiently large buffer: + +```csharp +int GetSomeTable(uint count, Data* buffer, uint* pNeeded) +``` + +The protocol is: +1. Always set `*pNeeded` to the required count (if `pNeeded` is not null). +2. If `count > 0 && buffer == null`: return `E_INVALIDARG`. +3. If `count < needed`: return `S_FALSE` (buffer too small, but `*pNeeded` is set). +4. If `count >= needed`: populate `buffer` and return `S_OK`. + +This matches the native implementation in `src/coreclr/debug/daccess/request.cpp`. + +### Pointer conversions + +- `TargetPointer` → `ClrDataAddress`: use `pointer.ToClrDataAddress(_target)`. + On 32-bit targets, this **sign-extends** the value (e.g., `0xAA000000` becomes + `0xFFFFFFFF_AA000000`). This matches native DAC behavior. +- `ClrDataAddress` → `TargetPointer`: use `address.ToTargetPointer(_target)`. + +Both are defined in `ConversionExtensions.cs`. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index c665f8f717dad8..681ded79e90bc2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4155,17 +4155,189 @@ int ISOSDacInterface8.GetNumberGenerations(uint* pGenerations) return hr; } - // WKS int ISOSDacInterface8.GetGenerationTable(uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetGenerationTable(cGenerations, pGenerationData, pNeeded) : HResults.E_NOTIMPL; + { + if (cGenerations > 0 && pGenerationData == null) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + uint totalGenerationCount = _target.ReadGlobal(Constants.Globals.TotalGenerationCount); + + if (pNeeded != null) + *pNeeded = totalGenerationCount; + + if (cGenerations < totalGenerationCount) + { + hr = HResults.S_FALSE; + } + else + { + GCHeapData heapData = gc.GetHeapData(); + DacpGenerationData* genData = (DacpGenerationData*)pGenerationData; + + for (int i = 0; i < (int)totalGenerationCount && i < heapData.GenerationTable.Count; i++) + { + GCGenerationData gen = heapData.GenerationTable[i]; + genData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); + genData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); + genData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); + genData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetGenerationTable(cGenerations, pGenerationData, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + } +#endif + return hr; + } + int ISOSDacInterface8.GetFinalizationFillPointers(uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetFinalizationFillPointers(cFillPointers, pFinalizationFillPointers, pNeeded) : HResults.E_NOTIMPL; + { + if (cFillPointers > 0 && pFinalizationFillPointers == null) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + GCHeapData heapData = gc.GetHeapData(); + uint numFillPointers = (uint)heapData.FillPointers.Count; + + if (pNeeded != null) + *pNeeded = numFillPointers; + + if (cFillPointers < numFillPointers) + { + hr = HResults.S_FALSE; + } + else + { + for (int i = 0; i < (int)numFillPointers; i++) + { + pFinalizationFillPointers[i] = heapData.FillPointers[i].ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetFinalizationFillPointers(cFillPointers, pFinalizationFillPointers, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + } +#endif + return hr; + } - // SVR int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetGenerationTableSvr(heapAddr, cGenerations, pGenerationData, pNeeded) : HResults.E_NOTIMPL; + { + if (heapAddr == 0 || (cGenerations > 0 && pGenerationData == null)) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + uint totalGenerationCount = _target.ReadGlobal(Constants.Globals.TotalGenerationCount); + + if (pNeeded != null) + *pNeeded = totalGenerationCount; + + if (cGenerations < totalGenerationCount) + { + hr = HResults.S_FALSE; + } + else + { + GCHeapData heapData = gc.GetHeapData(heapAddr.ToTargetPointer(_target)); + DacpGenerationData* genData = (DacpGenerationData*)pGenerationData; + + for (int i = 0; i < (int)totalGenerationCount && i < heapData.GenerationTable.Count; i++) + { + GCGenerationData gen = heapData.GenerationTable[i]; + genData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); + genData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); + genData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); + genData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetGenerationTableSvr(heapAddr, cGenerations, pGenerationData, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + } +#endif + return hr; + } + int ISOSDacInterface8.GetFinalizationFillPointersSvr(ClrDataAddress heapAddr, uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded) - => _legacyImpl8 is not null ? _legacyImpl8.GetFinalizationFillPointersSvr(heapAddr, cFillPointers, pFinalizationFillPointers, pNeeded) : HResults.E_NOTIMPL; + { + if (heapAddr == 0 || (cFillPointers > 0 && pFinalizationFillPointers == null)) + return HResults.E_INVALIDARG; + + int hr = HResults.S_OK; + try + { + IGC gc = _target.Contracts.GC; + GCHeapData heapData = gc.GetHeapData(heapAddr.ToTargetPointer(_target)); + uint numFillPointers = (uint)heapData.FillPointers.Count; + + if (pNeeded != null) + *pNeeded = numFillPointers; + + if (cFillPointers < numFillPointers) + { + hr = HResults.S_FALSE; + } + else + { + for (int i = 0; i < (int)numFillPointers; i++) + { + pFinalizationFillPointers[i] = heapData.FillPointers[i].ToClrDataAddress(_target); + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl8 is not null) + { + uint pNeededLocal; + int hrLocal = _legacyImpl8.GetFinalizationFillPointersSvr(heapAddr, cFillPointers, pFinalizationFillPointers, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + } +#endif + return hr; + } int ISOSDacInterface8.GetAssemblyLoadContext(ClrDataAddress methodTable, ClrDataAddress* assemblyLoadContext) { diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md new file mode 100644 index 00000000000000..e44c1b6ca28d97 --- /dev/null +++ b/src/native/managed/cdac/README.md @@ -0,0 +1,123 @@ +# cDAC (Data Contract Reader) + +The cDAC is a managed implementation of the diagnostic data access layer. It enables +diagnostic tools to inspect .NET runtime process state by reading memory through +well-defined data contracts, without requiring version-matched native DAC/DBI libraries. + +See [docs/design/datacontracts/datacontracts_design.md](/docs/design/datacontracts/datacontracts_design.md) +for the full design and motivation. + +## Architecture + +The cDAC has a layered architecture. When implementing or testing, it's important to +understand which layer you're working at: + +``` +ISOSDacInterface* / IXCLRDataProcess (COM-style API surface) + │ + ▼ + SOSDacImpl (Microsoft.Diagnostics.DataContractReader.Legacy) + │ Translates COM APIs into contract calls. + │ Handles HResult protocols, pointer conversions, + │ and #if DEBUG cross-validation with legacy DAC. + ▼ + Contract interfaces (Microsoft.Diagnostics.DataContractReader.Contracts) + │ e.g., IGC, IThread, ILoader — pure managed APIs + │ returning strongly-typed structs. + ▼ + Data types (Microsoft.Diagnostics.DataContractReader.Contracts/Data/) + │ e.g., Data.Generation, Data.CFinalize — read fields + │ from target memory at specified addresses/offsets. + ▼ + Target memory (Microsoft.Diagnostics.DataContractReader.Abstractions) + ReadPointer, ReadGlobal, ReadNUInt, etc. +``` + +- **To implement a new SOSDac API**: work in `SOSDacImpl` (Legacy project), calling + existing contracts. See the [Legacy project README](Microsoft.Diagnostics.DataContractReader.Legacy/README.md). +- **To implement a new contract**: work in the Contracts project. See the + [contract specifications](/docs/design/datacontracts/) for the data descriptors + and algorithms each contract must implement. +- **To write tests**: see the [tests README](tests/README.md). + +## Project structure + +| Directory | Purpose | +|-----------|---------| +| `Microsoft.Diagnostics.DataContractReader.Abstractions` | Core abstractions: `Target`, `TargetPointer`, `DataType`, contract interfaces | +| `Microsoft.Diagnostics.DataContractReader.Contracts` | Contract implementations (e.g., `GC_1`) and data type readers | +| `Microsoft.Diagnostics.DataContractReader.Legacy` | `SOSDacImpl` — bridges `ISOSDacInterface*` COM APIs to contracts | +| `Microsoft.Diagnostics.DataContractReader` | Contract/data descriptor parsing and `Target` construction | +| `mscordaccore_universal` | Entry point that wires everything together | +| `tests` | Unit tests with mock memory infrastructure | + +## Contract specifications + +Each contract has a specification document in +[docs/design/datacontracts/](/docs/design/datacontracts/) describing: + +- The API surface (C# structs and methods) +- Data descriptors (type layouts and field offsets) +- Global variables (with types and which GC mode they apply to) +- Algorithmic pseudo-code for the implementation + +Key specs: [GC](/docs/design/datacontracts/GC.md) · +[Thread](/docs/design/datacontracts/Thread.md) · +[Loader](/docs/design/datacontracts/Loader.md) · +[RuntimeTypeSystem](/docs/design/datacontracts/RuntimeTypeSystem.md) + +## Integration testing with SOS + +The [dotnet/diagnostics](https://github.com/dotnet/diagnostics) repo has SOS tests that +exercise the cDAC end-to-end against a live .NET process. These tests can run in two modes: +with the legacy DAC or with the cDAC enabled. + +### How cDAC is activated + +`SOSDacImpl` has `#if DEBUG` cross-validation that compares cDAC results against the legacy +DAC. To enable this, build the cDAC in Debug configuration while everything else can be Release. + +At runtime, the DAC checks the `ENABLE_CDAC` config knob +([daccess.cpp](/src/coreclr/debug/daccess/daccess.cpp)). When set to `1`, it looks up the +`DotNetRuntimeContractDescriptor` symbol in the target process, creates the managed cDAC +interface via `mscordaccore_universal`, and routes SOS queries through it. + +### Building the runtime for SOS testing + +Build from the runtime repo root with the cDAC in Debug and everything else in Release: + +```bash +./build.sh -s clr+libs+tools.cdac+host+packs -c Debug -rc release -lc release +``` + +This produces a testhost at: +`artifacts/bin/testhost/net--Release-/shared/Microsoft.NETCore.App//` + +### Running SOS tests in the diagnostics repo + +From the diagnostics repo: + +```bash +# Build managed code (skip native if already built) +./eng/build.sh -c Release --restore --build -skipnative + +# Install test runtimes, overlay your local build, and run tests with cDAC +./eng/build.sh -c Release -test -useCdac -privatebuild -installruntimes \ + -liveRuntimeDir +``` + +The `-useCdac` flag sets `SOS_TEST_CDAC=true`, which causes the test runner (`SOSRunner.cs`) +to set `DOTNET_ENABLE_CDAC=1` on each test process. + +### CI pipeline + +The `runtime-diagnostics.yml` pipeline runs the SOS tests automatically on every PR that +touches `src/native/managed/cdac/**` or `src/coreclr/debug/runtimeinfo/**`. It runs the +tests twice — once with `-useCdac` (cDAC path) and once without (legacy DAC path) — on +Windows x64. + +> **Note:** The runtime and diagnostics repos must be on the same major version. CLRMD +> validates the DAC binary version against the runtime, so a cross-major-version mismatch +> (e.g., 11.0 runtime with 10.0 diagnostics repo) causes `CreateDacInstance` failures. +> See [privatebuildtesting.md](https://github.com/dotnet/diagnostics/blob/main/documentation/privatebuildtesting.md) +> in the diagnostics repo for the full private build overlay procedure. diff --git a/src/native/managed/cdac/tests/GCTests.cs b/src/native/managed/cdac/tests/GCTests.cs new file mode 100644 index 00000000000000..8e7019ddb9b7db --- /dev/null +++ b/src/native/managed/cdac/tests/GCTests.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class GCTests +{ + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_ReturnsCorrectGenerationTable(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, + new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; + + Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(); + IGC gc = target.Contracts.GC; + + GCHeapData heapData = gc.GetHeapData(); + + Assert.Equal(generations.Length, heapData.GenerationTable.Count); + for (int i = 0; i < generations.Length; i++) + { + Assert.Equal(generations[i].StartSegment, (ulong)heapData.GenerationTable[i].StartSegment); + Assert.Equal(generations[i].AllocationStart, (ulong)heapData.GenerationTable[i].AllocationStart); + Assert.Equal(generations[i].AllocContextPointer, (ulong)heapData.GenerationTable[i].AllocationContextPointer); + Assert.Equal(generations[i].AllocContextLimit, (ulong)heapData.GenerationTable[i].AllocationContextLimit); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_ReturnsCorrectFillPointers(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, + new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; + + Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(); + IGC gc = target.Contracts.GC; + + GCHeapData heapData = gc.GetHeapData(); + + Assert.Equal(fillPointers.Length, heapData.FillPointers.Count); + for (int i = 0; i < fillPointers.Length; i++) + { + Assert.Equal(fillPointers[i], (ulong)heapData.FillPointers[i]); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_WithFiveGenerations(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xA000_0000, AllocationStart = 0xA000_1000, AllocContextPointer = 0xA000_2000, AllocContextLimit = 0xA000_3000 }, + new() { StartSegment = 0xB000_0000, AllocationStart = 0xB000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xC000_0000, AllocationStart = 0xC000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xD000_0000, AllocationStart = 0xD000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xE000_0000, AllocationStart = 0xE000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1001, 0x2002, 0x3003, 0x4004, 0x5005, 0x6006, 0x7007]; + + Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(); + IGC gc = target.Contracts.GC; + + GCHeapData heapData = gc.GetHeapData(); + + Assert.Equal(5, heapData.GenerationTable.Count); + for (int i = 0; i < generations.Length; i++) + { + Assert.Equal(generations[i].StartSegment, (ulong)heapData.GenerationTable[i].StartSegment); + Assert.Equal(generations[i].AllocationStart, (ulong)heapData.GenerationTable[i].AllocationStart); + } + + Assert.Equal(fillPointers.Length, heapData.FillPointers.Count); + for (int i = 0; i < fillPointers.Length; i++) + { + Assert.Equal(fillPointers[i], (ulong)heapData.FillPointers[i]); + } + } +} diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs new file mode 100644 index 00000000000000..e6100b8c85ed9c --- /dev/null +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs @@ -0,0 +1,427 @@ +// 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 Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// Configuration object for GC heap mock data, used with +/// and +/// . +/// +internal class GCHeapBuilder +{ + private GCHeapBuilder.GenerationInput[]? _generations; + private ulong[]? _fillPointers; + + public GCHeapBuilder SetGenerations(params GenerationInput[] generations) + { + _generations = generations; + return this; + } + + public GCHeapBuilder SetFillPointers(params ulong[] fillPointers) + { + _fillPointers = fillPointers; + return this; + } + + internal GenerationInput[] GetGenerationsOrDefault(uint defaultCount) => + _generations ?? new GenerationInput[defaultCount]; + + internal ulong[] GetFillPointersOrDefault(uint generationCount) => + _fillPointers ?? []; + + public record struct GenerationInput + { + public ulong StartSegment; + public ulong AllocationStart; + public ulong AllocContextPointer; + public ulong AllocContextLimit; + } +} + +internal static class GCHeapBuilderExtensions +{ + private const ulong DefaultAllocationRangeStart = 0x0010_0000; + private const ulong DefaultAllocationRangeEnd = 0x0020_0000; + private const uint DefaultGenerationCount = 4; + + public static TestPlaceholderTarget.Builder AddGCHeapWks( + this TestPlaceholderTarget.Builder targetBuilder, + Action configure) + { + var config = new GCHeapBuilder(); + configure(config); + BuildWksHeap(targetBuilder, config); + targetBuilder.AddContract(target => + ((IContractFactory)new GCFactory()).CreateContract(target, 1)); + return targetBuilder; + } + + public static TestPlaceholderTarget.Builder AddGCHeapSvr( + this TestPlaceholderTarget.Builder targetBuilder, + Action configure, + out ulong heapAddress) + { + var config = new GCHeapBuilder(); + configure(config); + heapAddress = BuildSvrHeap(targetBuilder, config); + targetBuilder.AddContract(target => + ((IContractFactory)new GCFactory()).CreateContract(target, 1)); + return targetBuilder; + } + + #region Type field definitions + + private static readonly MockDescriptors.TypeFields GCAllocContextFields = new() + { + DataType = DataType.GCAllocContext, + Fields = + [ + new(nameof(Data.GCAllocContext.Pointer), DataType.pointer), + new(nameof(Data.GCAllocContext.Limit), DataType.pointer), + ] + }; + + private static MockDescriptors.TypeFields GetGenerationFields(TargetTestHelpers helpers) + { + uint allocContextSize = MockDescriptors.GetTypesForTypeFields(helpers, [GCAllocContextFields])[DataType.GCAllocContext].Size!.Value; + return new MockDescriptors.TypeFields() + { + DataType = DataType.Generation, + Fields = + [ + new(nameof(Data.Generation.AllocationContext), DataType.GCAllocContext, allocContextSize), + new(nameof(Data.Generation.StartSegment), DataType.pointer), + new(nameof(Data.Generation.AllocationStart), DataType.pointer), + ] + }; + } + + private static readonly MockDescriptors.TypeFields CFinalizeFields = new() + { + DataType = DataType.CFinalize, + Fields = + [ + new(nameof(Data.CFinalize.FillPointers), DataType.pointer), + ] + }; + + private static readonly MockDescriptors.TypeFields OomHistoryFields = new() + { + DataType = DataType.OomHistory, + Fields = + [ + new(nameof(Data.OomHistory.Reason), DataType.int32), + new(nameof(Data.OomHistory.AllocSize), DataType.nuint), + new(nameof(Data.OomHistory.Reserved), DataType.pointer), + new(nameof(Data.OomHistory.Allocated), DataType.pointer), + new(nameof(Data.OomHistory.GcIndex), DataType.nuint), + new(nameof(Data.OomHistory.Fgm), DataType.int32), + new(nameof(Data.OomHistory.Size), DataType.nuint), + new(nameof(Data.OomHistory.AvailablePagefileMb), DataType.nuint), + new(nameof(Data.OomHistory.LohP), DataType.uint32), + ] + }; + + #endregion + + #region Shared helpers + + private static Dictionary GetBaseTypes(TargetTestHelpers helpers) + { + return MockDescriptors.GetTypesForTypeFields(helpers, + [ + GCAllocContextFields, + GetGenerationFields(helpers), + CFinalizeFields, + OomHistoryFields, + ]); + } + + private static Dictionary GetSvrTypes(TargetTestHelpers helpers, uint totalGenerationCount) + { + var baseTypes = GetBaseTypes(helpers); + + uint genSize = baseTypes[DataType.Generation].Size!.Value; + uint oomSize = baseTypes[DataType.OomHistory].Size!.Value; + + int ptrSize = helpers.PointerSize; + int offset = 0; + + var fields = new Dictionary(); + void AddPointerField(string name) { fields[name] = new Target.FieldInfo() { Offset = offset }; offset += ptrSize; } + + AddPointerField(nameof(Data.GCHeapSVR.MarkArray)); + AddPointerField(nameof(Data.GCHeapSVR.NextSweepObj)); + AddPointerField(nameof(Data.GCHeapSVR.BackgroundMinSavedAddr)); + AddPointerField(nameof(Data.GCHeapSVR.BackgroundMaxSavedAddr)); + AddPointerField(nameof(Data.GCHeapSVR.AllocAllocated)); + AddPointerField(nameof(Data.GCHeapSVR.EphemeralHeapSegment)); + AddPointerField(nameof(Data.GCHeapSVR.CardTable)); + AddPointerField(nameof(Data.GCHeapSVR.FinalizeQueue)); + + fields[nameof(Data.GCHeapSVR.GenerationTable)] = new Target.FieldInfo() { Offset = offset }; + offset += (int)(genSize * totalGenerationCount); + + fields[nameof(Data.GCHeapSVR.OomData)] = new Target.FieldInfo() { Offset = offset }; + offset += (int)oomSize; + + AddPointerField(nameof(Data.GCHeapSVR.InternalRootArray)); + AddPointerField(nameof(Data.GCHeapSVR.InternalRootArrayIndex)); + + fields[nameof(Data.GCHeapSVR.HeapAnalyzeSuccess)] = new Target.FieldInfo() { Offset = offset }; + offset += sizeof(int); + offset = (offset + ptrSize - 1) & ~(ptrSize - 1); + + fields[nameof(Data.GCHeapSVR.InterestingData)] = new Target.FieldInfo() { Offset = offset }; + fields[nameof(Data.GCHeapSVR.CompactReasons)] = new Target.FieldInfo() { Offset = offset }; + fields[nameof(Data.GCHeapSVR.ExpandMechanisms)] = new Target.FieldInfo() { Offset = offset }; + fields[nameof(Data.GCHeapSVR.InterestingMechanismBits)] = new Target.FieldInfo() { Offset = offset }; + + baseTypes[DataType.GCHeap] = new Target.TypeInfo() + { + Fields = fields, + Size = (uint)offset, + }; + + return baseTypes; + } + + private static void WriteGenerationData( + TargetTestHelpers helpers, + Span genSpan, + Dictionary types, + GCHeapBuilder.GenerationInput generation) + { + Target.TypeInfo genTypeInfo = types[DataType.Generation]; + Target.TypeInfo allocCtxTypeInfo = types[DataType.GCAllocContext]; + int allocCtxOffset = genTypeInfo.Fields[nameof(Data.Generation.AllocationContext)].Offset; + + helpers.WritePointer( + genSpan.Slice(allocCtxOffset + allocCtxTypeInfo.Fields[nameof(Data.GCAllocContext.Pointer)].Offset), + generation.AllocContextPointer); + helpers.WritePointer( + genSpan.Slice(allocCtxOffset + allocCtxTypeInfo.Fields[nameof(Data.GCAllocContext.Limit)].Offset), + generation.AllocContextLimit); + helpers.WritePointer( + genSpan.Slice(genTypeInfo.Fields[nameof(Data.Generation.StartSegment)].Offset), + generation.StartSegment); + helpers.WritePointer( + genSpan.Slice(genTypeInfo.Fields[nameof(Data.Generation.AllocationStart)].Offset), + generation.AllocationStart); + } + + private static void WriteFillPointers( + TargetTestHelpers helpers, + Span span, + ulong[] fillPointers) + { + for (int i = 0; i < fillPointers.Length; i++) + { + helpers.WritePointer( + span.Slice(helpers.PointerSize * i), + fillPointers[i]); + } + } + + private static MockMemorySpace.HeapFragment AllocatePointerGlobal( + MockMemorySpace.BumpAllocator allocator, + MockMemorySpace.Builder memBuilder, + TargetTestHelpers helpers, + ulong pointsTo, + string name) + { + MockMemorySpace.HeapFragment fragment = allocator.Allocate((ulong)helpers.PointerSize, $"[global pointer] {name}"); + helpers.WritePointer(fragment.Data, pointsTo); + memBuilder.AddHeapFragment(fragment); + return fragment; + } + + #endregion + + private static void BuildWksHeap(TestPlaceholderTarget.Builder targetBuilder, GCHeapBuilder config) + { + MockMemorySpace.Builder memBuilder = targetBuilder.MemoryBuilder; + TargetTestHelpers helpers = memBuilder.TargetTestHelpers; + MockMemorySpace.BumpAllocator allocator = memBuilder.CreateAllocator(DefaultAllocationRangeStart, DefaultAllocationRangeEnd); + + GCHeapBuilder.GenerationInput[] generations = config.GetGenerationsOrDefault(DefaultGenerationCount); + uint genCount = (uint)generations.Length; + ulong[] fillPointers = config.GetFillPointersOrDefault(genCount); + uint fpLength = (uint)fillPointers.Length; + + var types = GetBaseTypes(helpers); + Target.TypeInfo genTypeInfo = types[DataType.Generation]; + Target.TypeInfo cFinalizeTypeInfo = types[DataType.CFinalize]; + Target.TypeInfo oomTypeInfo = types[DataType.OomHistory]; + uint genSize = genTypeInfo.Size!.Value; + + // Allocate and populate generation table + MockMemorySpace.HeapFragment generationTable = allocator.Allocate(genSize * genCount, "GenerationTable"); + for (int i = 0; i < generations.Length; i++) + { + WriteGenerationData(helpers, + generationTable.Data.AsSpan().Slice((int)(i * genSize), (int)genSize), + types, generations[i]); + } + memBuilder.AddHeapFragment(generationTable); + + // Allocate and populate CFinalize with embedded fill pointers array + int fpFieldOffset = cFinalizeTypeInfo.Fields[nameof(Data.CFinalize.FillPointers)].Offset; + ulong cFinalizeSize = (ulong)fpFieldOffset + (ulong)(helpers.PointerSize * (int)fpLength); + MockMemorySpace.HeapFragment cFinalize = allocator.Allocate(cFinalizeSize, "CFinalize"); + WriteFillPointers(helpers, cFinalize.Data.AsSpan().Slice(fpFieldOffset), fillPointers); + memBuilder.AddHeapFragment(cFinalize); + + // Allocate OomHistory (zero-initialized) + MockMemorySpace.HeapFragment oomHistory = allocator.Allocate(oomTypeInfo.Size!.Value, "OomHistory"); + memBuilder.AddHeapFragment(oomHistory); + + // WKS global pointers (double-indirection) + MockMemorySpace.HeapFragment markArrayGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "MarkArray"); + MockMemorySpace.HeapFragment nextSweepObjGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "NextSweepObj"); + MockMemorySpace.HeapFragment bgMinGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "BgMinSavedAddr"); + MockMemorySpace.HeapFragment bgMaxGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "BgMaxSavedAddr"); + MockMemorySpace.HeapFragment allocAllocatedGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "AllocAllocated"); + MockMemorySpace.HeapFragment ephSegGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "EphemeralHeapSegment"); + MockMemorySpace.HeapFragment cardTableGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "CardTable"); + MockMemorySpace.HeapFragment finalizeQueueGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, cFinalize.Address, "FinalizeQueue"); + + MockMemorySpace.HeapFragment internalRootArrayGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "InternalRootArray"); + MockMemorySpace.HeapFragment internalRootArrayIndexGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "InternalRootArrayIndex"); + MockMemorySpace.HeapFragment heapAnalyzeSuccessGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[HeapAnalyzeSuccess]"); + helpers.Write(heapAnalyzeSuccessGlobal.Data.AsSpan(0, sizeof(int)), 0); + memBuilder.AddHeapFragment(heapAnalyzeSuccessGlobal); + + MockMemorySpace.HeapFragment interestingDataArray = allocator.Allocate((ulong)helpers.PointerSize, "InterestingDataArray"); + MockMemorySpace.HeapFragment compactReasonsArray = allocator.Allocate((ulong)helpers.PointerSize, "CompactReasonsArray"); + MockMemorySpace.HeapFragment expandMechanismsArray = allocator.Allocate((ulong)helpers.PointerSize, "ExpandMechanismsArray"); + MockMemorySpace.HeapFragment interestingMechBitsArray = allocator.Allocate((ulong)helpers.PointerSize, "InterestingMechBitsArray"); + memBuilder.AddHeapFragments([interestingDataArray, compactReasonsArray, expandMechanismsArray, interestingMechBitsArray]); + + MockMemorySpace.HeapFragment lowestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0x1000, "LowestAddress"); + MockMemorySpace.HeapFragment highestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0xFFFF_0000, "HighestAddress"); + MockMemorySpace.HeapFragment structInvalidCountGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[StructureInvalidCount]"); + helpers.Write(structInvalidCountGlobal.Data.AsSpan(0, sizeof(int)), 0); + memBuilder.AddHeapFragment(structInvalidCountGlobal); + + MockMemorySpace.HeapFragment maxGenGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[MaxGeneration]"); + helpers.Write(maxGenGlobal.Data.AsSpan(0, sizeof(uint)), genCount - 1); + memBuilder.AddHeapFragment(maxGenGlobal); + + targetBuilder.AddTypes(types); + targetBuilder.AddGlobals( + (nameof(Constants.Globals.TotalGenerationCount), genCount), + (nameof(Constants.Globals.CFinalizeFillPointersLength), fpLength), + (nameof(Constants.Globals.InterestingDataLength), 0UL), + (nameof(Constants.Globals.CompactReasonsLength), 0UL), + (nameof(Constants.Globals.ExpandMechanismsLength), 0UL), + (nameof(Constants.Globals.InterestingMechanismBitsLength), 0UL), + (nameof(Constants.Globals.GCHeapMarkArray), markArrayGlobal.Address), + (nameof(Constants.Globals.GCHeapNextSweepObj), nextSweepObjGlobal.Address), + (nameof(Constants.Globals.GCHeapBackgroundMinSavedAddr), bgMinGlobal.Address), + (nameof(Constants.Globals.GCHeapBackgroundMaxSavedAddr), bgMaxGlobal.Address), + (nameof(Constants.Globals.GCHeapAllocAllocated), allocAllocatedGlobal.Address), + (nameof(Constants.Globals.GCHeapEphemeralHeapSegment), ephSegGlobal.Address), + (nameof(Constants.Globals.GCHeapCardTable), cardTableGlobal.Address), + (nameof(Constants.Globals.GCHeapFinalizeQueue), finalizeQueueGlobal.Address), + (nameof(Constants.Globals.GCHeapGenerationTable), generationTable.Address), + (nameof(Constants.Globals.GCHeapOomData), oomHistory.Address), + (nameof(Constants.Globals.GCHeapInternalRootArray), internalRootArrayGlobal.Address), + (nameof(Constants.Globals.GCHeapInternalRootArrayIndex), internalRootArrayIndexGlobal.Address), + (nameof(Constants.Globals.GCHeapHeapAnalyzeSuccess), heapAnalyzeSuccessGlobal.Address), + (nameof(Constants.Globals.GCHeapInterestingData), interestingDataArray.Address), + (nameof(Constants.Globals.GCHeapCompactReasons), compactReasonsArray.Address), + (nameof(Constants.Globals.GCHeapExpandMechanisms), expandMechanismsArray.Address), + (nameof(Constants.Globals.GCHeapInterestingMechanismBits), interestingMechBitsArray.Address), + (nameof(Constants.Globals.GCLowestAddress), lowestAddrGlobal.Address), + (nameof(Constants.Globals.GCHighestAddress), highestAddrGlobal.Address), + (nameof(Constants.Globals.StructureInvalidCount), structInvalidCountGlobal.Address), + (nameof(Constants.Globals.MaxGeneration), maxGenGlobal.Address)); + targetBuilder.AddGlobalStrings( + (nameof(Constants.Globals.GCIdentifiers), "workstation,segments")); + } + + private static ulong BuildSvrHeap(TestPlaceholderTarget.Builder targetBuilder, GCHeapBuilder config) + { + MockMemorySpace.Builder memBuilder = targetBuilder.MemoryBuilder; + TargetTestHelpers helpers = memBuilder.TargetTestHelpers; + MockMemorySpace.BumpAllocator allocator = memBuilder.CreateAllocator(DefaultAllocationRangeStart, DefaultAllocationRangeEnd); + + GCHeapBuilder.GenerationInput[] generations = config.GetGenerationsOrDefault(DefaultGenerationCount); + uint genCount = (uint)generations.Length; + ulong[] fillPointers = config.GetFillPointersOrDefault(genCount); + uint fpLength = (uint)fillPointers.Length; + + var types = GetSvrTypes(helpers, genCount); + Target.TypeInfo gcHeapTypeInfo = types[DataType.GCHeap]; + Target.TypeInfo cFinalizeTypeInfo = types[DataType.CFinalize]; + uint genSize = types[DataType.Generation].Size!.Value; + + // Allocate and populate CFinalize with embedded fill pointers array + int fpFieldOffset = cFinalizeTypeInfo.Fields[nameof(Data.CFinalize.FillPointers)].Offset; + ulong cFinalizeSize = (ulong)fpFieldOffset + (ulong)(helpers.PointerSize * (int)fpLength); + MockMemorySpace.HeapFragment cFinalize = allocator.Allocate(cFinalizeSize, "CFinalize_SVR"); + WriteFillPointers(helpers, cFinalize.Data.AsSpan().Slice(fpFieldOffset), fillPointers); + memBuilder.AddHeapFragment(cFinalize); + + // Allocate the GCHeap struct, populate FinalizeQueue pointer and generation table + uint heapSize = gcHeapTypeInfo.Size!.Value; + MockMemorySpace.HeapFragment gcHeap = allocator.Allocate(heapSize, "GCHeap_SVR"); + helpers.WritePointer( + gcHeap.Data.AsSpan().Slice(gcHeapTypeInfo.Fields[nameof(Data.GCHeapSVR.FinalizeQueue)].Offset), + cFinalize.Address); + int genTableOffset = gcHeapTypeInfo.Fields[nameof(Data.GCHeapSVR.GenerationTable)].Offset; + for (int i = 0; i < generations.Length; i++) + { + WriteGenerationData(helpers, + gcHeap.Data.AsSpan().Slice(genTableOffset + (int)(i * genSize), (int)genSize), + types, generations[i]); + } + memBuilder.AddHeapFragment(gcHeap); + + // Heap table (array of pointers to heap structs) + MockMemorySpace.HeapFragment heapTable = allocator.Allocate((ulong)helpers.PointerSize, "HeapTable"); + helpers.WritePointer(heapTable.Data, gcHeap.Address); + memBuilder.AddHeapFragment(heapTable); + + // SVR globals + MockMemorySpace.HeapFragment numHeapsGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0, "NumHeaps"); + helpers.Write(numHeapsGlobal.Data.AsSpan(0, sizeof(int)), 1); + MockMemorySpace.HeapFragment heapsGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, heapTable.Address, "Heaps"); + + MockMemorySpace.HeapFragment lowestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0x1000, "LowestAddress"); + MockMemorySpace.HeapFragment highestAddrGlobal = AllocatePointerGlobal(allocator, memBuilder, helpers, 0x7FFF_0000, "HighestAddress"); + MockMemorySpace.HeapFragment structInvalidCountGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[StructureInvalidCount]"); + helpers.Write(structInvalidCountGlobal.Data.AsSpan(0, sizeof(int)), 0); + memBuilder.AddHeapFragment(structInvalidCountGlobal); + + MockMemorySpace.HeapFragment maxGenGlobal = allocator.Allocate((ulong)helpers.PointerSize, "[MaxGeneration]"); + helpers.Write(maxGenGlobal.Data.AsSpan(0, sizeof(uint)), genCount - 1); + memBuilder.AddHeapFragment(maxGenGlobal); + + targetBuilder.AddTypes(types); + targetBuilder.AddGlobals( + (nameof(Constants.Globals.TotalGenerationCount), genCount), + (nameof(Constants.Globals.CFinalizeFillPointersLength), fpLength), + (nameof(Constants.Globals.InterestingDataLength), 0UL), + (nameof(Constants.Globals.CompactReasonsLength), 0UL), + (nameof(Constants.Globals.ExpandMechanismsLength), 0UL), + (nameof(Constants.Globals.InterestingMechanismBitsLength), 0UL), + (nameof(Constants.Globals.NumHeaps), numHeapsGlobal.Address), + (nameof(Constants.Globals.Heaps), heapsGlobal.Address), + (nameof(Constants.Globals.GCLowestAddress), lowestAddrGlobal.Address), + (nameof(Constants.Globals.GCHighestAddress), highestAddrGlobal.Address), + (nameof(Constants.Globals.StructureInvalidCount), structInvalidCountGlobal.Address), + (nameof(Constants.Globals.MaxGeneration), maxGenGlobal.Address)); + targetBuilder.AddGlobalStrings( + (nameof(Constants.Globals.GCIdentifiers), "server,segments")); + + return gcHeap.Address; + } +} diff --git a/src/native/managed/cdac/tests/README.md b/src/native/managed/cdac/tests/README.md new file mode 100644 index 00000000000000..c5e317a6a38166 --- /dev/null +++ b/src/native/managed/cdac/tests/README.md @@ -0,0 +1,136 @@ +# cDAC Tests + +Unit tests for the cDAC data contract reader. Tests use mock memory to simulate +a target process without needing a real runtime. + +## Building and running + +```bash +export PATH="$(pwd)/.dotnet:$PATH" # from repo root +dotnet build src/native/managed/cdac/tests +dotnet test src/native/managed/cdac/tests +``` + +To run a subset: +```bash +dotnet test src/native/managed/cdac/tests --filter "FullyQualifiedName~GCTests" +``` + +## Test layers + +Tests can validate behavior at two layers: + +- **Contract-level tests** (e.g., `GCTests.cs`): Call contract APIs like + `IGC.GetHeapData()` directly. Use these to verify that contracts correctly + read and interpret mock target memory. +- **SOSDacImpl-level tests** (e.g., `SOSDacInterface8Tests.cs`): Call through + `ISOSDacInterface*` on `SOSDacImpl`. Use these to verify the full API surface + including HResult protocols, pointer conversions, and buffer sizing. + +When implementing a new `SOSDacImpl` method backed by an existing contract, write +tests at the SOSDacImpl level. When implementing a new contract, write tests at +the contract level. + +## Architecture support + +Tests run on all four architecture combinations using `[ClassData(typeof(MockTarget.StdArch))]`: +- 64-bit little-endian, 64-bit big-endian +- 32-bit little-endian, 32-bit big-endian + +Be aware that `ClrDataAddress` values are **sign-extended** on 32-bit targets +(see `ConversionExtensions.ToClrDataAddress`). A value like `0xAA000000` becomes +`0xFFFFFFFF_AA000000` on 32-bit. Either use values below `0x80000000` or account +for sign extension in assertions. + +## Creating a test target + +Use `TestPlaceholderTarget.Builder` to construct a mock target. Extension methods +like `AddGCHeapWks` add subsystem-specific mock data, types, globals, and +contracts in a single call. Each extension accepts an `Action<>` to configure +only the data the test needs — everything else defaults to zero. + +```csharp +// Contract-level test +Target target = new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(gen0, gen1, gen2, gen3) + .SetFillPointers(0x1000, 0x2000, 0x3000)) + .Build(); +IGC gc = target.Contracts.GC; + +// SOSDacImpl-level test +ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc.SetGenerations(generations).SetFillPointers(fillPointers)) + .Build(), + legacyObj: null); + +// Server GC — heap address returned via out parameter +ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapSvr(gc => gc.SetGenerations(generations).SetFillPointers(fillPointers), + out var heapAddr) + .Build(), + legacyObj: null); +``` + +The builder owns the `MockMemorySpace.Builder` internally, accumulates types +and globals from each `Add*` call, and wires up contracts automatically at +`Build()` time via `TestContractRegistry`. + +## MockDescriptors + +`MockDescriptors/` contains helpers that set up mock target memory for each +subsystem. The preferred pattern is an **extension method on +`TestPlaceholderTarget.Builder`** that takes an `Action` parameter: + +1. A configuration class (e.g., `GCHeapBuilder`) accumulates test data via + fluent `Set*()` methods. +2. The extension method allocates mock memory, registers types/globals/contracts + directly on the target builder, and returns the builder for chaining. +3. Unset arrays default to zero-length or zero-initialized so tests only + configure the data they care about. + +See `MockDescriptors.GC.cs` for a complete example (`GCHeapBuilder` + +`GCHeapBuilderExtensions`). + +### Key patterns + +#### Composite (embedded) fields + +When a type contains an embedded struct (not a pointer to it), you must specify +the size explicitly in the field definition: + +```csharp +// Get the size of the embedded type first +uint allocContextSize = MockDescriptors.GetTypesForTypeFields(helpers, [GCAllocContextFields]) + [DataType.GCAllocContext].Size!.Value; + +// Then reference it with an explicit size +new(nameof(Data.Generation.AllocationContext), DataType.GCAllocContext, allocContextSize) +``` + +Without the explicit size, the layout engine won't know how much space to reserve. + +#### Embedded arrays vs pointer fields + +Some fields are pointers to external data, others are the start of an inline array. +Check how the `Data.*` constructor reads the field: + +- **Pointer**: `target.ReadPointer(address + offset)` → allocate separate memory, + write a pointer to it. +- **Embedded/inline**: `address + offset` (no ReadPointer) → write array elements + directly into the struct at that offset. + +#### Global indirection levels + +Globals have different indirection patterns depending on the subsystem. +Check how the contract reads the global: + +- **`ReadGlobal(name)`**: reads a primitive value directly from the global. + The global value in the mock is the value itself. +- **`ReadGlobalPointer(name)`**: reads a pointer stored at the global address. + The global value in the mock is the *address* of a memory fragment containing + the actual pointer value. +- **Double indirection** (`ReadPointer(ReadGlobalPointer(name))`): the global + points to memory that contains a pointer to the actual data. diff --git a/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs new file mode 100644 index 00000000000000..3d3d8727958546 --- /dev/null +++ b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public unsafe class SOSDacInterface8Tests +{ + private const int S_OK = 0; + private const int S_FALSE = 1; + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetNumberGenerations_ReturnsCorrectCount(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, + new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; + + ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(), + legacyObj: null); + + uint numGenerations; + int hr = dac8.GetNumberGenerations(&numGenerations); + Assert.Equal(S_OK, hr); + Assert.Equal(4u, numGenerations); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetNumberGenerations_WithFiveGenerations(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xA000_0000, AllocationStart = 0xA000_1000, AllocContextPointer = 0xA000_2000, AllocContextLimit = 0xA000_3000 }, + new() { StartSegment = 0xB000_0000, AllocationStart = 0xB000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xC000_0000, AllocationStart = 0xC000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xD000_0000, AllocationStart = 0xD000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xE000_0000, AllocationStart = 0xE000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1001, 0x2002, 0x3003, 0x4004, 0x5005, 0x6006, 0x7007]; + + ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(), + legacyObj: null); + + uint numGenerations; + int hr = dac8.GetNumberGenerations(&numGenerations); + Assert.Equal(S_OK, hr); + Assert.Equal(5u, numGenerations); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTable_ReturnsCorrectData(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, + new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; + + ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(), + legacyObj: null); + + // First call with cGenerations=0 to query needed count + uint needed; + int hr = dac8.GetGenerationTable(0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal(4u, needed); + + // Second call with sufficient buffer + DacpGenerationData* genData = stackalloc DacpGenerationData[4]; + hr = dac8.GetGenerationTable(4, genData, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < generations.Length; i++) + { + ulong expectedStartSeg = SignExtend(generations[i].StartSegment, arch); + ulong expectedAllocStart = SignExtend(generations[i].AllocationStart, arch); + ulong expectedAllocCtxPtr = SignExtend(generations[i].AllocContextPointer, arch); + ulong expectedAllocCtxLim = SignExtend(generations[i].AllocContextLimit, arch); + Assert.Equal(expectedStartSeg, (ulong)genData[i].start_segment); + Assert.Equal(expectedAllocStart, (ulong)genData[i].allocation_start); + Assert.Equal(expectedAllocCtxPtr, (ulong)genData[i].allocContextPtr); + Assert.Equal(expectedAllocCtxLim, (ulong)genData[i].allocContextLimit); + } + } + + private static ulong SignExtend(ulong value, MockTarget.Architecture arch) + { + if (arch.Is64Bit) + return value; + + return (ulong)(long)(int)(uint)value; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointers_ReturnsCorrectData(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, + new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; + + ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapWks(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers)) + .Build(), + legacyObj: null); + + // First call with cFillPointers=0 to query needed count + uint needed; + int hr = dac8.GetFinalizationFillPointers(0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal(7u, needed); + + // Second call with sufficient buffer + ClrDataAddress* ptrs = stackalloc ClrDataAddress[7]; + hr = dac8.GetFinalizationFillPointers(7, ptrs, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < fillPointers.Length; i++) + { + Assert.Equal(SignExtend(fillPointers[i], arch), (ulong)ptrs[i]); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTableSvr_ReturnsCorrectData(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, + new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; + + ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapSvr(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers), out var heapAddr) + .Build(), + legacyObj: null); + + uint needed; + int hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, 0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal(4u, needed); + + DacpGenerationData* genData = stackalloc DacpGenerationData[4]; + hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, 4, genData, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < generations.Length; i++) + { + Assert.Equal(SignExtend(generations[i].StartSegment, arch), (ulong)genData[i].start_segment); + Assert.Equal(SignExtend(generations[i].AllocationStart, arch), (ulong)genData[i].allocation_start); + Assert.Equal(SignExtend(generations[i].AllocContextPointer, arch), (ulong)genData[i].allocContextPtr); + Assert.Equal(SignExtend(generations[i].AllocContextLimit, arch), (ulong)genData[i].allocContextLimit); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointersSvr_ReturnsCorrectData(MockTarget.Architecture arch) + { + var generations = new GCHeapBuilder.GenerationInput[] + { + new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, + new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + }; + + ulong[] fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; + + ISOSDacInterface8 dac8 = new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapSvr(gc => gc + .SetGenerations(generations) + .SetFillPointers(fillPointers), out var heapAddr) + .Build(), + legacyObj: null); + + uint needed; + int hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, 0, null, &needed); + Assert.Equal(S_FALSE, hr); + Assert.Equal(7u, needed); + + ClrDataAddress* ptrs = stackalloc ClrDataAddress[7]; + hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, 7, ptrs, &needed); + Assert.Equal(S_OK, hr); + + for (int i = 0; i < fillPointers.Length; i++) + { + Assert.Equal(SignExtend(fillPointers[i], arch), (ulong)ptrs[i]); + } + } +} diff --git a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs index f503c9eeb5adc1..3b7595e1ccc129 100644 --- a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs @@ -7,7 +7,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Text; -using Moq; +using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Tests; @@ -30,7 +30,7 @@ public TestPlaceholderTarget(MockTarget.Architecture arch, ReadFromTargetDelegat { IsLittleEndian = arch.IsLittleEndian; PointerSize = arch.Is64Bit ? 8 : 4; - _contractRegistry = new Mock().Object; + _contractRegistry = new TestContractRegistry(); _dataCache = new DefaultDataCache(this); _typeInfoCache = types ?? []; _dataReader = reader; @@ -43,6 +43,71 @@ internal void SetContracts(ContractRegistry contracts) _contractRegistry = contracts; } + /// + /// Fluent builder for . Accumulates types, + /// globals, and contract factories from mock descriptors, then materializes the + /// target and wires contracts in . + /// + internal class Builder + { + private readonly MockTarget.Architecture _arch; + private readonly MockMemorySpace.Builder _memBuilder; + private readonly Dictionary _types = new(); + private readonly List<(string Name, ulong Value)> _globals = new(); + private readonly List<(string Name, string Value)> _globalStrings = new(); + private readonly List<(Type Type, Func Factory)> _contractFactories = new(); + + public Builder(MockTarget.Architecture arch) + { + _arch = arch; + _memBuilder = new MockMemorySpace.Builder(new TargetTestHelpers(arch)); + } + + internal MockMemorySpace.Builder MemoryBuilder => _memBuilder; + + public Builder AddTypes(Dictionary types) + { + foreach (var kvp in types) + _types[kvp.Key] = kvp.Value; + return this; + } + + public Builder AddGlobals(params (string Name, ulong Value)[] globals) + { + _globals.AddRange(globals); + return this; + } + + public Builder AddGlobalStrings(params (string Name, string Value)[] globalStrings) + { + _globalStrings.AddRange(globalStrings); + return this; + } + + public Builder AddContract(Func factory) where TContract : IContract + { + _contractFactories.Add((typeof(TContract), target => factory(target))); + return this; + } + + public TestPlaceholderTarget Build() + { + var target = new TestPlaceholderTarget( + _arch, + _memBuilder.GetMemoryContext().ReadFromTarget, + _types, + _globals.ToArray(), + _globalStrings.ToArray()); + + var registry = new TestContractRegistry(); + foreach (var (type, factory) in _contractFactories) + registry.Add(type, new Lazy(() => factory(target))); + target.SetContracts(registry); + + return target; + } + } + public override int PointerSize { get; } public override bool IsLittleEndian { get; } @@ -372,4 +437,19 @@ public void Clear() } } + private sealed class TestContractRegistry : ContractRegistry + { + private readonly Dictionary> _contracts = new(); + + public void Add(Type type, Lazy contract) => _contracts[type] = contract; + + public override TContract GetContract() + { + if (_contracts.TryGetValue(typeof(TContract), out var lazy)) + return (TContract)lazy.Value; + + throw new NotImplementedException($"Contract {typeof(TContract).Name} is not registered."); + } + } + } From 3a67750c47afc8e9c6e5dca521e598fe78c27e7e Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Sun, 22 Feb 2026 07:06:55 +0000 Subject: [PATCH 2/4] Fix cDAC bugs found during SOS integration testing Fixes discovered while running SOS integration tests against the new GetGenerationTable/GetFinalizationFillPointers implementation: - Fix cDAC library loading on Unix (PAL_GetPalHostModule) - Fix Thread data model crash on non-Windows (TryGetValue for UEWatsonBucketTrackerBuckets) - Fix HRESULT variable shadowing in 8 SOSDacInterface APIs in request.cpp where inner HRESULT declarations masked the return value - Fix cDAC legacy stack walk not advancing in Release builds (ClrDataStackWalk.Next must call legacy outside #if DEBUG) - Fix Debug assertion failures: catch VirtualReadException in GetAppDomainName and GetObjectData, fall back to legacy in EnumMethodInstanceByAddress - Address PR review feedback: add insufficient-buffer tests, pNeeded cross-validation assertions, and reduce test data duplication with shared helpers - Update README documentation with VirtualReadException guidance and legacy delegation placement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/Thread.md | 10 +- src/coreclr/debug/daccess/cdac.cpp | 11 + src/coreclr/debug/daccess/request.cpp | 13 +- .../Data/Thread.cs | 2 +- .../ClrDataStackWalk.cs | 8 +- .../README.md | 24 +- .../SOSDacImpl.IXCLRDataProcess.cs | 33 ++- .../SOSDacImpl.cs | 51 +++- src/native/managed/cdac/README.md | 115 +++++++- .../cdac/tests/SOSDacInterface8Tests.cs | 248 +++++++++--------- 10 files changed, 356 insertions(+), 159 deletions(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index 606b3d924ad6ad..0e4d6ad73f9866 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -101,7 +101,7 @@ The contract additionally depends on these data descriptors | `Thread` | `ExceptionTracker` | Pointer to exception tracking information | | `Thread` | `RuntimeThreadLocals` | Pointer to some thread-local storage | | `Thread` | `ThreadLocalDataPtr` | Pointer to thread local data structure | -| `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data | +| `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data (optional, Windows only) | | `ThreadLocalData` | `NonCollectibleTlsData` | Count of non-collectible TLS data entries | | `ThreadLocalData` | `NonCollectibleTlsArrayData` | Pointer to non-collectible TLS array data | | `ThreadLocalData` | `CollectibleTlsData` | Count of collectible TLS data entries | @@ -281,7 +281,9 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) } else { - readFrom = target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */); + readFrom = /* Has Thread::UEWatsonBucketTrackerBuckets offset */ + ? target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */) + : TargetPointer.Null; if (readFrom == TargetPointer.Null) { readFrom = target.ReadPointer(exceptionTrackerPtr + /* ExceptionInfo::ExceptionWatsonBucketTrackerBuckets offset */); @@ -294,7 +296,9 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) } else { - readFrom = target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */); + readFrom = /* Has Thread::UEWatsonBucketTrackerBuckets offset */ + ? target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */) + : TargetPointer.Null; } Span span = new byte[_target.ReadGlobal("SizeOfGenericModeBlock")]; diff --git a/src/coreclr/debug/daccess/cdac.cpp b/src/coreclr/debug/daccess/cdac.cpp index 738db6aeb6b2ed..d62e134a700fe3 100644 --- a/src/coreclr/debug/daccess/cdac.cpp +++ b/src/coreclr/debug/daccess/cdac.cpp @@ -15,8 +15,19 @@ namespace { // Load cdac from next to current module (DAC binary) PathString path; + + // On Unix, GetCurrentModuleBase() returns a raw dladdr base address, not a PAL HMODULE. + // The DAC is typically loaded externally (e.g. by CLRMD via dlopen) and is not registered + // in the PAL module list. Use PAL_GetPalHostModule() which properly registers the module. +#ifdef HOST_UNIX + HMODULE hMod = PAL_GetPalHostModule(); + if (hMod == NULL || WszGetModuleFileName(hMod, path) == 0) +#else if (WszGetModuleFileName((HMODULE)GetCurrentModuleBase(), path) == 0) +#endif + { return false; + } SString::Iterator iter = path.End(); if (!path.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W)) diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index 310873f6e6232a..a1f800835fc331 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -1581,7 +1581,6 @@ ClrDataAccess::GetObjectStringData(CLRDATA_ADDRESS obj, unsigned int count, _Ino PTR_StringObject str(TO_TADDR(obj)); ULONG32 needed = (ULONG32)str->GetStringLength() + 1; - HRESULT hr; if (stringData && count > 0) { if (count > needed) @@ -3373,7 +3372,7 @@ HRESULT ClrDataAccess::GetHandleEnumForTypes(unsigned int types[], unsigned int DacHandleWalker *walker = new DacHandleWalker(); - HRESULT hr = walker->Init(this, types, count); + hr = walker->Init(this, types, count); if (SUCCEEDED(hr)) hr = walker->QueryInterface(__uuidof(ISOSHandleEnum), (void**)ppHandleEnum); @@ -3400,7 +3399,7 @@ HRESULT ClrDataAccess::GetHandleEnumForGC(unsigned int gen, ISOSHandleEnum **ppH DacHandleWalker *walker = new DacHandleWalker(); - HRESULT hr = walker->Init(this, types, ARRAY_SIZE(types), gen); + hr = walker->Init(this, types, ARRAY_SIZE(types), gen); if (SUCCEEDED(hr)) hr = walker->QueryInterface(__uuidof(ISOSHandleEnum), (void**)ppHandleEnum); @@ -4871,7 +4870,6 @@ HRESULT ClrDataAccess::GetGenerationTable(unsigned int cGenerations, struct Dacp SOSDacEnter(); - HRESULT hr = S_OK; unsigned int numGenerationTableEntries = (unsigned int)(g_gcDacGlobals->total_generation_count); if (pNeeded != NULL) { @@ -4917,7 +4915,6 @@ HRESULT ClrDataAccess::GetFinalizationFillPointers(unsigned int cFillPointers, C SOSDacEnter(); - HRESULT hr = S_OK; unsigned int numFillPointers = (unsigned int)(g_gcDacGlobals->total_generation_count + dac_finalize_queue::ExtraSegCount); if (pNeeded != NULL) { @@ -4958,7 +4955,6 @@ HRESULT ClrDataAccess::GetGenerationTableSvr(CLRDATA_ADDRESS heapAddr, unsigned SOSDacEnter(); - HRESULT hr = S_OK; #ifdef FEATURE_SVR_GC unsigned int numGenerationTableEntries = (unsigned int)(g_gcDacGlobals->total_generation_count); if (pNeeded != NULL) @@ -5008,7 +5004,6 @@ HRESULT ClrDataAccess::GetFinalizationFillPointersSvr(CLRDATA_ADDRESS heapAddr, SOSDacEnter(); - HRESULT hr = S_OK; #ifdef FEATURE_SVR_GC unsigned int numFillPointers = (unsigned int)(g_gcDacGlobals->total_generation_count + dac_finalize_queue::ExtraSegCount); if (pNeeded != NULL) @@ -5158,7 +5153,7 @@ HRESULT ClrDataAccess::GetObjectComWrappersData(CLRDATA_ADDRESS objAddr, CLRDATA SOSDacEnter(); // Default to having found no information. - HRESULT hr = S_FALSE; + hr = S_FALSE; if (pNeeded != NULL) { @@ -5225,8 +5220,6 @@ HRESULT ClrDataAccess::GetObjectComWrappersData(CLRDATA_ADDRESS objAddr, CLRDATA } } - hr = S_FALSE; - SOSDacLeave(); return hr; #else // FEATURE_COMWRAPPERS diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs index a49d65cf0fbd27..9e78142e7c97af 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs @@ -35,7 +35,7 @@ public Thread(Target target, TargetPointer address) // Address of the exception tracker ExceptionTracker = address + (ulong)type.Fields[nameof(ExceptionTracker)].Offset; - // UEWatsonBucketTrackerBuckets does not exist on certain platforms + // UEWatsonBucketTrackerBuckets does not exist on non-Windows platforms UEWatsonBucketTrackerBuckets = type.Fields.TryGetValue(nameof(UEWatsonBucketTrackerBuckets), out Target.FieldInfo watsonFieldInfo) ? target.ReadPointer(address + (ulong)watsonFieldInfo.Offset) : TargetPointer.Null; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs index b2e29676afea0a..7f25c92e4cc154 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataStackWalk.cs @@ -128,13 +128,17 @@ int IXCLRDataStackWalk.Next() hr = ex.HResult; } -#if DEBUG + // Advance the legacy stack walk to keep it in sync with the cDAC walk. + // GetFrame() passes the legacy frame to ClrDataFrame, which delegates + // GetArgumentByIndex/GetLocalVariableByIndex to it. If we don't advance + // the legacy walk here, those calls operate on the wrong frame. if (_legacyImpl is not null) { int hrLocal = _legacyImpl.Next(); +#if DEBUG Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); - } #endif + } return hr; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md index 30d1664c82cdc5..372128f85b88e4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md @@ -51,10 +51,32 @@ int ISOSDacInterface8.ExampleMethod(uint* pResult) - **Null pointer checks**: Validate output pointer arguments *before* the try block and return `E_INVALIDARG`. This matches the native DAC behavior. - **Exception handling**: Wrap all contract calls in try/catch. The catch converts - exceptions to HResult codes via `ex.HResult`. + exceptions to HResult codes via `ex.HResult`. When the native DAC has an explicit + readability check (e.g., `ptr.IsValid()` or `DACGetMethodTableFromObjectPointer` + returning NULL), catch `VirtualReadException` specifically and return the same + HResult the native DAC returns (typically `E_INVALIDARG`). Avoid catching all + exceptions and mapping to a single HRESULT, as this can mask unrelated bugs. - **Debug cross-validation**: In `#if DEBUG`, call the legacy implementation (if available) and assert the results match. This catches discrepancies during testing. +### Legacy delegation placement + +Some cDAC methods create child objects (e.g., `ClrDataMethodInstance`, +`ClrDataFrame`) that delegate certain operations to a legacy counterpart. This is +a temporary implementation workaround to let us create the cDAC incrementally that +should be removed before cDAC ships to customers. In these cases, the legacy call +that obtains the counterpart **must be outside `#if DEBUG`**, because the result is +used functionally, not just for validation. + +For example, `EnumMethodInstanceByAddress` passes `legacyMethod` to +`ClrDataMethodInstance`, which delegates `GetTokenAndScope` and other calls to it. +If the legacy enumeration only runs inside `#if DEBUG`, those delegated calls fail +in Release builds. + +**Rule of thumb**: if a legacy call's result is stored and passed to another +object, keep it outside `#if DEBUG`. Only the assertion that compares +HResults/values belongs inside `#if DEBUG`. + ### Sized-buffer protocol Several `ISOSDacInterface8` methods use a two-call pattern where the caller first diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs index 6fa6cbe173ec1a..6297e6867c9190 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs @@ -334,14 +334,15 @@ int IXCLRDataProcess.StartEnumMethodInstancesByAddress(ClrDataAddress address, / int hr = HResults.S_FALSE; *handle = 0; + // Start the legacy enumeration to keep it in sync with the cDAC enumeration. + // EnumMethodInstanceByAddress passes the legacy method instance to ClrDataMethodInstance, + // which delegates some operations to it. ulong handleLocal = default; -#if DEBUG int hrLocal = default; if (_legacyProcess is not null) { hrLocal = _legacyProcess.StartEnumMethodInstancesByAddress(address, appDomain, &handleLocal); } -#endif try { @@ -387,9 +388,9 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet GCHandle gcHandle = GCHandle.FromIntPtr((IntPtr)(*handle)); if (gcHandle.Target is not EnumMethodInstances emi) return HResults.E_INVALIDARG; + // Advance the legacy enumeration to keep it in sync with the cDAC enumeration. + // The legacy method instance is passed to ClrDataMethodInstance for delegation. IXCLRDataMethodInstance? legacyMethod = null; - -#if DEBUG int hrLocal = default; if (_legacyProcess is not null) { @@ -397,7 +398,6 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet hrLocal = _legacyProcess.EnumMethodInstanceByAddress(&legacyHandle, out legacyMethod); emi.LegacyHandle = legacyHandle; } -#endif try { @@ -413,7 +413,28 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet } catch (System.Exception ex) { - hr = ex.HResult; + // The cDAC's IterateMethodInstances() implementation is incomplete compared + // to the native DAC's EnumMethodInstances::Next(). The native DAC uses a + // MethodIterator backed by AppDomain assembly iteration with EX_TRY/EX_CATCH + // error handling around each step. The cDAC re-implements this with + // IterateModules()/IterateMethodInstantiations()/IterateTypeParams() which + // call into IRuntimeTypeSystem and ILoader contracts. These contract calls + // (e.g. GetMethodTable, GetTypeHandle, GetMethodDescForSlot, GetModule, + // GetTypeDefToken) can throw when encountering method descs or type handles + // from assemblies/modules that the cDAC cannot fully process. This has been + // observed for generic method instantiations (cases 2-4 in + // IterateMethodInstances) in the SOS.WebApp3 integration test. + // + // Fall back to the legacy DAC result when available, otherwise propagate the error. + if (_legacyProcess is not null) + { + hr = hrLocal; + method = legacyMethod; + } + else + { + hr = ex.HResult; + } } #if DEBUG diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 681ded79e90bc2..4a7d66885b6f60 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -241,10 +241,27 @@ int ISOSDacInterface.GetAppDomainName(ClrDataAddress addr, uint count, char* nam try { ILoader loader = _target.Contracts.Loader; - string friendlyName = loader.GetAppDomainFriendlyName(); TargetPointer systemDomainPtr = _target.ReadGlobalPointer(Constants.Globals.SystemDomain); ClrDataAddress systemDomain = _target.ReadPointer(systemDomainPtr).ToClrDataAddress(_target); - if (addr == systemDomain || friendlyName == string.Empty) + + string? friendlyName = null; + if (addr != systemDomain) + { + try + { + friendlyName = loader.GetAppDomainFriendlyName(); + } + catch (VirtualReadException) + { + // The FriendlyName field is a PTR_CWSTR (pointer to wide char string). + // ReadUtf16String throws VirtualReadException when the pointer targets + // unreadable memory (e.g. the name is not yet set during early init). + // The native DAC handles this via PTR_AppDomain->m_friendlyName.IsValid() + // and falls through to return an empty string. Match that behavior here. + } + } + + if (friendlyName is null || friendlyName.Length == 0) { if (pNeeded is not null) { @@ -252,14 +269,14 @@ int ISOSDacInterface.GetAppDomainName(ClrDataAddress addr, uint count, char* nam } if (name is not null && count > 0) { - name[0] = '\0'; // Set the first character to null terminator + name[0] = '\0'; } } else { if (pNeeded is not null) { - *pNeeded = (uint)(friendlyName.Length + 1); // +1 for null terminator + *pNeeded = (uint)(friendlyName.Length + 1); } if (name is not null && count > 0) @@ -2721,6 +2738,16 @@ int ISOSDacInterface.GetObjectData(ClrDataAddress objAddr, DacpObjectData* data) } } + catch (VirtualReadException) + { + // The native DAC returns E_INVALIDARG when it cannot read the object's + // method table pointer (DACGetMethodTableFromObjectPointer returns NULL) + // or when the method table fails structural validation + // (DacValidateMethodTable returns false). Both of these cases surface as + // VirtualReadException in the cDAC when GetMethodTableAddress or + // GetTypeHandle attempt to read unreadable target memory. + hr = HResults.E_INVALIDARG; + } catch (System.Exception ex) { hr = ex.HResult; @@ -4199,6 +4226,10 @@ int ISOSDacInterface8.GetGenerationTable(uint cGenerations, /*struct DacpGenerat uint pNeededLocal; int hrLocal = _legacyImpl8.GetGenerationTable(cGenerations, pGenerationData, &pNeededLocal); Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } } #endif return hr; @@ -4242,6 +4273,10 @@ int ISOSDacInterface8.GetFinalizationFillPointers(uint cFillPointers, ClrDataAdd uint pNeededLocal; int hrLocal = _legacyImpl8.GetFinalizationFillPointers(cFillPointers, pFinalizationFillPointers, &pNeededLocal); Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } } #endif return hr; @@ -4291,6 +4326,10 @@ int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGener uint pNeededLocal; int hrLocal = _legacyImpl8.GetGenerationTableSvr(heapAddr, cGenerations, pGenerationData, &pNeededLocal); Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } } #endif return hr; @@ -4334,6 +4373,10 @@ int ISOSDacInterface8.GetFinalizationFillPointersSvr(ClrDataAddress heapAddr, ui uint pNeededLocal; int hrLocal = _legacyImpl8.GetFinalizationFillPointersSvr(heapAddr, cFillPointers, pFinalizationFillPointers, &pNeededLocal); Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } } #endif return hr; diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index e44c1b6ca28d97..5bd873c63bde87 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -66,6 +66,94 @@ Key specs: [GC](/docs/design/datacontracts/GC.md) · [Loader](/docs/design/datacontracts/Loader.md) · [RuntimeTypeSystem](/docs/design/datacontracts/RuntimeTypeSystem.md) +## Unit testing + +### Setting up a solution + +For VS Code and Visual Studio, create a file `cdac.slnx` in the runtime repo root to bring +all the cDAC projects into scope: + +```xml + + + + + + + + + + + + + + + + +``` + +In VS Code, run the ".NET: Open Solution" command and select `cdac.slnx`. In Visual Studio, +open the solution file directly. You can then use Test Explorer to run and debug tests. + +### Running unit tests from the command line + +Use the `dotnet.sh` (or `dotnet.cmd`) script in the repo root: + +```bash +./dotnet.sh build /t:Test \ + src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj \ + -c Debug -p:RuntimeConfiguration=Debug -p:LibrariesConfiguration=Release +``` + +> **Note:** If you mix release libraries and a debug runtime, you must pass both +> `-p:RuntimeConfiguration=Debug` and `-p:LibrariesConfiguration=Release` so the test +> project resolves the correct shared framework. If everything is Debug, then just +> `-c Debug` is sufficient. + +## End-to-end testing with WinDbg + +### Building a sample app + +Create a hello-world app to use as a debugger target: + +```cmd +cd C:\helloworld +dotnet new console -f net9.0 +``` + +Add `LatestMajor` to the `.csproj` `` so it can +run on a .NET 10+ checkout. Add a `Console.ReadKey()` in `Program.cs` to keep the process +alive while debugging. + +Create a PowerShell script `debug.ps1` to launch WinDbg with the cDAC enabled: + +```powershell +$env:DOTNET_ENABLE_CDAC=1 +windbgx C:\runtime\artifacts\bin\testhost\net10.0-windows-Debug-x64\dotnet.exe .\bin\Debug\net9.0\helloworld.dll +``` + +Replace `C:\runtime` with your runtime repo checkout path. You can also use `corerun.exe` +with a CORE_ROOT directory instead of the testhost `dotnet.exe`. + +### Debugging the cDAC with Visual Studio + +1. Run `debug.ps1` from above. +2. In WinDbg, hit Run and wait for the app to reach the `Console.ReadKey()` pause. +3. Open Visual Studio and select "Attach to process". +4. Attach to the `enghost.exe` process with mixed native and managed debugging. +5. Set breakpoints in `request.cpp` (native DAC) or `SOSDacImpl.cs` (managed cDAC). + +### Useful SOS commands for testing + +| Command | What it exercises | +|---------|-------------------| +| `!clrthreads` | Thread enumeration APIs | +| `!dumpstack` | Stack walking — calls many SOS APIs in `request.cpp` | +| `!dso` / `!dumpstackobjects` | Object inspection for specific object types | + +Click on thread hyperlinks from `!clrthreads` output to switch the active thread before +running `!dumpstack`. + ## Integration testing with SOS The [dotnet/diagnostics](https://github.com/dotnet/diagnostics) repo has SOS tests that @@ -75,7 +163,11 @@ with the legacy DAC or with the cDAC enabled. ### How cDAC is activated `SOSDacImpl` has `#if DEBUG` cross-validation that compares cDAC results against the legacy -DAC. To enable this, build the cDAC in Debug configuration while everything else can be Release. +DAC. To enable this, build the cDAC in Debug configuration while everything else can be +Release. Note that some legacy calls must run outside `#if DEBUG` when their results are +used functionally (not just for validation) — see the +[Legacy project README](Microsoft.Diagnostics.DataContractReader.Legacy/README.md) for +details. At runtime, the DAC checks the `ENABLE_CDAC` config knob ([daccess.cpp](/src/coreclr/debug/daccess/daccess.cpp)). When set to `1`, it looks up the @@ -84,18 +176,29 @@ interface via `mscordaccore_universal`, and routes SOS queries through it. ### Building the runtime for SOS testing -Build from the runtime repo root with the cDAC in Debug and everything else in Release: +Build from the runtime repo root: + +```bash +./build.sh clr+clr.hosts+libs+tools.cdac -c Debug -lc Release +``` + +The debug build of the runtime (`-rc Debug`, which is the default when `-c Debug` is used) +is required for the brittle DAC to delegate to the cDAC. Release build of the libraries +(`-lc Release`) is highly recommended for a faster inner loop. + +Once the initial build is done, shorter incremental rebuilds can be done with: ```bash -./build.sh -s clr+libs+tools.cdac+host+packs -c Debug -rc release -lc release +./build.sh clr.native+tools.cdac -c Debug -lc Release ``` This produces a testhost at: -`artifacts/bin/testhost/net--Release-/shared/Microsoft.NETCore.App//` +`artifacts/bin/testhost/net--Debug-/shared/Microsoft.NETCore.App//` ### Running SOS tests in the diagnostics repo -From the diagnostics repo: +See [privatebuildtesting.md](https://github.com/dotnet/diagnostics/blob/main/documentation/privatebuildtesting.md) +in the diagnostics repo for the full procedure. The key steps are: ```bash # Build managed code (skip native if already built) @@ -119,5 +222,3 @@ Windows x64. > **Note:** The runtime and diagnostics repos must be on the same major version. CLRMD > validates the DAC binary version against the runtime, so a cross-major-version mismatch > (e.g., 11.0 runtime with 10.0 diagnostics repo) causes `CreateDacInstance` failures. -> See [privatebuildtesting.md](https://github.com/dotnet/diagnostics/blob/main/documentation/privatebuildtesting.md) -> in the diagnostics repo for the full private build overlay procedure. diff --git a/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs index 3d3d8727958546..d003f52066e47b 100644 --- a/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs +++ b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs @@ -11,39 +11,63 @@ public unsafe class SOSDacInterface8Tests private const int S_OK = 0; private const int S_FALSE = 1; - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void GetNumberGenerations_ReturnsCorrectCount(MockTarget.Architecture arch) - { - var generations = new GCHeapBuilder.GenerationInput[] - { - new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, - new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - }; + private static readonly GCHeapBuilder.GenerationInput[] s_generations = + [ + new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, + new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, + ]; - ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; + private static readonly ulong[] s_fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; - ISOSDacInterface8 dac8 = new SOSDacImpl( + private static ISOSDacInterface8 CreateWksDac8(MockTarget.Architecture arch) + { + return new SOSDacImpl( new TestPlaceholderTarget.Builder(arch) .AddGCHeapWks(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers)) + .SetGenerations(s_generations) + .SetFillPointers(s_fillPointers)) .Build(), legacyObj: null); + } + + private static ISOSDacInterface8 CreateSvrDac8(MockTarget.Architecture arch, out ulong heapAddr) + { + return new SOSDacImpl( + new TestPlaceholderTarget.Builder(arch) + .AddGCHeapSvr(gc => gc + .SetGenerations(s_generations) + .SetFillPointers(s_fillPointers), out heapAddr) + .Build(), + legacyObj: null); + } + + private static ulong SignExtend(ulong value, MockTarget.Architecture arch) + { + if (arch.Is64Bit) + return value; + + return (ulong)(long)(int)(uint)value; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetNumberGenerations_ReturnsCorrectCount(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateWksDac8(arch); uint numGenerations; int hr = dac8.GetNumberGenerations(&numGenerations); Assert.Equal(S_OK, hr); - Assert.Equal(4u, numGenerations); + Assert.Equal((uint)s_generations.Length, numGenerations); } [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetNumberGenerations_WithFiveGenerations(MockTarget.Architecture arch) { - var generations = new GCHeapBuilder.GenerationInput[] + var fiveGenerations = new GCHeapBuilder.GenerationInput[] { new() { StartSegment = 0xA000_0000, AllocationStart = 0xA000_1000, AllocContextPointer = 0xA000_2000, AllocContextLimit = 0xA000_3000 }, new() { StartSegment = 0xB000_0000, AllocationStart = 0xB000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, @@ -52,13 +76,11 @@ public void GetNumberGenerations_WithFiveGenerations(MockTarget.Architecture arc new() { StartSegment = 0xE000_0000, AllocationStart = 0xE000_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, }; - ulong[] fillPointers = [0x1001, 0x2002, 0x3003, 0x4004, 0x5005, 0x6006, 0x7007]; - ISOSDacInterface8 dac8 = new SOSDacImpl( new TestPlaceholderTarget.Builder(arch) .AddGCHeapWks(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers)) + .SetGenerations(fiveGenerations) + .SetFillPointers(s_fillPointers)) .Build(), legacyObj: null); @@ -72,169 +94,145 @@ public void GetNumberGenerations_WithFiveGenerations(MockTarget.Architecture arc [ClassData(typeof(MockTarget.StdArch))] public void GetGenerationTable_ReturnsCorrectData(MockTarget.Architecture arch) { - var generations = new GCHeapBuilder.GenerationInput[] - { - new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, - new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - }; - - ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; - - ISOSDacInterface8 dac8 = new SOSDacImpl( - new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers)) - .Build(), - legacyObj: null); + ISOSDacInterface8 dac8 = CreateWksDac8(arch); - // First call with cGenerations=0 to query needed count uint needed; int hr = dac8.GetGenerationTable(0, null, &needed); Assert.Equal(S_FALSE, hr); - Assert.Equal(4u, needed); + Assert.Equal((uint)s_generations.Length, needed); - // Second call with sufficient buffer - DacpGenerationData* genData = stackalloc DacpGenerationData[4]; - hr = dac8.GetGenerationTable(4, genData, &needed); + DacpGenerationData* genData = stackalloc DacpGenerationData[(int)needed]; + hr = dac8.GetGenerationTable(needed, genData, &needed); Assert.Equal(S_OK, hr); - for (int i = 0; i < generations.Length; i++) + for (int i = 0; i < s_generations.Length; i++) { - ulong expectedStartSeg = SignExtend(generations[i].StartSegment, arch); - ulong expectedAllocStart = SignExtend(generations[i].AllocationStart, arch); - ulong expectedAllocCtxPtr = SignExtend(generations[i].AllocContextPointer, arch); - ulong expectedAllocCtxLim = SignExtend(generations[i].AllocContextLimit, arch); - Assert.Equal(expectedStartSeg, (ulong)genData[i].start_segment); - Assert.Equal(expectedAllocStart, (ulong)genData[i].allocation_start); - Assert.Equal(expectedAllocCtxPtr, (ulong)genData[i].allocContextPtr); - Assert.Equal(expectedAllocCtxLim, (ulong)genData[i].allocContextLimit); + Assert.Equal(SignExtend(s_generations[i].StartSegment, arch), (ulong)genData[i].start_segment); + Assert.Equal(SignExtend(s_generations[i].AllocationStart, arch), (ulong)genData[i].allocation_start); + Assert.Equal(SignExtend(s_generations[i].AllocContextPointer, arch), (ulong)genData[i].allocContextPtr); + Assert.Equal(SignExtend(s_generations[i].AllocContextLimit, arch), (ulong)genData[i].allocContextLimit); } } - private static ulong SignExtend(ulong value, MockTarget.Architecture arch) + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTable_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) { - if (arch.Is64Bit) - return value; + ISOSDacInterface8 dac8 = CreateWksDac8(arch); - return (ulong)(long)(int)(uint)value; + uint needed; + DacpGenerationData* smallBuffer = stackalloc DacpGenerationData[2]; + int hr = dac8.GetGenerationTable(2, smallBuffer, &needed); + + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_generations.Length, needed); } [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetFinalizationFillPointers_ReturnsCorrectData(MockTarget.Architecture arch) { - var generations = new GCHeapBuilder.GenerationInput[] - { - new() { StartSegment = 0xAA00_0000, AllocationStart = 0xAA00_1000, AllocContextPointer = 0xAA00_2000, AllocContextLimit = 0xAA00_3000 }, - new() { StartSegment = 0xBB00_0000, AllocationStart = 0xBB00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0xCC00_0000, AllocationStart = 0xCC00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0xDD00_0000, AllocationStart = 0xDD00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - }; - - ulong[] fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; + ISOSDacInterface8 dac8 = CreateWksDac8(arch); - ISOSDacInterface8 dac8 = new SOSDacImpl( - new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers)) - .Build(), - legacyObj: null); - - // First call with cFillPointers=0 to query needed count uint needed; int hr = dac8.GetFinalizationFillPointers(0, null, &needed); Assert.Equal(S_FALSE, hr); - Assert.Equal(7u, needed); + Assert.Equal((uint)s_fillPointers.Length, needed); - // Second call with sufficient buffer - ClrDataAddress* ptrs = stackalloc ClrDataAddress[7]; - hr = dac8.GetFinalizationFillPointers(7, ptrs, &needed); + ClrDataAddress* ptrs = stackalloc ClrDataAddress[(int)needed]; + hr = dac8.GetFinalizationFillPointers(needed, ptrs, &needed); Assert.Equal(S_OK, hr); - for (int i = 0; i < fillPointers.Length; i++) + for (int i = 0; i < s_fillPointers.Length; i++) { - Assert.Equal(SignExtend(fillPointers[i], arch), (ulong)ptrs[i]); + Assert.Equal(SignExtend(s_fillPointers[i], arch), (ulong)ptrs[i]); } } [Theory] [ClassData(typeof(MockTarget.StdArch))] - public void GetGenerationTableSvr_ReturnsCorrectData(MockTarget.Architecture arch) + public void GetFinalizationFillPointers_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) { - var generations = new GCHeapBuilder.GenerationInput[] - { - new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, - new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - }; + ISOSDacInterface8 dac8 = CreateWksDac8(arch); - ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; + uint needed; + ClrDataAddress* smallBuffer = stackalloc ClrDataAddress[3]; + int hr = dac8.GetFinalizationFillPointers(3, smallBuffer, &needed); - ISOSDacInterface8 dac8 = new SOSDacImpl( - new TestPlaceholderTarget.Builder(arch) - .AddGCHeapSvr(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers), out var heapAddr) - .Build(), - legacyObj: null); + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_fillPointers.Length, needed); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGenerationTableSvr_ReturnsCorrectData(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); uint needed; int hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, 0, null, &needed); Assert.Equal(S_FALSE, hr); - Assert.Equal(4u, needed); + Assert.Equal((uint)s_generations.Length, needed); - DacpGenerationData* genData = stackalloc DacpGenerationData[4]; - hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, 4, genData, &needed); + DacpGenerationData* genData = stackalloc DacpGenerationData[(int)needed]; + hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, needed, genData, &needed); Assert.Equal(S_OK, hr); - for (int i = 0; i < generations.Length; i++) + for (int i = 0; i < s_generations.Length; i++) { - Assert.Equal(SignExtend(generations[i].StartSegment, arch), (ulong)genData[i].start_segment); - Assert.Equal(SignExtend(generations[i].AllocationStart, arch), (ulong)genData[i].allocation_start); - Assert.Equal(SignExtend(generations[i].AllocContextPointer, arch), (ulong)genData[i].allocContextPtr); - Assert.Equal(SignExtend(generations[i].AllocContextLimit, arch), (ulong)genData[i].allocContextLimit); + Assert.Equal(SignExtend(s_generations[i].StartSegment, arch), (ulong)genData[i].start_segment); + Assert.Equal(SignExtend(s_generations[i].AllocationStart, arch), (ulong)genData[i].allocation_start); + Assert.Equal(SignExtend(s_generations[i].AllocContextPointer, arch), (ulong)genData[i].allocContextPtr); + Assert.Equal(SignExtend(s_generations[i].AllocContextLimit, arch), (ulong)genData[i].allocContextLimit); } } [Theory] [ClassData(typeof(MockTarget.StdArch))] - public void GetFinalizationFillPointersSvr_ReturnsCorrectData(MockTarget.Architecture arch) + public void GetGenerationTableSvr_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) { - var generations = new GCHeapBuilder.GenerationInput[] - { - new() { StartSegment = 0x1A00_0000, AllocationStart = 0x1A00_1000, AllocContextPointer = 0x1A00_2000, AllocContextLimit = 0x1A00_3000 }, - new() { StartSegment = 0x1B00_0000, AllocationStart = 0x1B00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0x1C00_0000, AllocationStart = 0x1C00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - new() { StartSegment = 0x1D00_0000, AllocationStart = 0x1D00_1000, AllocContextPointer = 0, AllocContextLimit = 0 }, - }; + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); - ulong[] fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; + uint needed; + DacpGenerationData* smallBuffer = stackalloc DacpGenerationData[2]; + int hr = dac8.GetGenerationTableSvr((ClrDataAddress)heapAddr, 2, smallBuffer, &needed); - ISOSDacInterface8 dac8 = new SOSDacImpl( - new TestPlaceholderTarget.Builder(arch) - .AddGCHeapSvr(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers), out var heapAddr) - .Build(), - legacyObj: null); + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_generations.Length, needed); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointersSvr_ReturnsCorrectData(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); uint needed; int hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, 0, null, &needed); Assert.Equal(S_FALSE, hr); - Assert.Equal(7u, needed); + Assert.Equal((uint)s_fillPointers.Length, needed); - ClrDataAddress* ptrs = stackalloc ClrDataAddress[7]; - hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, 7, ptrs, &needed); + ClrDataAddress* ptrs = stackalloc ClrDataAddress[(int)needed]; + hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, needed, ptrs, &needed); Assert.Equal(S_OK, hr); - for (int i = 0; i < fillPointers.Length; i++) + for (int i = 0; i < s_fillPointers.Length; i++) { - Assert.Equal(SignExtend(fillPointers[i], arch), (ulong)ptrs[i]); + Assert.Equal(SignExtend(s_fillPointers[i], arch), (ulong)ptrs[i]); } } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetFinalizationFillPointersSvr_InsufficientBuffer_ReturnsSFalseAndNeededCount(MockTarget.Architecture arch) + { + ISOSDacInterface8 dac8 = CreateSvrDac8(arch, out ulong heapAddr); + + uint needed; + ClrDataAddress* smallBuffer = stackalloc ClrDataAddress[3]; + int hr = dac8.GetFinalizationFillPointersSvr((ClrDataAddress)heapAddr, 3, smallBuffer, &needed); + + Assert.Equal(S_FALSE, hr); + Assert.Equal((uint)s_fillPointers.Length, needed); + } } From 9da7539716f77a9e531c99860784fe5f112354b1 Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Wed, 25 Feb 2026 06:04:17 -0800 Subject: [PATCH 3/4] The runtime cDAC tests weren't preserving dumps on failure making it hard to diagnose CI failures. --- eng/pipelines/runtime-diagnostics.yml | 114 +------------------------- 1 file changed, 4 insertions(+), 110 deletions(-) diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index b67d6987ea80a1..cfd824597fef05 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -5,13 +5,6 @@ parameters: displayName: Diagnostics Branch type: string default: main -- name: cdacDumpPlatforms - displayName: cDAC Dump Platforms - type: object - default: - - windows_x64 - - linux_x64 - # - osx_x64 # Temporarily due to CI capacity constraints. Will re-enable once osx queues are more available. resources: repositories: @@ -110,15 +103,8 @@ extends: - task: PublishPipelineArtifact@1 inputs: targetPath: '$(Build.SourcesDirectory)/artifacts/tmp/$(_BuildConfig)/dumps' - artifactName: 'Dumps_cDAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' - displayName: 'Publish Crash Dumps' - continueOnError: true - condition: failed() - - task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(Build.SourcesDirectory)/artifacts/TestResults' - artifactName: 'TestResults_cDAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' - displayName: 'Publish Test Results and SOS Logs' + artifactName: Dumps_$(_PhaseName)_Attempt$(System.JobAttempt) + displayName: 'Publish Dumps on Failure' continueOnError: true condition: failed() - template: /eng/pipelines/common/platform-matrix.yml @@ -158,99 +144,7 @@ extends: - task: PublishPipelineArtifact@1 inputs: targetPath: '$(Build.SourcesDirectory)/artifacts/tmp/$(_BuildConfig)/dumps' - artifactName: 'Dumps_DAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' - displayName: 'Publish Crash Dumps' - continueOnError: true - condition: failed() - - task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(Build.SourcesDirectory)/artifacts/TestResults' - artifactName: 'TestResults_DAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' - displayName: 'Publish Test Results and SOS Logs' + artifactName: Dumps_$(_PhaseName)_Attempt$(System.JobAttempt) + displayName: 'Publish Dumps on Failure' continueOnError: true condition: failed() - - # - # cDAC Dump Creation — Build runtime, create crash dumps, publish dump artifacts - # - - stage: DumpCreation - dependsOn: [] - jobs: - - template: /eng/pipelines/common/platform-matrix.yml - parameters: - jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: release - platforms: ${{ parameters.cdacDumpPlatforms }} - jobParameters: - buildArgs: -s clr+libs+tools.cdac -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig) - nameSuffix: CdacDumpGeneration - timeoutInMinutes: 120 - postBuildSteps: - - script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) msbuild - $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj - /t:GenerateAllDumps - /p:CIDumpVersionsOnly=true - /p:SetDisableAuxProviderSignatureCheck=true - /p:TargetArchitecture=$(archType) - -bl:$(Build.SourcesDirectory)/artifacts/log/DumpGeneration.binlog - displayName: 'Generate cDAC Dumps' - - template: /eng/pipelines/common/upload-artifact-step.yml - parameters: - rootFolder: $(Build.SourcesDirectory)/artifacts/dumps/cdac - includeRootFolder: false - archiveType: tar - archiveExtension: .tar.gz - tarCompression: gz - artifactName: CdacDumps_$(osGroup)_$(archType) - displayName: cDAC Dump Artifacts - - # - # cDAC Dump Tests — Download dumps from all platforms, run tests cross-platform - # - - stage: DumpTest - dependsOn: - - DumpCreation - jobs: - - template: /eng/pipelines/common/platform-matrix.yml - parameters: - jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: release - platforms: ${{ parameters.cdacDumpPlatforms }} - jobParameters: - buildArgs: -s tools.cdacdumptests /p:SkipDumpVersions=net10.0 - nameSuffix: CdacDumpTests - timeoutInMinutes: 60 - postBuildSteps: - # Download and test against dumps from each platform - - ${{ each dumpPlatform in parameters.cdacDumpPlatforms }}: - - template: /eng/pipelines/common/download-artifact-step.yml - parameters: - artifactName: CdacDumps_${{ dumpPlatform }} - artifactFileName: CdacDumps_${{ dumpPlatform }}.tar.gz - unpackFolder: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }} - displayName: '${{ dumpPlatform }} Dumps' - - script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) test - $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj - --no-build - --logger "trx;LogFileName=CdacDumpTests_${{ dumpPlatform }}.trx" - --results-directory $(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }} - displayName: 'Run cDAC Dump Tests (${{ dumpPlatform }} dumps)' - continueOnError: true - env: - CDAC_DUMP_ROOT: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }} - - task: PublishTestResults@2 - displayName: 'Publish Results ($(osGroup)-$(archType) → ${{ dumpPlatform }})' - inputs: - testResultsFormat: VSTest - testResultsFiles: '**/*.trx' - searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }}' - testRunTitle: 'cDAC Dump Tests $(osGroup)-$(archType) → ${{ dumpPlatform }}' - failTaskOnFailedTests: true - publishRunAttachments: true - buildConfiguration: $(_BuildConfig) - continueOnError: true - condition: always() - # Fail the job if any test or publish step above reported issues. - - script: echo "One or more dump test steps failed." && exit 1 - displayName: 'Fail if tests failed' - condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues') From 77081c085a9ae66bf4b4ad2fc5deb9863aaa417c Mon Sep 17 00:00:00 2001 From: Noah Falk Date: Wed, 25 Feb 2026 22:57:04 -0800 Subject: [PATCH 4/4] Address PR feedback: use typed pointers, move validation inside try/catch, use heapData.GenerationTable.Count, and use local buffers for debug cross-validation - Change void* to DacpGenerationData* in ISOSDacInterface8 methods - Move null/validation checks inside try/catch blocks per PR #124814 pattern - Replace ReadGlobal(TotalGenerationCount) with heapData.GenerationTable.Count - Use local buffers for legacy DAC cross-validation to avoid overwriting cDAC data - Use 'is null'/'is not null' instead of '== null'/'!= null' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/runtime-diagnostics.yml | 114 +++++++++++- .../ISOSDacInterface.cs | 4 +- .../README.md | 14 +- .../SOSDacImpl.cs | 167 +++++++++++------- src/native/managed/cdac/tests/GCTests.cs | 24 ++- .../MockDescriptors/MockDescriptors.GC.cs | 33 +--- src/native/managed/cdac/tests/README.md | 21 ++- .../cdac/tests/SOSDacInterface8Tests.cs | 24 ++- 8 files changed, 275 insertions(+), 126 deletions(-) diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index cfd824597fef05..b67d6987ea80a1 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -5,6 +5,13 @@ parameters: displayName: Diagnostics Branch type: string default: main +- name: cdacDumpPlatforms + displayName: cDAC Dump Platforms + type: object + default: + - windows_x64 + - linux_x64 + # - osx_x64 # Temporarily due to CI capacity constraints. Will re-enable once osx queues are more available. resources: repositories: @@ -103,8 +110,15 @@ extends: - task: PublishPipelineArtifact@1 inputs: targetPath: '$(Build.SourcesDirectory)/artifacts/tmp/$(_BuildConfig)/dumps' - artifactName: Dumps_$(_PhaseName)_Attempt$(System.JobAttempt) - displayName: 'Publish Dumps on Failure' + artifactName: 'Dumps_cDAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' + displayName: 'Publish Crash Dumps' + continueOnError: true + condition: failed() + - task: PublishPipelineArtifact@1 + inputs: + targetPath: '$(Build.SourcesDirectory)/artifacts/TestResults' + artifactName: 'TestResults_cDAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' + displayName: 'Publish Test Results and SOS Logs' continueOnError: true condition: failed() - template: /eng/pipelines/common/platform-matrix.yml @@ -144,7 +158,99 @@ extends: - task: PublishPipelineArtifact@1 inputs: targetPath: '$(Build.SourcesDirectory)/artifacts/tmp/$(_BuildConfig)/dumps' - artifactName: Dumps_$(_PhaseName)_Attempt$(System.JobAttempt) - displayName: 'Publish Dumps on Failure' + artifactName: 'Dumps_DAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' + displayName: 'Publish Crash Dumps' + continueOnError: true + condition: failed() + - task: PublishPipelineArtifact@1 + inputs: + targetPath: '$(Build.SourcesDirectory)/artifacts/TestResults' + artifactName: 'TestResults_DAC_$(osGroup)$(osSubgroup)_$(archType)_$(_BuildConfig)_Attempt$(System.JobAttempt)' + displayName: 'Publish Test Results and SOS Logs' continueOnError: true condition: failed() + + # + # cDAC Dump Creation — Build runtime, create crash dumps, publish dump artifacts + # + - stage: DumpCreation + dependsOn: [] + jobs: + - template: /eng/pipelines/common/platform-matrix.yml + parameters: + jobTemplate: /eng/pipelines/common/global-build-job.yml + buildConfig: release + platforms: ${{ parameters.cdacDumpPlatforms }} + jobParameters: + buildArgs: -s clr+libs+tools.cdac -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig) + nameSuffix: CdacDumpGeneration + timeoutInMinutes: 120 + postBuildSteps: + - script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) msbuild + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj + /t:GenerateAllDumps + /p:CIDumpVersionsOnly=true + /p:SetDisableAuxProviderSignatureCheck=true + /p:TargetArchitecture=$(archType) + -bl:$(Build.SourcesDirectory)/artifacts/log/DumpGeneration.binlog + displayName: 'Generate cDAC Dumps' + - template: /eng/pipelines/common/upload-artifact-step.yml + parameters: + rootFolder: $(Build.SourcesDirectory)/artifacts/dumps/cdac + includeRootFolder: false + archiveType: tar + archiveExtension: .tar.gz + tarCompression: gz + artifactName: CdacDumps_$(osGroup)_$(archType) + displayName: cDAC Dump Artifacts + + # + # cDAC Dump Tests — Download dumps from all platforms, run tests cross-platform + # + - stage: DumpTest + dependsOn: + - DumpCreation + jobs: + - template: /eng/pipelines/common/platform-matrix.yml + parameters: + jobTemplate: /eng/pipelines/common/global-build-job.yml + buildConfig: release + platforms: ${{ parameters.cdacDumpPlatforms }} + jobParameters: + buildArgs: -s tools.cdacdumptests /p:SkipDumpVersions=net10.0 + nameSuffix: CdacDumpTests + timeoutInMinutes: 60 + postBuildSteps: + # Download and test against dumps from each platform + - ${{ each dumpPlatform in parameters.cdacDumpPlatforms }}: + - template: /eng/pipelines/common/download-artifact-step.yml + parameters: + artifactName: CdacDumps_${{ dumpPlatform }} + artifactFileName: CdacDumps_${{ dumpPlatform }}.tar.gz + unpackFolder: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }} + displayName: '${{ dumpPlatform }} Dumps' + - script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) test + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj + --no-build + --logger "trx;LogFileName=CdacDumpTests_${{ dumpPlatform }}.trx" + --results-directory $(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }} + displayName: 'Run cDAC Dump Tests (${{ dumpPlatform }} dumps)' + continueOnError: true + env: + CDAC_DUMP_ROOT: $(Build.SourcesDirectory)/artifacts/dumps/${{ dumpPlatform }} + - task: PublishTestResults@2 + displayName: 'Publish Results ($(osGroup)-$(archType) → ${{ dumpPlatform }})' + inputs: + testResultsFormat: VSTest + testResultsFiles: '**/*.trx' + searchFolder: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)/${{ dumpPlatform }}' + testRunTitle: 'cDAC Dump Tests $(osGroup)-$(archType) → ${{ dumpPlatform }}' + failTaskOnFailedTests: true + publishRunAttachments: true + buildConfiguration: $(_BuildConfig) + continueOnError: true + condition: always() + # Fail the job if any test or publish step above reported issues. + - script: echo "One or more dump test steps failed." && exit 1 + displayName: 'Fail if tests failed' + condition: eq(variables['Agent.JobStatus'], 'SucceededWithIssues') diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs index 7f8162dc7a83d4..698fad02e8131b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -830,13 +830,13 @@ public unsafe partial interface ISOSDacInterface8 // WKS [PreserveSig] - int GetGenerationTable(uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded); + int GetGenerationTable(uint cGenerations, DacpGenerationData* pGenerationData, uint* pNeeded); [PreserveSig] int GetFinalizationFillPointers(uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded); // SVR [PreserveSig] - int GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded); + int GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGenerations, DacpGenerationData* pGenerationData, uint* pNeeded); [PreserveSig] int GetFinalizationFillPointersSvr(ClrDataAddress heapAddr, uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md index 372128f85b88e4..5d4d8eae63b4cd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md @@ -11,13 +11,13 @@ replace it with a cDAC implementation following this pattern: ```csharp int ISOSDacInterface8.ExampleMethod(uint* pResult) { - // 1. Validate pointer arguments before the try block - if (pResult == null) - return HResults.E_INVALIDARG; - int hr = HResults.S_OK; try { + // 1. Validate pointer arguments inside the try block + if (pResult is null) + throw new ArgumentException(); + // 2. Get the relevant contract and call it IGC gc = _target.Contracts.GC; *pResult = gc.SomeMethod(); @@ -48,8 +48,8 @@ int ISOSDacInterface8.ExampleMethod(uint* pResult) - **HResult returns**: Methods return `int` HResult codes, not exceptions. Use `HResults.S_OK`, `HResults.S_FALSE`, `HResults.E_INVALIDARG`, etc. -- **Null pointer checks**: Validate output pointer arguments *before* the try block - and return `E_INVALIDARG`. This matches the native DAC behavior. +- **Null pointer checks**: Validate output pointer arguments *inside* the try block + and throw `ArgumentException`. The catch block converts this to an HResult code. - **Exception handling**: Wrap all contract calls in try/catch. The catch converts exceptions to HResult codes via `ex.HResult`. When the native DAC has an explicit readability check (e.g., `ptr.IsValid()` or `DACGetMethodTableFromObjectPointer` @@ -88,7 +88,7 @@ int GetSomeTable(uint count, Data* buffer, uint* pNeeded) The protocol is: 1. Always set `*pNeeded` to the required count (if `pNeeded` is not null). -2. If `count > 0 && buffer == null`: return `E_INVALIDARG`. +2. If `count > 0 && buffer is null`: throw `ArgumentException`. 3. If `count < needed`: return `S_FALSE` (buffer too small, but `*pNeeded` is set). 4. If `count >= needed`: populate `buffer` and return `S_OK`. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 4a7d66885b6f60..47112be07d966f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2738,16 +2738,6 @@ int ISOSDacInterface.GetObjectData(ClrDataAddress objAddr, DacpObjectData* data) } } - catch (VirtualReadException) - { - // The native DAC returns E_INVALIDARG when it cannot read the object's - // method table pointer (DACGetMethodTableFromObjectPointer returns NULL) - // or when the method table fails structural validation - // (DacValidateMethodTable returns false). Both of these cases surface as - // VirtualReadException in the cDAC when GetMethodTableAddress or - // GetTypeHandle attempt to read unreadable target memory. - hr = HResults.E_INVALIDARG; - } catch (System.Exception ex) { hr = ex.HResult; @@ -4155,11 +4145,12 @@ int ISOSDacInterface8.GetNumberGenerations(uint* pGenerations) int hr = HResults.S_OK; try { - if (pGenerations == null) + if (pGenerations is null) throw new ArgumentException(); - // Read the total generation count from the global - uint totalGenerationCount = _target.ReadGlobal(Constants.Globals.TotalGenerationCount); + IGC gc = _target.Contracts.GC; + GCHeapData heapData = gc.GetHeapData(); + uint totalGenerationCount = (uint)heapData.GenerationTable.Count; *pGenerations = totalGenerationCount; } catch (System.Exception ex) @@ -4182,18 +4173,19 @@ int ISOSDacInterface8.GetNumberGenerations(uint* pGenerations) return hr; } - int ISOSDacInterface8.GetGenerationTable(uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded) + int ISOSDacInterface8.GetGenerationTable(uint cGenerations, DacpGenerationData* pGenerationData, uint* pNeeded) { - if (cGenerations > 0 && pGenerationData == null) - return HResults.E_INVALIDARG; - int hr = HResults.S_OK; try { + if (cGenerations > 0 && pGenerationData is null) + throw new ArgumentException(); + IGC gc = _target.Contracts.GC; - uint totalGenerationCount = _target.ReadGlobal(Constants.Globals.TotalGenerationCount); + GCHeapData heapData = gc.GetHeapData(); + uint totalGenerationCount = (uint)heapData.GenerationTable.Count; - if (pNeeded != null) + if (pNeeded is not null) *pNeeded = totalGenerationCount; if (cGenerations < totalGenerationCount) @@ -4202,16 +4194,13 @@ int ISOSDacInterface8.GetGenerationTable(uint cGenerations, /*struct DacpGenerat } else { - GCHeapData heapData = gc.GetHeapData(); - DacpGenerationData* genData = (DacpGenerationData*)pGenerationData; - - for (int i = 0; i < (int)totalGenerationCount && i < heapData.GenerationTable.Count; i++) + for (int i = 0; i < (int)totalGenerationCount; i++) { GCGenerationData gen = heapData.GenerationTable[i]; - genData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); - genData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); - genData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); - genData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); + pGenerationData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); + pGenerationData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); + pGenerationData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); + pGenerationData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); } } } @@ -4224,11 +4213,25 @@ int ISOSDacInterface8.GetGenerationTable(uint cGenerations, /*struct DacpGenerat if (_legacyImpl8 is not null) { uint pNeededLocal; - int hrLocal = _legacyImpl8.GetGenerationTable(cGenerations, pGenerationData, &pNeededLocal); - Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); - if (pNeeded is not null) + DacpGenerationData[]? genDataLocal = cGenerations > 0 ? new DacpGenerationData[cGenerations] : null; + fixed (DacpGenerationData* pGenDataLocal = genDataLocal) { - Debug.Assert(*pNeeded == pNeededLocal); + int hrLocal = _legacyImpl8.GetGenerationTable(cGenerations, pGenDataLocal, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + if (hr == HResults.S_OK && pGenerationData is not null) + { + for (int i = 0; i < (int)cGenerations; i++) + { + Debug.Assert(pGenDataLocal[i].start_segment == pGenerationData[i].start_segment); + Debug.Assert(pGenDataLocal[i].allocation_start == pGenerationData[i].allocation_start); + Debug.Assert(pGenDataLocal[i].allocContextPtr == pGenerationData[i].allocContextPtr); + Debug.Assert(pGenDataLocal[i].allocContextLimit == pGenerationData[i].allocContextLimit); + } + } } } #endif @@ -4237,17 +4240,17 @@ int ISOSDacInterface8.GetGenerationTable(uint cGenerations, /*struct DacpGenerat int ISOSDacInterface8.GetFinalizationFillPointers(uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded) { - if (cFillPointers > 0 && pFinalizationFillPointers == null) - return HResults.E_INVALIDARG; - int hr = HResults.S_OK; try { + if (cFillPointers > 0 && pFinalizationFillPointers is null) + throw new ArgumentException(); + IGC gc = _target.Contracts.GC; GCHeapData heapData = gc.GetHeapData(); uint numFillPointers = (uint)heapData.FillPointers.Count; - if (pNeeded != null) + if (pNeeded is not null) *pNeeded = numFillPointers; if (cFillPointers < numFillPointers) @@ -4271,29 +4274,41 @@ int ISOSDacInterface8.GetFinalizationFillPointers(uint cFillPointers, ClrDataAdd if (_legacyImpl8 is not null) { uint pNeededLocal; - int hrLocal = _legacyImpl8.GetFinalizationFillPointers(cFillPointers, pFinalizationFillPointers, &pNeededLocal); - Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); - if (pNeeded is not null) + ClrDataAddress[]? fillPointersLocal = cFillPointers > 0 ? new ClrDataAddress[cFillPointers] : null; + fixed (ClrDataAddress* pFillPointersLocal = fillPointersLocal) { - Debug.Assert(*pNeeded == pNeededLocal); + int hrLocal = _legacyImpl8.GetFinalizationFillPointers(cFillPointers, pFillPointersLocal, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + if (hr == HResults.S_OK && pFinalizationFillPointers is not null) + { + for (int i = 0; i < (int)cFillPointers; i++) + { + Debug.Assert(pFillPointersLocal[i] == pFinalizationFillPointers[i]); + } + } } } #endif return hr; } - int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGenerations, /*struct DacpGenerationData*/ void* pGenerationData, uint* pNeeded) + int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGenerations, DacpGenerationData* pGenerationData, uint* pNeeded) { - if (heapAddr == 0 || (cGenerations > 0 && pGenerationData == null)) - return HResults.E_INVALIDARG; - int hr = HResults.S_OK; try { + if (heapAddr == 0 || (cGenerations > 0 && pGenerationData is null)) + throw new ArgumentException(); + IGC gc = _target.Contracts.GC; - uint totalGenerationCount = _target.ReadGlobal(Constants.Globals.TotalGenerationCount); + GCHeapData heapData = gc.GetHeapData(heapAddr.ToTargetPointer(_target)); + uint totalGenerationCount = (uint)heapData.GenerationTable.Count; - if (pNeeded != null) + if (pNeeded is not null) *pNeeded = totalGenerationCount; if (cGenerations < totalGenerationCount) @@ -4302,16 +4317,13 @@ int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGener } else { - GCHeapData heapData = gc.GetHeapData(heapAddr.ToTargetPointer(_target)); - DacpGenerationData* genData = (DacpGenerationData*)pGenerationData; - - for (int i = 0; i < (int)totalGenerationCount && i < heapData.GenerationTable.Count; i++) + for (int i = 0; i < (int)totalGenerationCount; i++) { GCGenerationData gen = heapData.GenerationTable[i]; - genData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); - genData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); - genData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); - genData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); + pGenerationData[i].start_segment = gen.StartSegment.ToClrDataAddress(_target); + pGenerationData[i].allocation_start = gen.AllocationStart.ToClrDataAddress(_target); + pGenerationData[i].allocContextPtr = gen.AllocationContextPointer.ToClrDataAddress(_target); + pGenerationData[i].allocContextLimit = gen.AllocationContextLimit.ToClrDataAddress(_target); } } } @@ -4324,11 +4336,25 @@ int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGener if (_legacyImpl8 is not null) { uint pNeededLocal; - int hrLocal = _legacyImpl8.GetGenerationTableSvr(heapAddr, cGenerations, pGenerationData, &pNeededLocal); - Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); - if (pNeeded is not null) + DacpGenerationData[]? genDataLocal = cGenerations > 0 ? new DacpGenerationData[cGenerations] : null; + fixed (DacpGenerationData* pGenDataLocal = genDataLocal) { - Debug.Assert(*pNeeded == pNeededLocal); + int hrLocal = _legacyImpl8.GetGenerationTableSvr(heapAddr, cGenerations, pGenDataLocal, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + if (hr == HResults.S_OK && pGenerationData is not null) + { + for (int i = 0; i < (int)cGenerations; i++) + { + Debug.Assert(pGenDataLocal[i].start_segment == pGenerationData[i].start_segment); + Debug.Assert(pGenDataLocal[i].allocation_start == pGenerationData[i].allocation_start); + Debug.Assert(pGenDataLocal[i].allocContextPtr == pGenerationData[i].allocContextPtr); + Debug.Assert(pGenDataLocal[i].allocContextLimit == pGenerationData[i].allocContextLimit); + } + } } } #endif @@ -4337,17 +4363,17 @@ int ISOSDacInterface8.GetGenerationTableSvr(ClrDataAddress heapAddr, uint cGener int ISOSDacInterface8.GetFinalizationFillPointersSvr(ClrDataAddress heapAddr, uint cFillPointers, ClrDataAddress* pFinalizationFillPointers, uint* pNeeded) { - if (heapAddr == 0 || (cFillPointers > 0 && pFinalizationFillPointers == null)) - return HResults.E_INVALIDARG; - int hr = HResults.S_OK; try { + if (heapAddr == 0 || (cFillPointers > 0 && pFinalizationFillPointers is null)) + throw new ArgumentException(); + IGC gc = _target.Contracts.GC; GCHeapData heapData = gc.GetHeapData(heapAddr.ToTargetPointer(_target)); uint numFillPointers = (uint)heapData.FillPointers.Count; - if (pNeeded != null) + if (pNeeded is not null) *pNeeded = numFillPointers; if (cFillPointers < numFillPointers) @@ -4371,11 +4397,22 @@ int ISOSDacInterface8.GetFinalizationFillPointersSvr(ClrDataAddress heapAddr, ui if (_legacyImpl8 is not null) { uint pNeededLocal; - int hrLocal = _legacyImpl8.GetFinalizationFillPointersSvr(heapAddr, cFillPointers, pFinalizationFillPointers, &pNeededLocal); - Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); - if (pNeeded is not null) + ClrDataAddress[]? fillPointersLocal = cFillPointers > 0 ? new ClrDataAddress[cFillPointers] : null; + fixed (ClrDataAddress* pFillPointersLocal = fillPointersLocal) { - Debug.Assert(*pNeeded == pNeededLocal); + int hrLocal = _legacyImpl8.GetFinalizationFillPointersSvr(heapAddr, cFillPointers, pFillPointersLocal, &pNeededLocal); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + if (pNeeded is not null) + { + Debug.Assert(*pNeeded == pNeededLocal); + } + if (hr == HResults.S_OK && pFinalizationFillPointers is not null) + { + for (int i = 0; i < (int)cFillPointers; i++) + { + Debug.Assert(pFillPointersLocal[i] == pFinalizationFillPointers[i]); + } + } } } #endif diff --git a/src/native/managed/cdac/tests/GCTests.cs b/src/native/managed/cdac/tests/GCTests.cs index 8e7019ddb9b7db..0ecb226db0143d 100644 --- a/src/native/managed/cdac/tests/GCTests.cs +++ b/src/native/managed/cdac/tests/GCTests.cs @@ -23,9 +23,11 @@ public void GetHeapData_ReturnsCorrectGenerationTable(MockTarget.Architecture ar ulong[] fillPointers = [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000]; Target target = new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers)) + .AddGCHeapWks(gc => + { + gc.Generations = generations; + gc.FillPointers = fillPointers; + }) .Build(); IGC gc = target.Contracts.GC; @@ -56,9 +58,11 @@ public void GetHeapData_ReturnsCorrectFillPointers(MockTarget.Architecture arch) ulong[] fillPointers = [0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777]; Target target = new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers)) + .AddGCHeapWks(gc => + { + gc.Generations = generations; + gc.FillPointers = fillPointers; + }) .Build(); IGC gc = target.Contracts.GC; @@ -87,9 +91,11 @@ public void GetHeapData_WithFiveGenerations(MockTarget.Architecture arch) ulong[] fillPointers = [0x1001, 0x2002, 0x3003, 0x4004, 0x5005, 0x6006, 0x7007]; Target target = new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(generations) - .SetFillPointers(fillPointers)) + .AddGCHeapWks(gc => + { + gc.Generations = generations; + gc.FillPointers = fillPointers; + }) .Build(); IGC gc = target.Contracts.GC; diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs index e6100b8c85ed9c..09e1e91c7ce07a 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.GC.cs @@ -14,26 +14,12 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; /// internal class GCHeapBuilder { - private GCHeapBuilder.GenerationInput[]? _generations; - private ulong[]? _fillPointers; + // The native GC sizes m_FillPointers as total_generation_count + ExtraSegCount. + private const int DefaultGenerationCount = 4; + private const int ExtraSegCount = 2; - public GCHeapBuilder SetGenerations(params GenerationInput[] generations) - { - _generations = generations; - return this; - } - - public GCHeapBuilder SetFillPointers(params ulong[] fillPointers) - { - _fillPointers = fillPointers; - return this; - } - - internal GenerationInput[] GetGenerationsOrDefault(uint defaultCount) => - _generations ?? new GenerationInput[defaultCount]; - - internal ulong[] GetFillPointersOrDefault(uint generationCount) => - _fillPointers ?? []; + public GenerationInput[] Generations { get; set; } = new GenerationInput[DefaultGenerationCount]; + public ulong[] FillPointers { get; set; } = new ulong[DefaultGenerationCount + ExtraSegCount]; public record struct GenerationInput { @@ -48,7 +34,6 @@ internal static class GCHeapBuilderExtensions { private const ulong DefaultAllocationRangeStart = 0x0010_0000; private const ulong DefaultAllocationRangeEnd = 0x0020_0000; - private const uint DefaultGenerationCount = 4; public static TestPlaceholderTarget.Builder AddGCHeapWks( this TestPlaceholderTarget.Builder targetBuilder, @@ -250,9 +235,9 @@ private static void BuildWksHeap(TestPlaceholderTarget.Builder targetBuilder, GC TargetTestHelpers helpers = memBuilder.TargetTestHelpers; MockMemorySpace.BumpAllocator allocator = memBuilder.CreateAllocator(DefaultAllocationRangeStart, DefaultAllocationRangeEnd); - GCHeapBuilder.GenerationInput[] generations = config.GetGenerationsOrDefault(DefaultGenerationCount); + GCHeapBuilder.GenerationInput[] generations = config.Generations; uint genCount = (uint)generations.Length; - ulong[] fillPointers = config.GetFillPointersOrDefault(genCount); + ulong[] fillPointers = config.FillPointers; uint fpLength = (uint)fillPointers.Length; var types = GetBaseTypes(helpers); @@ -353,9 +338,9 @@ private static ulong BuildSvrHeap(TestPlaceholderTarget.Builder targetBuilder, G TargetTestHelpers helpers = memBuilder.TargetTestHelpers; MockMemorySpace.BumpAllocator allocator = memBuilder.CreateAllocator(DefaultAllocationRangeStart, DefaultAllocationRangeEnd); - GCHeapBuilder.GenerationInput[] generations = config.GetGenerationsOrDefault(DefaultGenerationCount); + GCHeapBuilder.GenerationInput[] generations = config.Generations; uint genCount = (uint)generations.Length; - ulong[] fillPointers = config.GetFillPointersOrDefault(genCount); + ulong[] fillPointers = config.FillPointers; uint fpLength = (uint)fillPointers.Length; var types = GetSvrTypes(helpers, genCount); diff --git a/src/native/managed/cdac/tests/README.md b/src/native/managed/cdac/tests/README.md index c5e317a6a38166..29b515b0b412a2 100644 --- a/src/native/managed/cdac/tests/README.md +++ b/src/native/managed/cdac/tests/README.md @@ -52,24 +52,33 @@ only the data the test needs — everything else defaults to zero. ```csharp // Contract-level test Target target = new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(gen0, gen1, gen2, gen3) - .SetFillPointers(0x1000, 0x2000, 0x3000)) + .AddGCHeapWks(gc => + { + gc.Generations = [gen0, gen1, gen2, gen3]; + gc.FillPointers = [0x1000, 0x2000, 0x3000]; + }) .Build(); IGC gc = target.Contracts.GC; // SOSDacImpl-level test ISOSDacInterface8 dac8 = new SOSDacImpl( new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc.SetGenerations(generations).SetFillPointers(fillPointers)) + .AddGCHeapWks(gc => + { + gc.Generations = generations; + gc.FillPointers = fillPointers; + }) .Build(), legacyObj: null); // Server GC — heap address returned via out parameter ISOSDacInterface8 dac8 = new SOSDacImpl( new TestPlaceholderTarget.Builder(arch) - .AddGCHeapSvr(gc => gc.SetGenerations(generations).SetFillPointers(fillPointers), - out var heapAddr) + .AddGCHeapSvr(gc => + { + gc.Generations = generations; + gc.FillPointers = fillPointers; + }, out var heapAddr) .Build(), legacyObj: null); ``` diff --git a/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs index d003f52066e47b..47d2c744cf2dfa 100644 --- a/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs +++ b/src/native/managed/cdac/tests/SOSDacInterface8Tests.cs @@ -25,9 +25,11 @@ private static ISOSDacInterface8 CreateWksDac8(MockTarget.Architecture arch) { return new SOSDacImpl( new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(s_generations) - .SetFillPointers(s_fillPointers)) + .AddGCHeapWks(gc => + { + gc.Generations = s_generations; + gc.FillPointers = s_fillPointers; + }) .Build(), legacyObj: null); } @@ -36,9 +38,11 @@ private static ISOSDacInterface8 CreateSvrDac8(MockTarget.Architecture arch, out { return new SOSDacImpl( new TestPlaceholderTarget.Builder(arch) - .AddGCHeapSvr(gc => gc - .SetGenerations(s_generations) - .SetFillPointers(s_fillPointers), out heapAddr) + .AddGCHeapSvr(gc => + { + gc.Generations = s_generations; + gc.FillPointers = s_fillPointers; + }, out heapAddr) .Build(), legacyObj: null); } @@ -78,9 +82,11 @@ public void GetNumberGenerations_WithFiveGenerations(MockTarget.Architecture arc ISOSDacInterface8 dac8 = new SOSDacImpl( new TestPlaceholderTarget.Builder(arch) - .AddGCHeapWks(gc => gc - .SetGenerations(fiveGenerations) - .SetFillPointers(s_fillPointers)) + .AddGCHeapWks(gc => + { + gc.Generations = fiveGenerations; + gc.FillPointers = s_fillPointers; + }) .Build(), legacyObj: null);