Skip to content

[cDAC] Read WKS card table from preserved VM global - #132938

Open
steveisok wants to merge 2 commits into
dotnet:mainfrom
steveisok:steveisok-cdac-card-table-resilience
Open

[cDAC] Read WKS card table from preserved VM global#132938
steveisok wants to merge 2 commits into
dotnet:mainfrom
steveisok:steveisok-cdac-card-table-resilience

Conversation

@steveisok

@steveisok steveisok commented Aug 30, 2026

Copy link
Copy Markdown
Member

Windows CDB /mw single-file dumps preserve the VM g_card_table global but can omit the workstation GC's gc_heap::card_table slot. Map GCHeapCardTable to the preserved VM global in CoreCLR's embedded WKS descriptor so cDAC can return the actual card-table value and continue reading heap data.

The standalone GC descriptor continues using gc_heap::card_table because it cannot depend on VM state.

Tests: CoreCLR Release and Debug builds; cDAC GCTests; focused workstation GC dump test.

Note

This pull request description was generated with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9a58113d-e1d8-4ed2-9113-9f1de69cd49e
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@steveisok
steveisok requested a review from a team August 30, 2026 01:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

Review tier: Lite
Findings: None

What changed in this PR

This PR updates the cDAC GC workstation heap contract to treat the GCHeapCardTable global slot as optional when the slot’s target memory can’t be read, returning a null card table pointer while still providing valid heap and generation data.

Changes:

  • Make GCHeapWKS.CardTable resilient to an unreadable card-table slot by using TryReadPointer and falling back to TargetPointer.Null.
  • Relax DEBUG cross-validation in SOSDacImpl.GetGCHeapStaticData to tolerate a zero card table when the legacy DAC still reports a non-zero value.
  • Add unit test coverage for both readable and unreadable card-table slot scenarios, including validating GetGCHeapStaticData output.
File Description
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.cs Adds tests for readable vs unreadable workstation card-table slot, and validates GetGCHeapStaticData returns a null card table without breaking generation data.
src/​native/​managed/​cdac/​Microsoft.Diagnostics.DataContractReader.Legacy/​SOSDacImpl.cs Narrows DEBUG-only cross-validation to allow card_table == 0 for reduced-dump scenarios in GetGCHeapStaticData.
src/​native/​managed/​cdac/​Microsoft.Diagnostics.DataContractReader.Contracts/​Contracts/​GC/​GCHeapWKS.cs Switches card-table read to TryReadPointer with a TargetPointer.Null fallback to tolerate unreadable target memory.

@rcj1

rcj1 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

I don't think this is the root cause of the issue; rather, we are trying to read memory at gc_heap::card_table which we do not preserve in a dump. g_card_table, however, is preserved but cDAC is not reading from it.

@steveisok

Copy link
Copy Markdown
Member Author

I don't think this is the root cause of the issue; rather, we are trying to read memory at gc_heap::card_table which we do not preserve in a dump. g_card_table, however, is preserved but cDAC is not reading from it.

Making sure I'm following you - Are you suggesting this PR should remap GCHeapCardTable to g_card_table instead of treating the current slot as optional?

@rcj1

rcj1 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Yes, I think that is the best solution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9a58113d-e1d8-4ed2-9113-9f1de69cd49e
Copilot AI review requested due to automatic review settings August 31, 2026 03:20
@steveisok steveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps [cDAC] Read WKS card table from preserved VM global Aug 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

Review tier: Lite
Findings: None

@jkotas

jkotas commented Aug 31, 2026

Copy link
Copy Markdown
Member

Why is the card_table needed in the first place?

The standalone GC descriptor continues using gc_heap::card_table because it cannot depend on VM state.

This suggests that the fix is not quite right.

@steveisok

Copy link
Copy Markdown
Member Author

Why is the card_table needed in the first place?

This suggests that the fix is not quite right.

From my read it's ISOSDacInterface compat. I'm not sure if we have any consumers who rely on it.

Are you suggesting we report 0 for this?

@max-charlamb

Copy link
Copy Markdown
Member

Why is the card_table needed in the first place?

The standalone GC descriptor continues using gc_heap::card_table because it cannot depend on VM state.

This suggests that the fix is not quite right.

