Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ If you make code changes, do not complete without checking the relevant code bui

Before completing, use the `code-review` skill to review your code changes. Any issues flagged as errors or warnings should be addressed before completing.

Before making changes to a directory, search for `README.md` files in that directory and its parent directories up to the repository root. Read any you find — they contain conventions, patterns, and architectural context relevant to your work.

Copy link
Member Author

Choose a reason for hiding this comment

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

@stephentoub - Since earlier you mentioned context window sizes were a concern, any thoughts on using something like this as a mechanism for progressive discovery? I wanted to add more component-specific guidance (see READMEs elsewhere in this PR as an example) but I didn't want to bloat the context window for prompts working in unrelated parts of the code.

If the changes are intended to improve performance, or if they could negatively impact performance, use the `performance-benchmark` skill to validate the impact before completing.

You MUST follow all code-formatting and naming conventions defined in [`.editorconfig`](/.editorconfig).
Expand Down
12 changes: 8 additions & 4 deletions docs/design/datacontracts/Thread.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The contract additionally depends on these data descriptors
| `Thread` | `ExceptionTracker` | Pointer to exception tracking information |
| `Thread` | `RuntimeThreadLocals` | Pointer to some thread-local storage |
| `Thread` | `ThreadLocalDataPtr` | Pointer to thread local data structure |
| `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data |
| `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data (optional, Windows only) |
| `ThreadLocalData` | `NonCollectibleTlsData` | Count of non-collectible TLS data entries |
| `ThreadLocalData` | `NonCollectibleTlsArrayData` | Pointer to non-collectible TLS array data |
| `ThreadLocalData` | `CollectibleTlsData` | Count of collectible TLS data entries |
Expand Down Expand Up @@ -271,7 +271,9 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer)
}
else
{
readFrom = target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */);
readFrom = /* Has Thread::UEWatsonBucketTrackerBuckets offset */
? target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */)
: TargetPointer.Null;
if (readFrom == TargetPointer.Null)
{
readFrom = target.ReadPointer(exceptionTrackerPtr + /* ExceptionInfo::ExceptionWatsonBucketTrackerBuckets offset */);
Expand All @@ -284,13 +286,15 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer)
}
else
{
readFrom = target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */);
readFrom = /* Has Thread::UEWatsonBucketTrackerBuckets offset */
? target.ReadPointer(threadPointer + /* Thread::UEWatsonBucketTrackerBuckets offset */)
: TargetPointer.Null;
}

Span<byte> span = new byte[_target.ReadGlobal<uint>("SizeOfGenericModeBlock")];
if (readFrom == TargetPointer.Null)
return Array.Empty<byte>();

