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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

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

[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

@steveisoksteveisok 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 teamAugust 30, 2026 01:24

CopilotAI 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.
FileDescription
src/​native/​managed/​cdac/​tests/​UnitTests/​GCTests.csAdds 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.csNarrows 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.csSwitches 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
MemberAuthor

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
CopilotAI review requested due to automatic review settings August 31, 2026 03:20
@steveisoksteveisok changed the title [cDAC] Tolerate missing WKS card-table slot in reduced dumps[cDAC] Read WKS card table from preserved VM globalAug 31, 2026

CopilotAI 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

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
MemberAuthor

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

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();
TargetPointerbkGlobal=target.ReadGlobalPointer("BookkeepingStart");
if (bkGlobal==TargetPointer.Null) throwE_FAIL;
TargetPointerbookkeepingStart=target.ReadPointer(bkGlobal);
if (bookkeepingStart==TargetPointer.Null) throwE_FAIL;
uintcardTableInfoSize=/* global value "CardTableInfoSize" */;
uintrecount=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Recount offset */);
ulongsize=target.ReadNUInt(bookkeepingStart+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=bookkeepingStart, Size=size });
TargetPointernext=target.ReadPointer(bookkeepingStart+/* CardTableInfo::NextCardTable offset */);
TargetPointerfirstNext=next;
intmaxRegions=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)
{
TargetPointerctAddr=next-cardTableInfoSize;
recount=target.ReadNUInt(ctAddr+/* CardTableInfo::Recount offset */);
size=target.ReadNUInt(ctAddr+/* CardTableInfo::Size offset */);
if (recount!=0&&size!=0)
regions.Add(newGCMemoryRegionData { Start=ctAddr, Size=size });
next=target.ReadPointer(ctAddr+/* CardTableInfo::NextCardTable offset */);
if (next==firstNext) break;
maxRegions--;
}
returnregions;
}
. 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
MemberAuthor

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.

@noahfalk

Copy link
Copy Markdown
Member

#132973

+1 for that version of the fix (assuming we confirm it works, it seems like it should)

@steveisok

Copy link
Copy Markdown
MemberAuthor

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

@max-charlamb

Copy link
Copy Markdown
Member

I generated a dump w/ cdb and #132973 and read it with cdac. While the card table read was fixed, it exposed a failure on the next read where interesting_data_per_heap + 8 only has the first element preserved.

compact_reasons_per_heap, expand_mechanisms_per_heap, and interesting_mechanism_bits_per_heap from gcinterface.dacvars.def:71-74 are other arrays likely to display similar characteristics.

Given the cdac reader is eager, we probably should enumerate the full WKS diagnostics arrays.

I updated #132973 to also enumerate those arrays. Previously the DAC did not support !dumpgcdata on heap dumps (as those arrays were not enumerated).

I have tested the single file scenario as described in this PR description and the cDAC and DAC are both able to read the heap dumps and support !dumpgcdata.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@max-charlamb I tested against your PR and it looks good. I'll close this in favor of yours.

Sign up for freeto 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.

6 participants

@steveisok@rcj1@jkotas@max-charlamb@noahfalk