Skip to content

Use cdac for metadata serialization - #131027

Merged
rcj1 merged 10 commits into
dotnet:mainfrom
rcj1:cdac-metadata
Jul 30, 2026
Merged

Use cdac for metadata serialization#131027
rcj1 merged 10 commits into
dotnet:mainfrom
rcj1:cdac-metadata

Conversation

@rcj1

@rcj1 rcj1 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor
  • Remove the native path for reading metadata outside of DacDbi and introduce DacDbi APIs to size and fill a local metadata buffer that is generated by walking read-write metadata structures.
  • Encapsulate this algorithm in the cDAC.

Copilot AI review requested due to automatic review settings July 19, 2026 05:43
@rcj1 rcj1 self-assigned this Jul 19, 2026
@azure-pipelines

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR switches debugger metadata acquisition for read-write metadata from the legacy “remote MD structure deserialization” path to a DacDbi + cDAC-based serialization approach, removing the old custom data source infrastructure and associated layout/version plumbing.

Changes:

  • Add DacDbi APIs to compute the size of, and fill, a serialized contiguous ECMA-335 metadata blob for a module’s writable (MDInternalRW) metadata.
  • Move/encapsulate the read-write metadata reconstruction into the cDAC EcmaMetadata contract (with caching) and update the debugger to consume the serialized blob via OpenScopeOnMemory.
  • Remove metadata custom data source / layout verification code paths and drop obsolete debugger “defines/version” fields and tests.