_target.ReadBuffer(readFrom, span);
return span.ToArray();
}
Expand Down
11 changes: 11 additions & 0 deletions src/coreclr/debug/daccess/cdac.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,19 @@ namespace
{
// Load cdac from next to current module (DAC binary)
PathString path;

// On Unix, GetCurrentModuleBase() returns a raw dladdr base address, not a PAL HMODULE.
// The DAC is typically loaded externally (e.g. by CLRMD via dlopen) and is not registered
// in the PAL module list. Use PAL_GetPalHostModule() which properly registers the module.
#ifdef HOST_UNIX
HMODULE hMod = PAL_GetPalHostModule();
if (hMod == NULL || WszGetModuleFileName(hMod, path) == 0)
#else
if (WszGetModuleFileName((HMODULE)GetCurrentModuleBase(), path) == 0)
#endif
{
return false;
}

SString::Iterator iter = path.End();
if (!path.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W))
Expand Down
13 changes: 3 additions & 10 deletions src/coreclr/debug/daccess/request.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1581,7 +1581,6 @@ ClrDataAccess::GetObjectStringData(CLRDATA_ADDRESS obj, unsigned int count, _Ino
PTR_StringObject str(TO_TADDR(obj));
ULONG32 needed = (ULONG32)str->GetStringLength() + 1;

HRESULT hr;
if (stringData && count > 0)
{
if (count > needed)
Expand Down Expand Up @@ -3373,7 +3372,7 @@ HRESULT ClrDataAccess::GetHandleEnumForTypes(unsigned int types[], unsigned int

DacHandleWalker *walker = new DacHandleWalker();

HRESULT hr = walker->Init(this, types, count);
hr = walker->Init(this, types, count);

if (SUCCEEDED(hr))
hr = walker->QueryInterface(__uuidof(ISOSHandleEnum), (void**)ppHandleEnum);
Expand All @@ -3400,7 +3399,7 @@ HRESULT ClrDataAccess::GetHandleEnumForGC(unsigned int gen, ISOSHandleEnum **ppH

DacHandleWalker *walker = new DacHandleWalker();

HRESULT hr = walker->Init(this, types, ARRAY_SIZE(types), gen);
hr = walker->Init(this, types, ARRAY_SIZE(types), gen);
if (SUCCEEDED(hr))
hr = walker->QueryInterface(__uuidof(ISOSHandleEnum), (void**)ppHandleEnum);

Expand Down Expand Up @@ -4871,7 +4870,6 @@ HRESULT ClrDataAccess::GetGenerationTable(unsigned int cGenerations, struct Dacp

SOSDacEnter();

HRESULT hr = S_OK;
unsigned int numGenerationTableEntries = (unsigned int)(g_gcDacGlobals->total_generation_count);
if (pNeeded != NULL)
{
Expand Down Expand Up @@ -4917,7 +4915,6 @@ HRESULT ClrDataAccess::GetFinalizationFillPointers(unsigned int cFillPointers, C

SOSDacEnter();

HRESULT hr = S_OK;
unsigned int numFillPointers = (unsigned int)(g_gcDacGlobals->total_generation_count + dac_finalize_queue::ExtraSegCount);
if (pNeeded != NULL)
{
Expand Down Expand Up @@ -4958,7 +4955,6 @@ HRESULT ClrDataAccess::GetGenerationTableSvr(CLRDATA_ADDRESS heapAddr, unsigned

SOSDacEnter();

HRESULT hr = S_OK;
#ifdef FEATURE_SVR_GC
unsigned int numGenerationTableEntries = (unsigned int)(g_gcDacGlobals->total_generation_count);
if (pNeeded != NULL)
Expand Down Expand Up @@ -5008,7 +5004,6 @@ HRESULT ClrDataAccess::GetFinalizationFillPointersSvr(CLRDATA_ADDRESS heapAddr,

SOSDacEnter();

HRESULT hr = S_OK;
#ifdef FEATURE_SVR_GC
unsigned int numFillPointers = (unsigned int)(g_gcDacGlobals->total_generation_count + dac_finalize_queue::ExtraSegCount);
if (pNeeded != NULL)
Expand Down Expand Up @@ -5158,7 +5153,7 @@ HRESULT ClrDataAccess::GetObjectComWrappersData(CLRDATA_ADDRESS objAddr, CLRDATA
SOSDacEnter();

// Default to having found no information.
HRESULT hr = S_FALSE;
hr = S_FALSE;

if (pNeeded != NULL)
{
Expand Down Expand Up @@ -5225,8 +5220,6 @@ HRESULT ClrDataAccess::GetObjectComWrappersData(CLRDATA_ADDRESS objAddr, CLRDATA
}
}

hr = S_FALSE;

SOSDacLeave();
return hr;
#else // FEATURE_COMWRAPPERS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public Thread(Target target, TargetPointer address)

// Address of the exception tracker
ExceptionTracker = address + (ulong)type.Fields[nameof(ExceptionTracker)].Offset;
// UEWatsonBucketTrackerBuckets does not exist on certain platforms
// UEWatsonBucketTrackerBuckets does not exist on non-Windows platforms
UEWatsonBucketTrackerBuckets = type.Fields.TryGetValue(nameof(UEWatsonBucketTrackerBuckets), out Target.FieldInfo watsonFieldInfo)
? target.ReadPointer(address + (ulong)watsonFieldInfo.Offset)
: TargetPointer.Null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,17 @@ int IXCLRDataStackWalk.Next()
hr = ex.HResult;
}

#if DEBUG
// Advance the legacy stack walk to keep it in sync with the cDAC walk.
// GetFrame() passes the legacy frame to ClrDataFrame, which delegates
// GetArgumentByIndex/GetLocalVariableByIndex to it. If we don't advance
// the legacy walk here, those calls operate on the wrong frame.
if (_legacyImpl is not null)
{
int hrLocal = _legacyImpl.Next();
#if DEBUG
Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}");
}
#endif
}

return hr;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Microsoft.Diagnostics.DataContractReader.Legacy

This project contains `SOSDacImpl`, which implements the `ISOSDacInterface*` and
`IXCLRDataProcess` COM-style APIs by delegating to the cDAC contract layer.

## Implementing a new SOSDacImpl method

When a method currently delegates to `_legacyImpl` (returning `E_NOTIMPL` when null),
replace it with a cDAC implementation following this pattern:

```csharp
int ISOSDacInterface8.ExampleMethod(uint* pResult)
{
// 1. Validate pointer arguments before the try block
if (pResult == null)
return HResults.E_INVALIDARG;

int hr = HResults.S_OK;
try
{
// 2. Get the relevant contract and call it
IGC gc = _target.Contracts.GC;
*pResult = gc.SomeMethod();
}
catch (System.Exception ex)
{
hr = ex.HResult;
}

// 3. Cross-validate with legacy DAC in debug builds
#if DEBUG
if (_legacyImpl8 is not null)
{
uint resultLocal;
int hrLocal = _legacyImpl8.ExampleMethod(&resultLocal);
Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}");
if (hr == HResults.S_OK)
{
Debug.Assert(*pResult == resultLocal);
}
}
#endif
return hr;
}
```

### Key conventions

- **HResult returns**: Methods return `int` HResult codes, not exceptions.
Use `HResults.S_OK`, `HResults.S_FALSE`, `HResults.E_INVALIDARG`, etc.
- **Null pointer checks**: Validate output pointer arguments *before* the try block
and return `E_INVALIDARG`. This matches the native DAC behavior.
- **Exception handling**: Wrap all contract calls in try/catch. The catch converts
exceptions to HResult codes via `ex.HResult`. When the native DAC has an explicit
readability check (e.g., `ptr.IsValid()` or `DACGetMethodTableFromObjectPointer`
returning NULL), catch `VirtualReadException` specifically and return the same
HResult the native DAC returns (typically `E_INVALIDARG`). Avoid catching all
exceptions and mapping to a single HRESULT, as this can mask unrelated bugs.
- **Debug cross-validation**: In `#if DEBUG`, call the legacy implementation (if
available) and assert the results match. This catches discrepancies during testing.

### Legacy delegation placement

Some cDAC methods create child objects (e.g., `ClrDataMethodInstance`,
`ClrDataFrame`) that delegate certain operations to a legacy counterpart. This is
a temporary implementation workaround to let us create the cDAC incrementally that
should be removed before cDAC ships to customers. In these cases, the legacy call
that obtains the counterpart **must be outside `#if DEBUG`**, because the result is
used functionally, not just for validation.

For example, `EnumMethodInstanceByAddress` passes `legacyMethod` to
`ClrDataMethodInstance`, which delegates `GetTokenAndScope` and other calls to it.
If the legacy enumeration only runs inside `#if DEBUG`, those delegated calls fail
in Release builds.

**Rule of thumb**: if a legacy call's result is stored and passed to another
object, keep it outside `#if DEBUG`. Only the assertion that compares
HResults/values belongs inside `#if DEBUG`.

### Sized-buffer protocol

Several `ISOSDacInterface8` methods use a two-call pattern where the caller first
queries the needed buffer size, then calls again with a sufficiently large buffer:

```csharp
int GetSomeTable(uint count, Data* buffer, uint* pNeeded)
```

The protocol is:
1. Always set `*pNeeded` to the required count (if `pNeeded` is not null).
2. If `count > 0 && buffer == null`: return `E_INVALIDARG`.
3. If `count < needed`: return `S_FALSE` (buffer too small, but `*pNeeded` is set).
4. If `count >= needed`: populate `buffer` and return `S_OK`.

This matches the native implementation in `src/coreclr/debug/daccess/request.cpp`.

### Pointer conversions

- `TargetPointer` → `ClrDataAddress`: use `pointer.ToClrDataAddress(_target)`.
On 32-bit targets, this **sign-extends** the value (e.g., `0xAA000000` becomes
`0xFFFFFFFF_AA000000`). This matches native DAC behavior.
- `ClrDataAddress` → `TargetPointer`: use `address.ToTargetPointer(_target)`.

Both are defined in `ConversionExtensions.cs`.
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,15 @@ int IXCLRDataProcess.StartEnumMethodInstancesByAddress(ClrDataAddress address, /
int hr = HResults.S_FALSE;
*handle = 0;

// Start the legacy enumeration to keep it in sync with the cDAC enumeration.
// EnumMethodInstanceByAddress passes the legacy method instance to ClrDataMethodInstance,
// which delegates some operations to it.
ulong handleLocal = default;
#if DEBUG
int hrLocal = default;
if (_legacyProcess is not null)
{
hrLocal = _legacyProcess.StartEnumMethodInstancesByAddress(address, appDomain, &handleLocal);
}
#endif

try
{
Expand Down Expand Up @@ -387,17 +388,16 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet
GCHandle gcHandle = GCHandle.FromIntPtr((IntPtr)(*handle));
if (gcHandle.Target is not EnumMethodInstances emi) return HResults.E_INVALIDARG;

// Advance the legacy enumeration to keep it in sync with the cDAC enumeration.
// The legacy method instance is passed to ClrDataMethodInstance for delegation.
IXCLRDataMethodInstance? legacyMethod = null;

#if DEBUG
int hrLocal = default;
if (_legacyProcess is not null)
{
ulong legacyHandle = emi.LegacyHandle;
hrLocal = _legacyProcess.EnumMethodInstanceByAddress(&legacyHandle, out legacyMethod);
emi.LegacyHandle = legacyHandle;
}
#endif

try
{
Expand All @@ -413,7 +413,28 @@ int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, out IXCLRDataMet
}
catch (System.Exception ex)
{
hr = ex.HResult;
// The cDAC's IterateMethodInstances() implementation is incomplete compared
// to the native DAC's EnumMethodInstances::Next(). The native DAC uses a
// MethodIterator backed by AppDomain assembly iteration with EX_TRY/EX_CATCH
// error handling around each step. The cDAC re-implements this with
// IterateModules()/IterateMethodInstantiations()/IterateTypeParams() which
// call into IRuntimeTypeSystem and ILoader contracts. These contract calls
// (e.g. GetMethodTable, GetTypeHandle, GetMethodDescForSlot, GetModule,
// GetTypeDefToken) can throw when encountering method descs or type handles
// from assemblies/modules that the cDAC cannot fully process. This has been
// observed for generic method instantiations (cases 2-4 in
// IterateMethodInstances) in the SOS.WebApp3 integration test.
//
// Fall back to the legacy DAC result when available, otherwise propagate the error.
if (_legacyProcess is not null)
{
hr = hrLocal;
method = legacyMethod;
}
else
{
hr = ex.HResult;
}
}

#if DEBUG
Expand Down
Loading
Loading