This is exposed by ISOSDacInterface13::GetGCBookkeepingMemoryRegions() which is used by CLRMD. It appears that is only really used to list the regions used by the runtime.

I would prefer not to map this value to the runtime copy (this seems like it would not fix the issue if using a standalone GC), but instead fix dump collection to make sure this value is enumerated.

@jkotas

jkotas commented Aug 31, 2026

Copy link
Copy Markdown
Member

This is exposed by ISOSDacInterface13::GetGCBookkeepingMemoryRegions() which is used by CLRMD.

How is this exposed by this API?

This API walks linked list starting at BookkeepingStart:

IReadOnlyList<GCMemoryRegionData> IGC.GetGCBookkeepingMemoryRegions()
{
List<GCMemoryRegionData> regions = new();
TargetPointer bkGlobal = target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal == TargetPointer.Null) throw E_FAIL;
TargetPointer bookkeepingStart = target.ReadPointer(bkGlobal);
if (bookkeepingStart == TargetPointer.Null) throw E_FAIL;
uint cardTableInfoSize = /* global value "CardTableInfoSize" */;
uint recount = target.ReadNUInt(bookkeepingStart + /* CardTableInfo::Recount offset */);
ulong size = target.ReadNUInt(bookkeepingStart + /* CardTableInfo::Size offset */);
if (recount != 0 && size != 0)
regions.Add(new GCMemoryRegionData { Start = bookkeepingStart, Size = size });
TargetPointer next = target.ReadPointer(bookkeepingStart + /* CardTableInfo::NextCardTable offset */);
TargetPointer firstNext = next;
int maxRegions = MaxBookkeepingRegions;
// Compare next > cardTableInfoSize to guard against underflow when subtracting
// cardTableInfoSize. Matches native DAC: `while (next > card_table_info_size)`.
while (next != TargetPointer.Null && next > cardTableInfoSize && maxRegions > 0)
{
TargetPointer ctAddr = next - cardTableInfoSize;
recount = target.ReadNUInt(ctAddr + /* CardTableInfo::Recount offset */);
size = target.ReadNUInt(ctAddr + /* CardTableInfo::Size offset */);
if (recount != 0 && size != 0)
regions.Add(new GCMemoryRegionData { Start = ctAddr, Size = size });
next = target.ReadPointer(ctAddr + /* CardTableInfo::NextCardTable offset */);
if (next == firstNext) break;
maxRegions--;
}
return regions;
}
. Where does the raw card table pointer enter the picture?

@rcj1

rcj1 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Where does the raw card table pointer enter the picture

details->card_table = heapData.CardTable.ToClrDataAddress(_target);

details->card_table = heapData.CardTable.ToClrDataAddress(_target);

@max-charlamb

Copy link
Copy Markdown
Member

Where does the raw card table pointer enter the picture?

You are right, I misread the function. CLRMD does expose this through ISOSDacInterface::GetGCHeapDetails, but it doesn't look like it is used in the enumeration logic.

@rcj1

rcj1 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fwiw it doesn’t look like we use this card table value in the three consumers I checked.

@jkotas

jkotas commented Aug 31, 2026

Copy link
Copy Markdown
Member

details->card_table = heapData.CardTable.ToClrDataAddress(_target);

details->card_table = heapData.CardTable.ToClrDataAddress(_target);

I would expect the card table pointer to be included in the dump with the rest of heapData to handle this path.

@max-charlamb

Copy link
Copy Markdown
Member

I created a draft PR to fix this from the enumeration side. The DAC previously explicitly enumerated the static WKS GC values for everything besides the card_table. Adding this would be a minor gc/dac interface bump.

#132973

I'm fine with either fix. If we go forwards with the datadescriptor sided fix (in this PR), we should leave a note to undo it when we switch over to using the cDAC to enumerate memory.

@steveisok

Copy link
Copy Markdown
Member Author

I created a draft PR to fix this from the enumeration side. The DAC previously explicitly enumerated the static WKS GC values for everything besides > the card_table. Adding this would be a minor gc/dac interface bump.

I think we need to test the draft PR runtime with CDB and see if it fixes the problem. If it doesn't / we can't somehow fix it, then I think this a short term fix and as you said, we remove it once we switch over.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants