From 1e7c9dd88985e5d6c45213df3b92e4bd529ab6db Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Sat, 29 Aug 2026 21:21:19 -0400 Subject: [PATCH 1/2] [cDAC] Tolerate missing WKS card-table slot Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9a58113d-e1d8-4ed2-9113-9f1de69cd49e --- .../Contracts/GC/GCHeapWKS.cs | 5 +- .../SOSDacImpl.cs | 3 +- .../managed/cdac/tests/UnitTests/GCTests.cs | 72 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs index 20ab0a9fd28290..ad1c56418a3872 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs @@ -17,7 +17,10 @@ public GCHeapWKS(Target target) BackgroundMaxSavedAddr = target.ReadPointer(bgMaxPtr.Value); AllocAllocated = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapAllocAllocated)); EphemeralHeapSegment = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapEphemeralHeapSegment)); - CardTable = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapCardTable)); + TargetPointer cardTablePtr = target.ReadGlobalPointer(Constants.Globals.GCHeapCardTable); + CardTable = target.TryReadPointer(cardTablePtr, out TargetPointer cardTable) + ? cardTable + : TargetPointer.Null; FinalizeQueue = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapFinalizeQueue)); GenerationTable = target.ReadGlobalPointer(Constants.Globals.GCHeapGenerationTable); 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 c045d545c443f6..6fbd5aec89a7e4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -1640,7 +1640,8 @@ int ISOSDacInterface.GetGCHeapStaticData(DacpGcHeapDetails* details) Debug.Assert(details->lowest_address == detailsLocal.lowest_address, $"cDAC: {details->lowest_address:x}, DAC: {detailsLocal.lowest_address:x}"); Debug.Assert(details->highest_address == detailsLocal.highest_address, $"cDAC: {details->highest_address:x}, DAC: {detailsLocal.highest_address:x}"); - Debug.Assert(details->card_table == detailsLocal.card_table, $"cDAC: {details->card_table:x}, DAC: {detailsLocal.card_table:x}"); + // Reduced dumps may omit the cDAC card-table slot while retaining the legacy VM mirror. + Debug.Assert(details->card_table == 0 || details->card_table == detailsLocal.card_table, $"cDAC: {details->card_table:x}, DAC: {detailsLocal.card_table:x}"); } } #endif diff --git a/src/native/managed/cdac/tests/UnitTests/GCTests.cs b/src/native/managed/cdac/tests/UnitTests/GCTests.cs index d5529d134950f4..b485bb1c601cb4 100644 --- a/src/native/managed/cdac/tests/UnitTests/GCTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/GCTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; @@ -11,6 +12,54 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; public class GCTests { + private const ulong CardTableAddress = 0x1234_5000; + + private static readonly MockGCBuilder.Generation[] s_cardTableGenerations = + [ + 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 }, + ]; + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_ReadableCardTableSlot_ReturnsCardTable(MockTarget.Architecture arch) + { + Target target = CreateWksTargetWithCardTable(arch, cardTableSlotReadable: true); + + GCHeapData heapData = target.Contracts.GC.GetHeapData(); + + Assert.Equal(CardTableAddress, (ulong)heapData.CardTable); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHeapData_UnreadableCardTableSlot_ReturnsHeapDataWithNullCardTable(MockTarget.Architecture arch) + { + Target target = CreateWksTargetWithCardTable(arch, cardTableSlotReadable: false); + + GCHeapData heapData = target.Contracts.GC.GetHeapData(); + + Assert.Equal(TargetPointer.Null, heapData.CardTable); + Assert.Equal(s_cardTableGenerations.Length, heapData.GenerationTable.Count); + Assert.Equal(s_cardTableGenerations[0].StartSegment, (ulong)heapData.GenerationTable[0].StartSegment); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public unsafe void GetGCHeapStaticData_UnreadableCardTableSlot_ReturnsHeapDataWithNullCardTable(MockTarget.Architecture arch) + { + Target target = CreateWksTargetWithCardTable(arch, cardTableSlotReadable: false); + + ISOSDacInterface sosDac = new SOSDacImpl(target, legacyObj: null, new()); + DacpGcHeapDetails details = default; + + Assert.Equal(0, sosDac.GetGCHeapStaticData(&details)); + Assert.Equal(0UL, details.card_table.Value); + Assert.Equal(s_cardTableGenerations[0].StartSegment, details.generation_table[0].start_segment.Value); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetHeapData_ReturnsCorrectGenerationTable(MockTarget.Architecture arch) @@ -120,6 +169,29 @@ public void GetHeapData_WithFiveGenerations(MockTarget.Architecture arch) private sealed record CapturedSegment(ulong Start, ulong End, GCSegmentClassification Generation); + private static Target CreateWksTargetWithCardTable(MockTarget.Architecture arch, bool cardTableSlotReadable) + { + ulong cardTableGlobalAddress = 0; + var builder = new TestPlaceholderTarget.Builder(arch); + builder.AddGCHeapWks(gc => + { + gc.Generations = s_cardTableGenerations; + gc.ConfigureMemory = gcBuilder => + { + cardTableGlobalAddress = gcBuilder.CardTableGlobalAddress; + gcBuilder.WritePointerGlobal(cardTableGlobalAddress, CardTableAddress); + }; + }); + + if (!cardTableSlotReadable) + { + TestPlaceholderTarget.ReadFromTargetDelegate reader = builder.MemoryBuilder.GetMemoryContext().ReadFromTarget; + builder.UseReader((address, buffer) => address == cardTableGlobalAddress ? -1 : reader(address, buffer)); + } + + return builder.Build(); + } + private static MockGCBuilder.Generation[] MakeGenerations(ulong gen0Seg, ulong gen0Start, ulong gen1Seg, ulong gen1Start, ulong gen2Seg, ulong lohSeg, ulong pohSeg) => [ From 2e676c52b2ac36f4a5eae67486e437b13a320ed2 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Sun, 30 Aug 2026 23:20:23 -0400 Subject: [PATCH 2/2] [cDAC] Read WKS card table from VM global Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9a58113d-e1d8-4ed2-9113-9f1de69cd49e --- .../gc/datadescriptor/datadescriptor.h | 7 ++ src/coreclr/vm/datadescriptor/CMakeLists.txt | 1 + .../Contracts/GC/GCHeapWKS.cs | 5 +- .../SOSDacImpl.cs | 3 +- .../tests/DumpTests/WorkstationGCDumpTests.cs | 1 + .../managed/cdac/tests/UnitTests/GCTests.cs | 72 ------------------- 6 files changed, 11 insertions(+), 78 deletions(-) diff --git a/src/coreclr/gc/datadescriptor/datadescriptor.h b/src/coreclr/gc/datadescriptor/datadescriptor.h index 166d9ada0b9a5e..a52044a40c026f 100644 --- a/src/coreclr/gc/datadescriptor/datadescriptor.h +++ b/src/coreclr/gc/datadescriptor/datadescriptor.h @@ -11,6 +11,9 @@ #include "gchandletableimpl.h" #include "handletablepriv.h" #include "gceventstatus.h" +#ifdef GC_HEAP_CARD_TABLE_IN_VM +#include "gcheaputilities.h" +#endif // GC_HEAP_CARD_TABLE_IN_VM #ifdef SERVER_GC #define GC_NAMESPACE SVR @@ -60,7 +63,11 @@ struct cdac_data #endif // BACKGROUND_GC GC_HEAP_FIELD(AllocAllocated, alloc_allocated) GC_HEAP_FIELD(EphemeralHeapSegment, ephemeral_heap_segment) +#ifdef GC_HEAP_CARD_TABLE_IN_VM + static constexpr decltype(&g_card_table) CardTable = &g_card_table; +#else // !GC_HEAP_CARD_TABLE_IN_VM GC_HEAP_FIELD(CardTable, card_table) +#endif // !GC_HEAP_CARD_TABLE_IN_VM GC_HEAP_FIELD(FinalizeQueue, finalize_queue) GC_HEAP_FIELD(GenerationTable, generation_table) #if !defined(USE_REGIONS) && defined(BACKGROUND_GC) diff --git a/src/coreclr/vm/datadescriptor/CMakeLists.txt b/src/coreclr/vm/datadescriptor/CMakeLists.txt index c14d4d7cb6c46d..e5bdb7807e52d9 100644 --- a/src/coreclr/vm/datadescriptor/CMakeLists.txt +++ b/src/coreclr/vm/datadescriptor/CMakeLists.txt @@ -26,6 +26,7 @@ add_library(gc_wks_descriptor_interface INTERFACE) target_include_directories(gc_wks_descriptor_interface INTERFACE ${GC_DESCRIPTOR_DIR} ${CLR_DIR}/gc) +target_compile_definitions(gc_wks_descriptor_interface INTERFACE -DGC_HEAP_CARD_TABLE_IN_VM) add_dependencies(gc_wks_descriptor_interface cee_wks_core) generate_data_descriptors( LIBRARY_NAME gc_wks_descriptor diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs index ad1c56418a3872..20ab0a9fd28290 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs @@ -17,10 +17,7 @@ public GCHeapWKS(Target target) BackgroundMaxSavedAddr = target.ReadPointer(bgMaxPtr.Value); AllocAllocated = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapAllocAllocated)); EphemeralHeapSegment = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapEphemeralHeapSegment)); - TargetPointer cardTablePtr = target.ReadGlobalPointer(Constants.Globals.GCHeapCardTable); - CardTable = target.TryReadPointer(cardTablePtr, out TargetPointer cardTable) - ? cardTable - : TargetPointer.Null; + CardTable = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapCardTable)); FinalizeQueue = target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.GCHeapFinalizeQueue)); GenerationTable = target.ReadGlobalPointer(Constants.Globals.GCHeapGenerationTable); 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 6fbd5aec89a7e4..c045d545c443f6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -1640,8 +1640,7 @@ int ISOSDacInterface.GetGCHeapStaticData(DacpGcHeapDetails* details) Debug.Assert(details->lowest_address == detailsLocal.lowest_address, $"cDAC: {details->lowest_address:x}, DAC: {detailsLocal.lowest_address:x}"); Debug.Assert(details->highest_address == detailsLocal.highest_address, $"cDAC: {details->highest_address:x}, DAC: {detailsLocal.highest_address:x}"); - // Reduced dumps may omit the cDAC card-table slot while retaining the legacy VM mirror. - Debug.Assert(details->card_table == 0 || details->card_table == detailsLocal.card_table, $"cDAC: {details->card_table:x}, DAC: {detailsLocal.card_table:x}"); + Debug.Assert(details->card_table == detailsLocal.card_table, $"cDAC: {details->card_table:x}, DAC: {detailsLocal.card_table:x}"); } } #endif diff --git a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs index 733263f7001b21..dcc183df9a94ca 100644 --- a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs @@ -78,6 +78,7 @@ public void WorkstationGC_CanGetHeapData(TestConfiguration config) GCHeapData heapData = gcContract.GetHeapData(); Assert.NotNull(heapData.GenerationTable); Assert.True(heapData.GenerationTable.Count > 0, "Expected at least one generation"); + Assert.NotEqual(TargetPointer.Null, heapData.CardTable); } [ConditionalTheory] diff --git a/src/native/managed/cdac/tests/UnitTests/GCTests.cs b/src/native/managed/cdac/tests/UnitTests/GCTests.cs index b485bb1c601cb4..d5529d134950f4 100644 --- a/src/native/managed/cdac/tests/UnitTests/GCTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/GCTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts; -using Microsoft.Diagnostics.DataContractReader.Legacy; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; @@ -12,54 +11,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; public class GCTests { - private const ulong CardTableAddress = 0x1234_5000; - - private static readonly MockGCBuilder.Generation[] s_cardTableGenerations = - [ - 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 }, - ]; - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void GetHeapData_ReadableCardTableSlot_ReturnsCardTable(MockTarget.Architecture arch) - { - Target target = CreateWksTargetWithCardTable(arch, cardTableSlotReadable: true); - - GCHeapData heapData = target.Contracts.GC.GetHeapData(); - - Assert.Equal(CardTableAddress, (ulong)heapData.CardTable); - } - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void GetHeapData_UnreadableCardTableSlot_ReturnsHeapDataWithNullCardTable(MockTarget.Architecture arch) - { - Target target = CreateWksTargetWithCardTable(arch, cardTableSlotReadable: false); - - GCHeapData heapData = target.Contracts.GC.GetHeapData(); - - Assert.Equal(TargetPointer.Null, heapData.CardTable); - Assert.Equal(s_cardTableGenerations.Length, heapData.GenerationTable.Count); - Assert.Equal(s_cardTableGenerations[0].StartSegment, (ulong)heapData.GenerationTable[0].StartSegment); - } - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public unsafe void GetGCHeapStaticData_UnreadableCardTableSlot_ReturnsHeapDataWithNullCardTable(MockTarget.Architecture arch) - { - Target target = CreateWksTargetWithCardTable(arch, cardTableSlotReadable: false); - - ISOSDacInterface sosDac = new SOSDacImpl(target, legacyObj: null, new()); - DacpGcHeapDetails details = default; - - Assert.Equal(0, sosDac.GetGCHeapStaticData(&details)); - Assert.Equal(0UL, details.card_table.Value); - Assert.Equal(s_cardTableGenerations[0].StartSegment, details.generation_table[0].start_segment.Value); - } - [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetHeapData_ReturnsCorrectGenerationTable(MockTarget.Architecture arch) @@ -169,29 +120,6 @@ public void GetHeapData_WithFiveGenerations(MockTarget.Architecture arch) private sealed record CapturedSegment(ulong Start, ulong End, GCSegmentClassification Generation); - private static Target CreateWksTargetWithCardTable(MockTarget.Architecture arch, bool cardTableSlotReadable) - { - ulong cardTableGlobalAddress = 0; - var builder = new TestPlaceholderTarget.Builder(arch); - builder.AddGCHeapWks(gc => - { - gc.Generations = s_cardTableGenerations; - gc.ConfigureMemory = gcBuilder => - { - cardTableGlobalAddress = gcBuilder.CardTableGlobalAddress; - gcBuilder.WritePointerGlobal(cardTableGlobalAddress, CardTableAddress); - }; - }); - - if (!cardTableSlotReadable) - { - TestPlaceholderTarget.ReadFromTargetDelegate reader = builder.MemoryBuilder.GetMemoryContext().ReadFromTarget; - builder.UseReader((address, buffer) => address == cardTableGlobalAddress ? -1 : reader(address, buffer)); - } - - return builder.Build(); - } - private static MockGCBuilder.Generation[] MakeGenerations(ulong gen0Seg, ulong gen0Start, ulong gen1Seg, ulong gen1Start, ulong gen2Seg, ulong lohSeg, ulong pohSeg) => [