Show a summary per file
File Description
src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs Updates unit tests and mock layouts after removing Debugger defines/version fields.
src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiDebuggerDumpTests.cs Removes dump tests for DacDbi APIs that were deleted (defines/version).
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/StressTestApi/CdacStressApi.cs Uses shared CorDbgHResults.ERROR_INSUFFICIENT_BUFFER constant.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs Removes old methods; adds GetReadWriteMetadataSize / FillReadWriteMetadata.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs Implements new read-write metadata sizing/filling via cDAC contract.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs Switches insufficient-buffer HRESULT to shared constant.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Debugger.cs Removes Debugger data descriptor fields for defines/version.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/EcmaMetadata_1.cs Adds read-write metadata blob construction + caching; GetMetadata uses it for RW modules.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/Debugger_1.cs Updates DebuggerData projection to only include initialization state.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CorDbHResults.cs Adds ERROR_INSUFFICIENT_BUFFER constant.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IEcmaMetadata.cs Adds GetReadWriteMetadata to the public contract interface.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs Shrinks DebuggerData contract shape (drops defines/version).
src/coreclr/vm/datadescriptor/datadescriptor.inc Removes Debugger field descriptors for defines/version.
src/coreclr/md/runtime/stgpool.cpp Updates comment to reference new DacDbiInterfaceImpl::ReadStoragePool dependency.
src/coreclr/md/inc/VerifyLayouts.inc Deletes layout-verification macro data (no longer needed).
src/coreclr/md/inc/verifylayouts.h Deletes metadata layout verification infrastructure.
src/coreclr/md/inc/stgpooli.h Removes VerifyLayoutsMD friend declarations.
src/coreclr/md/inc/stgpool.h Removes VerifyLayoutsMD friend declarations.
src/coreclr/md/inc/recordpool.h Removes VerifyLayoutsMD friend declarations.
src/coreclr/md/inc/metamodelrw.h Removes custom data source hooks; adds DacDbiInterfaceImpl friend where needed.
src/coreclr/md/inc/metamodel.h Removes VerifyLayoutsMD friend; adds DacDbiInterfaceImpl friend.
src/coreclr/md/inc/mdinternalrw.h Removes VerifyLayoutsMD friend.
src/coreclr/md/inc/liteweightstgdb.h Removes custom data source path and layout verifier friendship; adds DacDbiInterfaceImpl friend.
src/coreclr/md/enc/metamodelrw.cpp Deletes FEATURE_METADATA_CUSTOM_DATA_SOURCE implementation (InitOnCustomDataSource).
src/coreclr/md/enc/liteweightstgdbrw.cpp Deletes custom-data-source open path and related guards.
src/coreclr/md/datasource/targettypes.h Deletes legacy target-type deserialization definitions.
src/coreclr/md/datasource/targettypes.cpp Deletes legacy target-type deserialization implementation.
src/coreclr/md/datasource/stdafx.h Deletes datasource PCH header.
src/coreclr/md/datasource/remotemdinternalrwsource.h Deletes RemoteMDInternalRWSource (custom data source).
src/coreclr/md/datasource/remotemdinternalrwsource.cpp Deletes RemoteMDInternalRWSource implementation.
src/coreclr/md/datasource/datatargetreader.h Deletes DataTargetReader (remote read helper).
src/coreclr/md/datasource/datatargetreader.cpp Deletes DataTargetReader implementation.
src/coreclr/md/datasource/CMakeLists.txt Removes datasource library build.
src/coreclr/md/datasource/api.cpp Deletes datasource factory entrypoint.
src/coreclr/md/compiler/verifylayouts.cpp Deletes compilation unit that enforced layout verification.
src/coreclr/md/compiler/regmeta.h Removes OpenExistingMD(IMDCustomDataSource*) declaration.
src/coreclr/md/compiler/regmeta.cpp Removes custom-data-source open path and version-string fallback.
src/coreclr/md/compiler/disp.h Removes IMetaDataDispenserCustom interface and helpers.
src/coreclr/md/compiler/disp.cpp Removes custom-data-source scope opening implementation and QI support.
src/coreclr/md/compiler/CMakeLists.txt Stops building verifylayouts-only WKS variant; uses common sources.
src/coreclr/md/CMakeLists.txt Removes metadata custom data source/debuggee defines and datasource subdir.
src/coreclr/inc/utilcode.h Removes VerifyLayoutsMD friend from CChainedHash.
src/coreclr/inc/shash.h Removes VerifyLayoutsMD friend from SHash.
src/coreclr/inc/metadata.h Removes IMDCustomDataSource/IMetaDataDispenserCustom and related declarations.
src/coreclr/inc/dacdbi.idl Removes defines/version methods from the IDL definition of IDacDbiInterface.
src/coreclr/dlls/mscordbi/CMakeLists.txt Removes mddatasource_dbi from CoreDBI link libraries.
src/coreclr/debug/inc/dacdbiinterface.h Removes defines/version APIs; adds new read-write metadata serialization APIs.
src/coreclr/debug/ee/debugger.h Removes metadata layout defines/version fields.
src/coreclr/debug/ee/debugger.cpp Removes initialization of defines/version fields and version table comment block.
src/coreclr/debug/di/module.cpp Switches to new DacDbi read-write metadata APIs and OpenScopeOnMemory.
src/coreclr/debug/di/CMakeLists.txt Removes custom data source compile defines.
src/coreclr/debug/daccess/dacdbiimpl.h Removes old APIs; adds new metadata serialization APIs + helper declaration.
src/coreclr/debug/daccess/dacdbiimpl.cpp Implements serialization of MDInternalRW metadata into a contiguous ECMA-335 image and new DacDbi entrypoints.
src/coreclr/debug/daccess/CMakeLists.txt Enables FEATURE_METADATA_INTERNAL_APIS for daccess to access internal metadata types.
docs/design/datacontracts/EcmaMetadata.md Updates contract documentation to include GetReadWriteMetadata and revised RW behavior.
docs/design/datacontracts/Debugger.md Updates contract documentation for reduced DebuggerData and removed descriptors.

Copilot's findings

Comments suppressed due to low confidence (1)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:405

  • The copy should size the destination span to the actual blob length, not cbBuffer. Using cbBuffer forces an unchecked (int) cast and can misbehave for large buffers; the contract already validates cbBuffer is at least the blob size.
  • Files reviewed: 56/56 changed files
  • Comments generated: 4

Comment thread src/coreclr/debug/daccess/dacdbiimpl.cpp
Comment thread src/coreclr/debug/inc/dacdbiinterface.h
Copilot AI review requested due to automatic review settings July 19, 2026 06:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (1)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:419

  • This #if DEBUG validation block won’t compile: new byte[cbBuffer] uses a uint length, but array lengths must be int. Also, the span length casts are unchecked. Use checked((int)cbBuffer) consistently here.
  • Files reviewed: 56/56 changed files
  • Comments generated: 4

Comment thread src/coreclr/debug/di/module.cpp
Comment thread src/coreclr/debug/di/module.cpp
Comment thread src/coreclr/debug/daccess/dacdbiimpl.cpp
@rcj1
rcj1 marked this pull request as ready for review July 19, 2026 07:21
@azure-pipelines

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

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
  "version": 5,
  "last_dispatched_commit": "cc878ae8c7c9eeeaa4704da04e246ab688c063f2",
  "last_dispatched_base_ref": "main",
  "last_dispatched_base_sha": "cb84c8521ce6a4cbde63cb1a700fcb850be791ae",
  "last_reviewed_commit": "cc878ae8c7c9eeeaa4704da04e246ab688c063f2",
  "last_reviewed_base_ref": "main",
  "last_reviewed_base_sha": "cb84c8521ce6a4cbde63cb1a700fcb850be791ae",
  "last_recorded_worker_run_id": "29689531871",
  "review_attempt_commit": "",
  "review_attempt_base_ref": "",
  "review_attempt_count": 0,
  "max_review_attempts": 5,
  "review_history_format": "holistic-review-disclosure-v1",
  "review_history": [
    {
      "commit": "cc878ae8c7c9eeeaa4704da04e246ab688c063f2",
      "review_id": 4730879754
    }
  ]
}

@github-actions github-actions Bot 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.

Holistic Review

Motivation: Justified. Out-of-process debuggers previously reconstructed a target's writable (MDInternalRW) metadata by reading raw MD structures via a IMDCustomDataSource/RemoteMDInternalRWSource path, which required the layout-version + defines-bitfield handshake (GetMDStructuresVersion/GetDefinesBitField) and the parallel VerifyLayouts machinery to keep DBI and CLR structure layouts in lockstep. This PR replaces that fragile, layout-coupled path with a DacDbi API that serializes a self-describing ECMA-335 image, deleting ~3000 lines of hard-to-maintain code.

Approach: Sound and consistent. GetReadWriteMetadataSize/FillReadWriteMetadata are added to the (internal, unstable) DacDbi interface; the consumer in CordbModule::RefreshMetaData now allocates a buffer and calls OpenScopeOnMemory(... ofTakeOwnership), removing the custom-data-source dependency entirely. The serialization is implemented both in native DAC (SerializeReadWriteMetadata) and cDAC (EcmaMetadata_1.BuildReadWriteMetadataBlob), and the cDAC DacDbiImpl cross-validates its output byte-for-byte against the legacy DAC under #if DEBUG, giving strong parity confidence. The VerifyLayoutsMD friend declarations, VerifyLayouts.inc/.h, the md/datasource directory, and the now-unused Debugger::m_defines/m_mdDataStructureVersion fields (and their datadescriptor.inc entries + Debugger contract fields) are all removed cleanly; I grepped for dangling references to every removed symbol and found none.

Summary: ✅ LGTM. This is a clean, well-scoped refactor with correct doc updates (EcmaMetadata.md, Debugger.md) matching the contract/abstraction changes as required by the cDAC instructions, appropriate test removal for the deleted GetDefinesBitField/GetMDStructuresVersion APIs, and no behavioral regressions I could find. Per the cDAC guidance for .NET 11 preview branches, the DacDbi COM surface changes are internal/unstable and not treated as breaking. One non-blocking performance suggestion is left inline (the native path serializes the full image twice per refresh, once for size and once for fill, whereas cDAC caches per generation). I could not build or run tests in this review environment, so native compilation and the cDAC cross-validation assertions should be confirmed by CI.


Detailed Findings

✅ Correctness — removed-symbol cleanup is complete

Verified no remaining references to GetDefinesBitField, GetMDStructuresVersion, VerifyLayoutsMD, FEATURE_METADATA_CUSTOM_DATA_SOURCE, FEATURE_METADATA_DEBUGGEE_DATA_SOURCE, CreateRemoteMDInternalRWSource, m_defines, or m_mdDataStructureVersion outside the deletions. RegMeta::GetVersionString correctly drops the now-impossible m_pvMd == NULL fallback branch.

✅ Serialization parity — native vs. cDAC

The native MetadataBlobBuilder/ReadStoragePool logic mirrors the managed BlobBuilder path field-for-field (signature, version string, stream headers, #JTD marker for all-4-byte columns, heap alignment, valid/sorted table masks, row counts). The #if DEBUG cross-validation in DacDbiImpl.FillReadWriteMetadata/GetReadWriteMetadataSize asserts byte-for-byte equality with the legacy DAC, which is the right safety net for this dual implementation.

✅ Bounds/robustness in native serialization

ReadStoragePool guards segment count (MaxPoolSegments) and total size (MaxPoolBytes) and throws CLDB_E_FILE_CORRUPT on overflow; SerializeReadWriteMetadata validates tableCount <= MaxTableCount and clamps the metadata version length. These are appropriate hardening for reading potentially-corrupt target memory.

✅ Documentation & tests

EcmaMetadata.md and Debugger.md are updated to match the new GetReadWriteMetadata API and the trimmed DebuggerData record, satisfying the cDAC doc-sync requirement. Test removals in DebuggerTests.cs and DacDbiDebuggerDumpTests.cs correctly track the deleted fields/APIs; the ERROR_INSUFFICIENT_BUFFER constant is consolidated into CorDbgHResults and reused, removing local duplicates.

💡 Performance (inline) — native path serializes twice per refresh

See inline comment on dacdbiimpl.cpp: CordbModule::RefreshMetaData calls GetReadWriteMetadataSize then FillReadWriteMetadata, each of which fully rebuilds the ECMA-335 image (walking all storage pools). The cDAC side caches per module/generation; the native side could do the same. Non-blocking.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 215.6 AIC · ⌖ 11.6 AIC · ⊞ 10K

Comment thread src/coreclr/debug/daccess/dacdbiimpl.cpp
Comment thread src/coreclr/debug/di/module.cpp

@noahfalk noahfalk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The approach looks fine to me - I'm mostly just curious about testing. We need to ensure that the new behavior is compatible with the old one and that cDAC going forward has test coverage around this.

Comment thread src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs
Copilot AI review requested due to automatic review settings July 21, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (4)

src/coreclr/debug/di/module.cpp:507

  • CoTaskMemAlloc(cbSize) can return null on OOM. If that happens, the subsequent FillReadWriteMetadata call will fail with E_INVALIDARG (null buffer) rather than surfacing an OOM, and OpenScopeOnMemory could also dereference a null pointer depending on the path. Please add an explicit allocation failure check and throw E_OUTOFMEMORY (or equivalent) before calling into DAC/metadata dispenser.
            ULONG32 cbSize = 0;
            IfFailThrow(GetProcess()->GetDAC()->GetReadWriteMetadataSize(m_vmModule, &cbSize));
            CoTaskMemHolder<BYTE> pBuffer{ (BYTE*)CoTaskMemAlloc(cbSize) };

            IfFailThrow(GetProcess()->GetDAC()->FillReadWriteMetadata(m_vmModule, pBuffer, cbSize));
            IMetaDataDispenserEx *  pDisp = GetProcess()->GetDispenser();
            _ASSERTE(pDisp != NULL);
            IfFailThrow(pDisp->OpenScopeOnMemory(pBuffer, cbSize, ofTakeOwnership, IID_IMetaDataImport, (IUnknown**)&m_pIMImport));
            pBuffer.Detach(); // ownership transferred to the IMetaDataImport

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:407

  • new Span<byte>(pBuffer, (int)cbBuffer) can overflow if cbBuffer is > int.MaxValue, and it also overstates the destination length (we only need to copy blob.Length bytes). Using the blob length avoids the cast and keeps the copy bounded to the actual serialized size.
    src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiDebuggerDumpTests.cs:58
  • This test suite removed coverage for the old defines/version APIs, but the PR also introduces new DacDbi APIs (GetReadWriteMetadataSize / FillReadWriteMetadata) that are now used by the debugger (CordbModule::RefreshMetaData). Please add dump-based integration tests that (1) call the new APIs, (2) validate FillReadWriteMetadata returns a blob of the expected size, and (3) optionally validate OpenScopeOnMemory succeeds on the returned image (or at least that the blob begins with the ECMA metadata signature BSJB).
    src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IEcmaMetadata.cs:15
  • This adds a new public contract method (IEcmaMetadata.GetReadWriteMetadata) which expands public surface area. The PR description doesn’t link an api-approved issue/proposal, so it’s unclear whether this new public API has gone through the required approval process. If this API is intended to be internal-only, consider making it internal (or otherwise non-public) until approved; otherwise please link the approved issue and ensure the final approved API shape matches exactly.
  • Files reviewed: 56/56 changed files
  • Comments generated: 1

Copilot AI review requested due to automatic review settings July 22, 2026 23:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (4)

src/coreclr/debug/di/module.cpp:503

  • The CoTaskMemAlloc result isn't checked for NULL before being passed to FillReadWriteMetadata / OpenScopeOnMemory. This can turn an OOM into a null-dereference (or an E_INVALIDARG from DAC) and is inconsistent with CopyRemoteMetaData(), which throws OOM on allocation failure.
            ULONG32 cbSize = 0;
            IfFailThrow(GetProcess()->GetDAC()->GetReadWriteMetadataSize(m_vmModule, &cbSize));
            CoTaskMemHolder<BYTE> pBuffer{ (BYTE*)CoTaskMemAlloc(cbSize) };

            IfFailThrow(GetProcess()->GetDAC()->FillReadWriteMetadata(m_vmModule, pBuffer, cbSize));

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:447

  • This copy uses cbBuffer for the Span length and casts it to int. Since you already validated cbBuffer against blob.Length, you can avoid the potentially-overflowing cast and the unnecessarily-large destination Span by copying exactly blob.Length bytes.
    src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:465
  • This Span length casts cbBuffer (uint) to int without a checked conversion. Even if today callers pass the exact size, this is still an unchecked narrowing conversion in unsafe code; use checked((int)cbBuffer) for robustness.
    src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IEcmaMetadata.cs:16
  • This PR adds new public API surface (IEcmaMetadata.GetReadWriteMetadata). Per dotnet/runtime process, new public APIs require an approved proposal (an issue labeled api-approved with an approved API shape comment) or the API should be made internal/experimental-only pending review. No approved issue is referenced in the PR metadata provided here.
  • Files reviewed: 56/56 changed files
  • Comments generated: 3

Comment thread src/coreclr/debug/daccess/dacdbiimpl.cpp

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (4)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:456

  • cbBuffer is a uint, so new byte[cbBuffer] does not compile (array lengths require int). This will break DEBUG builds.

Cast with checked((int)cbBuffer) (and consider guarding against values > int.MaxValue if that can happen).
src/coreclr/debug/di/module.cpp:503

  • After CoTaskMemAlloc(cbSize), the code doesn't check for allocation failure. If allocation returns NULL, the subsequent DAC call will fail with a misleading HRESULT.

Match CopyRemoteMetaData's pattern and throw OOM when the allocation fails.

            ULONG32 cbSize = 0;
            IfFailThrow(GetProcess()->GetDAC()->GetReadWriteMetadataSize(m_vmModule, &cbSize));
            CoTaskMemHolder<BYTE> pBuffer{ (BYTE*)CoTaskMemAlloc(cbSize) };

            IfFailThrow(GetProcess()->GetDAC()->FillReadWriteMetadata(m_vmModule, pBuffer, cbSize));

src/coreclr/debug/daccess/dacdbiimpl.cpp:4318

  • FillReadWriteMetadata treats a null pBuffer as E_INVALIDARG, but this is a pointer-out parameter and should report E_POINTER (consistent with GetReadWriteMetadataSize, typical COM conventions, and the managed wrappers throwing ArgumentNullExceptionE_POINTER).
        if (pBuffer == NULL)
        {
            ThrowHR(E_INVALIDARG);
        }

src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiDebuggerDumpTests.cs:53

  • The PR introduces new DacDbi APIs (GetReadWriteMetadataSize / FillReadWriteMetadata), but this dump integration test suite no longer exercises any DacDbi metadata path. Adding a basic success + sanity test here would help catch regressions (e.g., non-zero size, Fill succeeds, and the bytes can be opened by a metadata dispenser).

Without coverage, failures in the new serialization path may only show up in product debugging scenarios.

  • Files reviewed: 56/56 changed files
  • Comments generated: 0 new

Copilot AI mentioned this pull request Jul 28, 2026
noahfalk added a commit to dotnet/diagnostics that referenced this pull request Jul 29, 2026
Our tests expect to be able to toggle cDAC support on and off. Although
most tests run with the managed host enabled, some of them explicitly
disable it. Right now if a test disables managed SOS hosting and
attempts to run:

!runtimes --usecdac false

the native host doesn't recognize the option and ignores it. The test
then runs using default native host policy which will load cDAC. This
means tests that claim to be validating the legacy DAC path are actually
validating the SOS bundled cDAC instead. This was blocking Rachel's PR
dotnet/runtime#131027 because she had both
runtime and cDAC changes included but the test was only using the
runtime portion of the change without the matching cDAC portion.
Copilot AI review requested due to automatic review settings July 29, 2026 22:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (4)

src/coreclr/debug/di/module.cpp:503

  • CoTaskMemAlloc(cbSize) can return null; the code then passes a null buffer into FillReadWriteMetadata/OpenScopeOnMemory, which can turn an OOM into a misleading failure or crash. Add an explicit null check and throw OOM (same pattern is already used in CopyRemoteMetaData).
            ULONG32 cbSize = 0;
            IfFailThrow(GetProcess()->GetDAC()->GetReadWriteMetadataSize(m_vmModule, &cbSize));
            CoTaskMemHolder<BYTE> pBuffer{ (BYTE*)CoTaskMemAlloc(cbSize) };

            IfFailThrow(GetProcess()->GetDAC()->FillReadWriteMetadata(m_vmModule, pBuffer, cbSize));

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:447

  • The destination Span length is created from (int)cbBuffer, which can overflow for large buffers and is larger than necessary. Since the API already validated cbBuffer >= blob.Length, the Span length should use blob.Length to avoid the cast and to scope the copy to the actual serialized metadata size.
    src/coreclr/debug/daccess/dacdbiimpl.cpp:4316
  • GetReadWriteMetadataSize returns E_POINTER for a null out-parameter, but FillReadWriteMetadata returns E_INVALIDARG for a null output buffer. For consistency (and to match the managed Legacy shim behavior), a null output buffer should return E_POINTER.
    EX_TRY
    {
        if (pBuffer == NULL)
        {

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IEcmaMetadata.cs:16

  • This adds new public API surface (IEcmaMetadata.GetReadWriteMetadata) and the PR also changes existing public contract surface (DebuggerData shape) without any linked api-approved issue/proposal in the PR description. dotnet/runtime requires approved API review for new public APIs; please link the approved issue or make the new surface internal/pending-review.
  • Files reviewed: 56/56 changed files
  • Comments generated: 0 new

Copilot AI review requested due to automatic review settings July 29, 2026 22:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (7)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:449

  • Copying the metadata blob into a destination span sized to cbBuffer forces an (int) cast and can throw for large cbBuffer values, and it also writes an undefined tail when cbBuffer > blob.Length. Since the API only promises to fill blob.Length bytes, copy into a span sized to blob.Length instead.
    src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:468
  • DEBUG cross-validation compares the entire cbBuffer even though FillReadWriteMetadata is only required to write the first N bytes of the blob. If a caller passes a larger cbBuffer, the tail of pBuffer may contain unrelated data and cause a false mismatch. Compare only the expected blob size.
    src/coreclr/debug/di/module.cpp:503
  • CoTaskMemAlloc(cbSize) can return NULL on OOM; the current code would then pass a null buffer to FillReadWriteMetadata and lose the real failure reason (and may return E_INVALIDARG instead of E_OUTOFMEMORY). Check the allocation result and throw/return E_OUTOFMEMORY explicitly.
            ULONG32 cbSize = 0;
            IfFailThrow(GetProcess()->GetDAC()->GetReadWriteMetadataSize(m_vmModule, &cbSize));
            CoTaskMemHolder<BYTE> pBuffer{ (BYTE*)CoTaskMemAlloc(cbSize) };
            if (pBuffer == NULL)
            {

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IEcmaMetadata.cs:17

  • IEcmaMetadata is public, and this PR adds a new public member (GetReadWriteMetadata). New public APIs require an approved API proposal (api-approved issue + approved API shape). The PR description doesn’t reference any such issue, so this can’t be validated and is a merge blocker unless the API is made non-public or an approved issue is linked.
    src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs:9
  • DebuggerData is public and this change removes previously exposed members (DefinesBitField, MDStructuresVersion). That’s a breaking public API change and should go through the same API-approval process (or be made non-public) before it can merge.
    src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:433
  • New DacDbi APIs GetReadWriteMetadataSize/FillReadWriteMetadata are introduced here but there are no tests exercising them (no usages found under src/native/managed/cdac/tests). Adding at least one dump-based integration test that calls both APIs and validates the buffer can be opened by a MetadataReader would help prevent regressions.
    src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:438
  • FillReadWriteMetadata throws ArgumentNullException for a null pBuffer, which maps to E_POINTER. The native DacDbiInterfaceImpl::FillReadWriteMetadata returns E_INVALIDARG for a null buffer, so the DEBUG cross-validation (ValidateHResult) can assert due to mismatched HRESULTs. Use ArgumentException here to return E_INVALIDARG and match the legacy/native behavior.
  • Files reviewed: 56/56 changed files
  • Comments generated: 0 new

Copilot AI review requested due to automatic review settings July 29, 2026 22:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (4)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs:464

  • The DEBUG cross-check allocates new byte[cbBuffer], but cbBuffer is a uint, which is not a valid array length type in C#. This won’t compile.
    src/coreclr/debug/daccess/dacdbiimpl.cpp:3919
  • SerializeReadWriteMetadata bounds-checks tableCount against MaxTableCount = 64, but then indexes pMiniMd->m_Schema.m_cRecs[i] and related metadata arrays which are sized to TBL_COUNT (ECMA-335 table count). If m_TblCount were ever > TBL_COUNT, this would read past the end of those arrays in DAC code.
    const ULONG64 MaxPoolBytes = 100000000;
    const ULONG32 MaxPoolSegments = 1000;
    const ULONG32 MaxTableCount = 64;

src/coreclr/debug/di/module.cpp:511

  • This PR introduces a new dump-debugging metadata path that depends on the new DAC APIs GetReadWriteMetadataSize/FillReadWriteMetadata, but there are no tests covering the new API pair (success path + insufficient-buffer behavior) or validating that the returned blob can be opened as metadata. Adding a dump test would help catch regressions in the serialization logic.
            _ASSERTE(m_pIMImport == NULL);
            ULONG32 cbSize = 0;
            IfFailThrow(GetProcess()->GetDAC()->GetReadWriteMetadataSize(m_vmModule, &cbSize));
            CoTaskMemHolder<BYTE> pBuffer{ (BYTE*)CoTaskMemAlloc(cbSize) };
            if (pBuffer == NULL)
            {
                ThrowOutOfMemory();
            }

            IfFailThrow(GetProcess()->GetDAC()->FillReadWriteMetadata(m_vmModule, pBuffer, cbSize));
            IMetaDataDispenserEx *  pDisp = GetProcess()->GetDispenser();
            _ASSERTE(pDisp != NULL);
            IfFailThrow(pDisp->OpenScopeOnMemory(pBuffer, cbSize, ofTakeOwnership, IID_IMetaDataImport, (IUnknown**)&m_pIMImport));
            pBuffer.Detach(); // ownership transferred to the IMetaDataImport

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IEcmaMetadata.cs:17

  • This adds a new public contract method (GetReadWriteMetadata). Per the repo’s API approval gate for new public surface, the PR needs to link an api-approved issue (with the approved API shape) or the API should be made non-public until it’s been through that process.
  • Files reviewed: 56/56 changed files
  • Comments generated: 0 new

Copilot AI review requested due to automatic review settings July 29, 2026 23:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Comments suppressed due to low confidence (2)

src/coreclr/debug/daccess/dacdbiimpl.cpp:4133

  • ReadStoragePool expects a StgPool/StgPoolSeg-style object at the provided address, but CMiniMdRW::m_Tables is an array of MetaData::TableRW, which contains its RecordPool storage as a member. The current code passes the address of the TableRW object itself, which relies on TableRW's m_RecordStorage being located at offset 0. That layout dependency is brittle and could silently break if TableRW ever grows a leading field.

Prefer passing the address of the TableRW::m_RecordStorage member explicitly so the reader is anchored to the correct subobject regardless of layout changes.

    NewArrayHolder<PoolData> tables = new PoolData[tableCount == 0 ? 1 : tableCount];
    TADDR tablesBase = PTR_HOST_MEMBER_TADDR(CMiniMdRW, pMiniMd, m_Tables);
    ULONG32 tableStride = (ULONG32)sizeof(MetaData::TableRW);
    for (ULONG32 i = 0; i < tableCount; i++)
    {
        ReadStoragePool(tablesBase + i * tableStride, tables[i]);
    }

src/coreclr/debug/di/module.cpp:511

  • This path calls GetReadWriteMetadataSize and then FillReadWriteMetadata back-to-back. In the current DAC implementation, both methods reconstruct the serialized metadata blob via SerializeReadWriteMetadata, so RefreshMetaData ends up walking the target metadata and allocating/copying twice per refresh.

Consider an API/implementation shape that allows retrieving the bytes in a single pass (for example, a TargetBuffer-style API where DAC allocates once, or a Fill... API that can return the required size without rebuilding).

            _ASSERTE(m_pIMImport == NULL);
            ULONG32 cbSize = 0;
            IfFailThrow(GetProcess()->GetDAC()->GetReadWriteMetadataSize(m_vmModule, &cbSize));
            CoTaskMemHolder<BYTE> pBuffer{ (BYTE*)CoTaskMemAlloc(cbSize) };
            if (pBuffer == NULL)
            {
                ThrowOutOfMemory();
            }

            IfFailThrow(GetProcess()->GetDAC()->FillReadWriteMetadata(m_vmModule, pBuffer, cbSize));
            IMetaDataDispenserEx *  pDisp = GetProcess()->GetDispenser();
            _ASSERTE(pDisp != NULL);
            IfFailThrow(pDisp->OpenScopeOnMemory(pBuffer, cbSize, ofTakeOwnership, IID_IMetaDataImport, (IUnknown**)&m_pIMImport));
            pBuffer.Detach(); // ownership transferred to the IMetaDataImport
  • Files reviewed: 57/57 changed files
  • Comments generated: 0 new

@rcj1
rcj1 enabled auto-merge (squash) July 30, 2026 02:06
@rcj1
rcj1 merged commit 7a6987d into dotnet:main Jul 30, 2026
140 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants