From 851dc52e6b12c6876b966d872b0047beb9ec71d4 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 22 Apr 2026 15:14:22 -0400 Subject: [PATCH 1/3] update commit --- .../design/datacontracts/CodeNotifications.md | 127 +++++++ docs/design/datacontracts/Notifications.md | 5 +- src/coreclr/debug/daccess/cdac.cpp | 31 +- src/coreclr/debug/daccess/daccess.cpp | 2 +- src/coreclr/vm/cdacstress.cpp | 4 +- .../vm/datadescriptor/datadescriptor.inc | 10 + src/coreclr/vm/util.hpp | 7 +- .../ContractRegistry.cs | 4 + .../Contracts/ICodeNotifications.cs | 57 ++++ .../DataType.cs | 1 + .../Target.cs | 28 ++ .../TargetFieldExtensions.cs | 43 +++ .../Constants.cs | 2 + .../Contracts/CodeNotifications_1.cs | 238 +++++++++++++ .../CoreCLRContracts.cs | 1 + .../Data/JITNotification.cs | 63 ++++ .../ClrDataMethodDefinition.cs | 46 ++- .../ClrDataModule.cs | 2 + .../CodeNotificationFlagsConverter.cs | 55 +++ .../IXCLRData.cs | 24 +- .../LegacyFallbackHelper.cs | 3 - .../SOSDacImpl.IXCLRDataProcess.cs | 147 +++++++- .../ContractDescriptorTarget.cs | 45 ++- src/native/managed/cdac/inc/cdac_reader.h | 3 + .../mscordaccore_universal/Entrypoints.cs | 52 +++ .../cdac/tests/CodeNotificationsTests.cs | 323 ++++++++++++++++++ .../ContractDescriptorBuilder.cs | 2 +- .../cdac/tests/DumpTests/DumpTestBase.cs | 1 + .../cdac/tests/TestPlaceholderTarget.cs | 39 ++- src/tools/StressLogAnalyzer/src/Program.cs | 1 + 30 files changed, 1330 insertions(+), 36 deletions(-) create mode 100644 docs/design/datacontracts/CodeNotifications.md create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/JITNotification.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/CodeNotificationFlagsConverter.cs create mode 100644 src/native/managed/cdac/tests/CodeNotificationsTests.cs diff --git a/docs/design/datacontracts/CodeNotifications.md b/docs/design/datacontracts/CodeNotifications.md new file mode 100644 index 00000000000000..724a139f82fead --- /dev/null +++ b/docs/design/datacontracts/CodeNotifications.md @@ -0,0 +1,127 @@ +# Contract CodeNotifications + +This contract provides read/write access to the in-target JIT code notification +allowlist. The runtime consults this table when JIT-compiling or discarding a +method; if the (module, methodToken) pair is present with a non-zero flag set, +the runtime raises a `DEBUG_CODE_NOTIFICATION` event so the debugger/DAC can +observe JIT events for that method. + +Unlike the [Notifications](Notifications.md) contract (which only decodes +events raised by the runtime), this contract writes into the target process +and may lazily allocate the notification table when needed. + +## APIs of contract + +``` csharp +/// Notification flag set. Mapped to/from the native `CLRDATA_METHNOTIFY_*` values at the +/// IXCLRData COM boundary (None=0, Generated=1, Discarded=2). +[Flags] +public enum CodeNotificationKind : uint { None = 0, Generated = 1, Discarded = 2 } + +// Set the JIT code notification flags for a specific method. +void SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags); + +// Get the JIT code notification flags for a specific method. Returns None for both an unset +// method and an unallocated in-target table — the cDAC cannot (and does not need to) +// distinguish the two, since "no notifications present" is the same observable state. +CodeNotificationKind GetCodeNotification(TargetPointer module, uint methodToken); + +// Set notification flags for all methods in a module, or all methods if module is null. +void SetAllCodeNotifications(TargetPointer module, CodeNotificationKind flags); +``` + +## Version 1 + +Data descriptors used: +| Data Descriptor Name | Field | Type | Purpose | +| --- | --- | --- | --- | +| `JITNotification` | `State` | uint16 | Notification flags (CLRDATA_METHNOTIFY_*) | +| `JITNotification` | `ClrModule` | nuint | Target pointer to the module | +| `JITNotification` | `MethodToken` | uint32 | Method metadata token | + +Global variables used: +| Global Name | Type | Purpose | +| --- | --- | --- | +| `JITNotificationTable` | TargetPointer | Pointer to the `g_pNotificationTable` array of `JITNotification` entries | +| `JITNotificationTableSize` | uint32 | Maximum number of entries in the notification table (excluding bookkeeping) | + +Contracts used: none + +The JIT notification table is an array of `JITNotification` structs. Index 0 is reserved for +bookkeeping: its `MethodToken` field stores the current entry count (length). The table capacity +is a compile-time invariant exposed via the `JITNotificationTableSize` global, so slot 0's +`ClrModule` and `State` fields are unused. Actual entries start at index 1. + +On Windows, the table starts as NULL (`g_pNotificationTable == 0`). On Unix, it is pre-allocated +at startup; the runtime's `new JITNotification[1001]` default-constructs every slot with +`state = 0`, `clrModule = 0`, `methodToken = 0`, so slot 0's length starts at 0 naturally. +The contract handles both cases uniformly: +- **GetCodeNotification** returns `CodeNotificationKind.None` when the table is NULL, which + is the same value returned when the method is not registered. The cDAC does not distinguish + "table absent" from "entry absent" — both are surfaced as "no notifications for this method". + This is a deliberate simplification from the legacy DAC, which returned `E_OUTOFMEMORY` when + `JITNotifications::IsActive()` was false; the information to the caller is semantically the + same in both cases. +- **SetAllCodeNotifications** is a no-op when the table is NULL. +- **SetCodeNotification** with `CodeNotificationKind.None` is a no-op when the table is NULL. +- **SetCodeNotification** with a non-zero flag lazily allocates the table via `Target.AllocateMemory`, + zero-fills it (so slot 0's length starts at 0), and writes the pointer back to + `g_pNotificationTable`. If the in-target table is full, a `COMException` with + `HResult == E_FAIL` is thrown, matching the legacy DAC's `SetNotification` failure path. + If `AllocateMemory` is not available (e.g., when the debugger host does not support + `ICLRDataTarget2`), a `NotImplementedException` is thrown. + +This contract doesn't currently offer a capacity check, so consumers won't be able to +confirm in advance whether a batch of notification updates will all succeed. If a batch +would overflow the in-target table, the contract writes as many entries as fit and then +throws `COMException` with `HResult == E_FAIL` from the first `SetCodeNotification` +that cannot allocate a slot. + +``` csharp +void SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags) +{ + // Read g_pNotificationTable pointer + TargetPointer tablePointer = target.ReadPointer( + target.ReadGlobalPointer("JITNotificationTable")); + + if (tablePointer == null) + { + if (flags == CodeNotificationKind.None) return; // nothing to clear + // Lazily allocate via Target.AllocateMemory + tablePointer = AllocateAndInitializeTable(); + } + + // Read bookkeeping from index 0 (length only; capacity comes from the global). + uint length = Read(tablePointer + MethodTokenOffset); + uint capacity = target.ReadGlobal("JITNotificationTableSize"); + ulong entriesBase = tablePointer + entrySize; + + if (flags == CodeNotificationKind.None) + { + // Find and clear the matching entry + } + else + { + // Find existing entry and update, or find free slot and insert. + // If no free slot is found: throw COMException with HResult = E_FAIL. + } +} + +CodeNotificationKind GetCodeNotification(TargetPointer module, uint methodToken) +{ + // Returns CodeNotificationKind.None both when the table is NULL and when the method + // is not registered. The cDAC does not distinguish these cases (legacy DAC did, via + // E_OUTOFMEMORY vs. S_OK+None); the observable state is the same. +} + +void SetAllCodeNotifications(TargetPointer module, CodeNotificationKind flags) +{ + // If table pointer is NULL, return (no-op) + // Iterate all active entries; if module is non-null, filter by module + // Set or clear each matching entry's flags. + // When clearing (flags == None), trim trailing free entries from the stored length. + // This deliberately diverges from JITNotifications::SetAllNotifications in + // src/coreclr/vm/util.cpp, whose length algorithm can orphan entries from other modules + // that sit past the trimmed length. +} +``` diff --git a/docs/design/datacontracts/Notifications.md b/docs/design/datacontracts/Notifications.md index 90d15bd44d7e14..71a23f2567131d 100644 --- a/docs/design/datacontracts/Notifications.md +++ b/docs/design/datacontracts/Notifications.md @@ -1,6 +1,6 @@ # Contract Notifications -This contract is for debugger notifications. +This contract is for decoding debugger notifications raised by the runtime. ## APIs of contract @@ -14,6 +14,9 @@ void SetGcNotification(int condemnedGeneration); bool TryParseNotification(ReadOnlySpan exceptionInformation, out NotificationData? notification); ``` +Management of the JIT code-notification allowlist is a separate contract, see +[CodeNotifications](CodeNotifications.md). + ## Version 1 Data descriptors used: none diff --git a/src/coreclr/debug/daccess/cdac.cpp b/src/coreclr/debug/daccess/cdac.cpp index 6c84fbe688e091..0ea7990fd880cb 100644 --- a/src/coreclr/debug/daccess/cdac.cpp +++ b/src/coreclr/debug/daccess/cdac.cpp @@ -78,6 +78,28 @@ namespace return S_OK; } + + int AllocVirtualCallback(uint32_t size, uint64_t* allocatedAddress, void* context) + { + ICorDebugDataTarget* target = reinterpret_cast(context); + ICLRDataTarget2* target2 = nullptr; + HRESULT hr = target->QueryInterface(__uuidof(ICLRDataTarget2), (void**)&target2); + if (FAILED(hr)) + { + *allocatedAddress = 0; + return hr; + } + + CLRDATA_ADDRESS addr = 0; + hr = target2->AllocVirtual(0, size, MEM_COMMIT, PAGE_READWRITE, &addr); + target2->Release(); + *allocatedAddress = addr; + if (FAILED(hr)) + { + *allocatedAddress = 0; + } + return hr; + } } CDAC CDAC::Create(uint64_t descriptorAddr, ICorDebugDataTarget* target, IUnknown* legacyImpl) @@ -89,8 +111,15 @@ CDAC CDAC::Create(uint64_t descriptorAddr, ICorDebugDataTarget* target, IUnknown decltype(&cdac_reader_init) init = reinterpret_cast(::GetProcAddress(cdacLib, "cdac_reader_init")); _ASSERTE(init != nullptr); + // Check if the target supports memory allocation (ICLRDataTarget2) + ICLRDataTarget2* target2 = nullptr; + auto allocCallback = (target->QueryInterface(__uuidof(ICLRDataTarget2), (void**)&target2) == S_OK) + ? &AllocVirtualCallback : nullptr; + if (target2 != nullptr) + target2->Release(); + intptr_t handle; - if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContext, target, &handle) != 0) + if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContext, allocCallback, target, &handle) != 0) { ::FreeLibrary(cdacLib); return {}; diff --git a/src/coreclr/debug/daccess/daccess.cpp b/src/coreclr/debug/daccess/daccess.cpp index 0c8801da35fe27..222c8cf9eba00f 100644 --- a/src/coreclr/debug/daccess/daccess.cpp +++ b/src/coreclr/debug/daccess/daccess.cpp @@ -6019,7 +6019,7 @@ ClrDataAccess::GetHostJitNotificationTable() if (m_jitNotificationTable == NULL) { m_jitNotificationTable = - JITNotifications::InitializeNotificationTable(1000); + JITNotifications::InitializeNotificationTable(JIT_NOTIFICATION_TABLE_SIZE); } return m_jitNotificationTable; diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index fbf019d01c7d58..b750c069891af0 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -325,8 +325,8 @@ bool CdacStress::Initialize() // Get the address of the contract descriptor in our own process uint64_t descriptorAddr = reinterpret_cast(&DotNetRuntimeContractDescriptor); - // Initialize the cDAC reader with in-process callbacks - if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContextCallback, nullptr, &s_cdacHandle) != 0) + // Initialize the cDAC reader with in-process callbacks (no alloc_virtual for in-process stress) + if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContextCallback, nullptr, nullptr, &s_cdacHandle) != 0) { LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: cdac_reader_init failed\n")); ::FreeLibrary(s_cdacModule); diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index be78f9c1bf6028..ba023274638a2b 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -155,6 +155,13 @@ CDAC_TYPE_BEGIN(ObjectHandle) CDAC_TYPE_SIZE(sizeof(OBJECTHANDLE)) CDAC_TYPE_END(ObjectHandle) +CDAC_TYPE_BEGIN(JITNotification) +CDAC_TYPE_SIZE(sizeof(::JITNotification)) +CDAC_TYPE_FIELD(JITNotification, T_UINT16, State, offsetof(::JITNotification, state)) +CDAC_TYPE_FIELD(JITNotification, T_NUINT, ClrModule, offsetof(::JITNotification, clrModule)) +CDAC_TYPE_FIELD(JITNotification, T_UINT32, MethodToken, offsetof(::JITNotification, methodToken)) +CDAC_TYPE_END(JITNotification) + // Object CDAC_TYPE_BEGIN(Object) @@ -1448,6 +1455,8 @@ CDAC_GLOBAL(DispatchThisPtrMask, T_NUINT, InteropLib::ABI::DispatchThisPtrMask) CDAC_GLOBAL_POINTER(ComWrappersVtablePtrs, InteropLib::ABI::g_knownQueryInterfaceImplementations) #endif // FEATURE_COMWRAPPERS CDAC_GLOBAL_POINTER(GcNotificationFlags, &::g_gcNotificationFlags) +CDAC_GLOBAL_POINTER(JITNotificationTable, &::g_pNotificationTable) +CDAC_GLOBAL(JITNotificationTableSize, T_UINT32, JIT_NOTIFICATION_TABLE_SIZE) CDAC_GLOBAL_POINTER(GlobalAllocContext, &::g_global_alloc_context) CDAC_GLOBAL_POINTER(CoreLib, &::g_CoreLib) #ifdef TARGET_WINDOWS @@ -1505,6 +1514,7 @@ CDAC_GLOBAL_CONTRACT(AuxiliarySymbols, c1) CDAC_GLOBAL_CONTRACT(BuiltInCOM, c1) #endif // FEATURE_COMINTEROP CDAC_GLOBAL_CONTRACT(CodeVersions, c1) +CDAC_GLOBAL_CONTRACT(CodeNotifications, c1) #ifdef FEATURE_COMWRAPPERS CDAC_GLOBAL_CONTRACT(ComWrappers, c1) #endif // FEATURE_COMWRAPPERS diff --git a/src/coreclr/vm/util.hpp b/src/coreclr/vm/util.hpp index b64a0ce9753c3a..1f09735dfff4fb 100644 --- a/src/coreclr/vm/util.hpp +++ b/src/coreclr/vm/util.hpp @@ -566,6 +566,11 @@ inline bool IsInCantStopRegion() BOOL IsValidMethodCodeNotification(ULONG32 Notification); +// Number of usable JIT notification entries. The allocated table has +// JIT_NOTIFICATION_TABLE_SIZE + 1 slots; slot 0 stores bookkeeping (length). +// Referenced by the cDAC via CDAC_GLOBAL(JITNotificationTableSize, ...). +constexpr UINT JIT_NOTIFICATION_TABLE_SIZE = 1000; + typedef DPTR(struct JITNotification) PTR_JITNotification; struct JITNotification { @@ -598,7 +603,7 @@ GVAL_DECL(ULONG32, g_dacNotificationFlags); inline void InitializeJITNotificationTable() { - g_pNotificationTable = new (nothrow) JITNotification[1001]; + g_pNotificationTable = new (nothrow) JITNotification[JIT_NOTIFICATION_TABLE_SIZE + 1]; } #endif // TARGET_UNIX && !DACCESS_COMPILE diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs index 28753618fa84d9..9d4088ee27b15c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -93,6 +93,10 @@ public abstract class ContractRegistry /// public virtual INotifications Notifications => GetContract(); /// + /// Gets an instance of the CodeNotifications contract for the target. + /// + public virtual ICodeNotifications CodeNotifications => GetContract(); + /// /// Gets an instance of the SignatureDecoder contract for the target. /// public virtual ISignatureDecoder SignatureDecoder => GetContract(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs new file mode 100644 index 00000000000000..90d061ed5017e7 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +/// +/// Kinds of JIT code notifications that can be requested for a given method. +/// The contract layer only exchanges this typed enum — COM wrappers translate +/// to/from the raw uint at the boundary. +/// +[Flags] +public enum CodeNotificationKind : uint +{ + None = 0, + Generated = 1, + Discarded = 2, +} + +/// +/// Contract for reading and writing the JIT code notification table in the target process. +/// The table is an allowlist of (module, method token) pairs that causes the runtime to +/// raise DEBUG_CODE_NOTIFICATION events when the specified methods are JIT-compiled +/// or discarded. +/// +/// This contract doesn't currently offer a capacity check, so consumers won't be able to +/// confirm in advance whether a batch of notification updates will all succeed. If the +/// in-target table fills up, throws a +/// with HResult == E_FAIL. +/// +public interface ICodeNotifications : IContract +{ + static string IContract.Name { get; } = nameof(CodeNotifications); + + /// + /// Set the notification flags for a single (module, methodToken) pair. + /// If the in-target table has not been allocated yet, lazily allocates it when + /// is non-zero. + /// + void SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags) => throw new NotImplementedException(); + + /// + /// Get the notification flags for a single (module, methodToken) pair. + /// + CodeNotificationKind GetCodeNotification(TargetPointer module, uint methodToken) => throw new NotImplementedException(); + + /// + /// Set notification flags for all methods in a module, or all methods if module is null. + /// + void SetAllCodeNotifications(TargetPointer module, CodeNotificationKind flags) => throw new NotImplementedException(); +} + +public readonly struct CodeNotifications : ICodeNotifications +{ + // Everything throws NotImplementedException +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 989db1625b33f9..f6fe1c47cc1aad 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -22,6 +22,7 @@ public enum DataType /* VM Data Types */ ObjectHandle, + JITNotification, CodePointer, Thread, ThreadStore, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index 39bdaabcb53be9..fc994d02521f0e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -98,6 +98,20 @@ public abstract class Target /// Source of the bytes to write, the number of bytes to write is the span length public abstract void WriteBuffer(ulong address, Span buffer); + /// + /// Allocate memory in the target process + /// + /// The number of bytes to allocate + /// The address of the allocated memory in the target process + /// Thrown when the target does not support memory allocation + /// + /// This is used for lazy allocation patterns where the debugger needs to allocate memory + /// in the target process (e.g., JIT notification tables on Windows). + /// The default implementation throws . + /// + public virtual TargetPointer AllocateMemory(uint size) + => throw new NotImplementedException("Target does not support memory allocation"); + /// /// Read a null-terminated UTF-8 string from the target /// @@ -188,6 +202,20 @@ public abstract class Target /// Value to write public abstract void Write(ulong address, T value) where T : unmanaged, IBinaryInteger, IMinMaxValue; + /// + /// Write a target pointer to the target in target endianness, using the target's pointer size. + /// + /// Address to write to + /// Pointer value to write + public abstract void WritePointer(ulong address, TargetPointer value); + + /// + /// Write a target NUInt to the target in target endianness, using the target's pointer size. + /// + /// Address to write to + /// Pointer value to write + public abstract void WriteNUInt(ulong address, TargetNUInt value); + /// /// Read a target pointer from a span of bytes /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs index 007af1845cb798..39de962c4bb322 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetFieldExtensions.cs @@ -144,6 +144,49 @@ public static T ReadDataField(this Target target, ulong address, Target.TypeI return target.ProcessedData.GetOrAdd(pointer); } + /// + /// Write a primitive integer field to the target with type validation. + /// + public static T WriteField(this Target target, ulong address, Target.TypeInfo typeInfo, string fieldName, T value) + where T : unmanaged, IBinaryInteger, IMinMaxValue + { + Target.FieldInfo field = typeInfo.Fields[fieldName]; + AssertPrimitiveType(field, fieldName); + + target.Write(address + (ulong)field.Offset, value); + return value; + } + + /// + /// Write a native unsigned integer field to the target with type validation. + /// Returns the value written for convenient single-line backing-field updates. + /// + public static TargetNUInt WriteNUIntField(this Target target, ulong address, Target.TypeInfo typeInfo, string fieldName, TargetNUInt value) + { + Target.FieldInfo field = typeInfo.Fields[fieldName]; + Debug.Assert( + field.TypeName is null or "" or "nuint", + $"Type mismatch writing field '{fieldName}': declared as '{field.TypeName}', expected nuint"); + + ulong addr = address + (ulong)field.Offset; + target.WriteNUInt(addr, value); + return value; + } + + /// + /// Write a pointer field to the target with type validation. + /// Returns the value written for convenient single-line backing-field updates. + /// + public static TargetPointer WritePointerField(this Target target, ulong address, Target.TypeInfo typeInfo, string fieldName, TargetPointer value) + { + Target.FieldInfo field = typeInfo.Fields[fieldName]; + AssertPointerType(field, fieldName); + + ulong addr = address + (ulong)field.Offset; + target.WritePointer(addr, value); + return value; + } + [Conditional("DEBUG")] private static void AssertPrimitiveType(Target.FieldInfo field, string fieldName) where T : unmanaged, IBinaryInteger, IMinMaxValue diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 41eccea4c65d96..d550c20596526a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -46,6 +46,8 @@ public static class Globals public const string DispatchThisPtrMask = nameof(DispatchThisPtrMask); public const string ComWrappersVtablePtrs = nameof(ComWrappersVtablePtrs); public const string GcNotificationFlags = nameof(GcNotificationFlags); + public const string JITNotificationTable = nameof(JITNotificationTable); + public const string JITNotificationTableSize = nameof(JITNotificationTableSize); public const string GlobalAllocContext = nameof(GlobalAllocContext); public const string StressLogEnabled = nameof(StressLogEnabled); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs new file mode 100644 index 00000000000000..3f20fa3d6f8de6 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal readonly struct CodeNotifications_1 : ICodeNotifications +{ + private readonly Target _target; + + internal CodeNotifications_1(Target target) + { + _target = target; + } + + void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags) + { + TableView? view = PrepareTable(allocateIfMissing: flags != CodeNotificationKind.None); + if (view is null) + return; + + TableView v = view.Value; + + if (flags == CodeNotificationKind.None) + { + if (v.TryFindEntry(module, methodToken, out uint foundIndex)) + { + Data.JITNotification entry = v.GetEntry(foundIndex); + entry.Clear(); + if (foundIndex == v.Length - 1) + { + v.Length--; + } + } + + return; + } + + if (v.TryFindEntry(module, methodToken, out uint existingIndex)) + { + v.GetEntry(existingIndex).State = (ushort)flags; + + return; + } + + uint firstFree = v.Length; + for (uint i = 0; i < v.Length; i++) + { + if (v.GetEntry(i).IsFree) + { + firstFree = i; + break; + } + } + + if (firstFree >= v.Capacity) + { + // Match legacy DAC (ClrDataMethodDefinition::SetCodeNotification path): when the + // notification table is full, SetNotification returns FALSE which bubbles up as E_FAIL. + const int E_FAIL = unchecked((int)0x80004005); + throw new COMException("JIT notification table is full", E_FAIL); + } + + v.GetEntry(firstFree).WriteEntry(module, methodToken, (ushort)flags); + + if (firstFree >= v.Length) + { + v.Length++; + } + } + + CodeNotificationKind ICodeNotifications.GetCodeNotification(TargetPointer module, uint methodToken) + { + TableView? view = PrepareTable(allocateIfMissing: false); + if (view is null) + return CodeNotificationKind.None; + + TableView v = view.Value; + if (v.TryFindEntry(module, methodToken, out uint foundIndex)) + { + return (CodeNotificationKind)v.GetEntry(foundIndex).State; + } + + return CodeNotificationKind.None; + } + + void ICodeNotifications.SetAllCodeNotifications(TargetPointer module, CodeNotificationKind flags) + { + // When the table has not been allocated there are no entries to update, so this is a + // no-op. Matches native JITNotifications::SetAllNotifications (util.cpp:1112). + TableView? maybeView = PrepareTable(allocateIfMissing: false); + if (maybeView is null) + return; + + TableView v = maybeView.Value; + bool changed = false; + for (uint i = 0; i < v.Length; i++) + { + Data.JITNotification entry = v.GetEntry(i); + if (entry.IsFree) + continue; + + if (module != TargetPointer.Null && entry.ClrModule.Value != module.Value) + continue; + + if (flags == CodeNotificationKind.None) + { + entry.Clear(); + } + else + { + entry.State = (ushort)flags; + } + + changed = true; + } + + if (changed && flags == CodeNotificationKind.None) + { + // Trim only trailing free entries. This deliberately diverges from native + // JITNotifications::SetAllNotifications (src/coreclr/vm/util.cpp:1140-1149), which + // decrements the stored length for every free slot in [0, Length), including holes. + // That algorithm can trim Length below the index of still-active entries belonging + // to other modules (e.g., when SetAllCodeNotifications filters by module), orphaning + // those entries. Trimming only trailing free slots preserves the invariant + // "Length > index of every active entry" which the lookup/iteration code relies on. + uint newLength = v.Length; + while (newLength > 0 && v.GetEntry(newLength - 1).IsFree) + { + newLength--; + } + + v.Length = newLength; + } + } + + /// + /// A live handle to the JIT notification table in the target process. + /// reads and writes through the sentinel slot (index 0) via the + /// IData; comes from the + /// JITNotificationTableSize global. Per-entry access is via + /// and . + /// + private readonly struct TableView + { + private readonly Target _target; + private readonly Data.JITNotification _sentinel; + public readonly ulong EntriesBase; + public readonly uint EntrySize; + + public TableView(Target target, TargetPointer basePointer, uint entrySize) + { + _target = target; + _sentinel = new Data.JITNotification(target, basePointer); + EntrySize = entrySize; + EntriesBase = basePointer + entrySize; + } + + public uint Length + { + get => _sentinel.MethodToken; + set => _sentinel.MethodToken = value; + } + + public uint Capacity => _target.ReadGlobal(Constants.Globals.JITNotificationTableSize); + + public Data.JITNotification GetEntry(uint index) + => new(_target, new TargetPointer(EntriesBase + (ulong)(index * EntrySize))); + + public bool TryFindEntry(TargetPointer module, uint methodToken, out uint index) + { + uint length = Length; + for (uint i = 0; i < length; i++) + { + Data.JITNotification entry = GetEntry(i); + if (entry.IsFree) + continue; + if (entry.ClrModule.Value != module.Value) + continue; + if (entry.MethodToken != methodToken) + continue; + + index = i; + + return true; + } + + index = 0; + + return false; + } + } + + /// + /// Read (and optionally lazily allocate) the JIT notification table. Returns null if + /// the table is not allocated and is false. + /// + private TableView? PrepareTable(bool allocateIfMissing) + { + Target.TypeInfo jitNotifType = _target.GetTypeInfo(DataType.JITNotification); + uint entrySize = (uint)(jitNotifType.Size + ?? throw new InvalidOperationException("JITNotification has no declared size")); + + TargetPointer globalAddr = _target.ReadGlobalPointer(Constants.Globals.JITNotificationTable); + TargetPointer tablePointer = _target.ReadPointer(globalAddr); + + if (tablePointer == TargetPointer.Null) + { + if (!allocateIfMissing) + return null; + tablePointer = AllocateTable(entrySize, globalAddr); + } + + return new TableView(_target, tablePointer, entrySize); + } + + /// + /// Lazily allocate a JIT notification table in the target process using AllocateMemory, + /// zero-fill it (slot 0's methodToken is the length, which starts at 0), and write the + /// pointer back to g_pNotificationTable. + /// + private TargetPointer AllocateTable(uint entrySize, TargetPointer globalAddr) + { + uint capacity = _target.ReadGlobal(Constants.Globals.JITNotificationTableSize); + // Table has capacity+1 entries: index 0 is bookkeeping + uint tableByteSize = entrySize * (capacity + 1); + TargetPointer tablePointer = _target.AllocateMemory(tableByteSize); + + byte[] zeros = new byte[checked((int)tableByteSize)]; + _target.WriteBuffer(tablePointer.Value, zeros); + + _target.WritePointer(globalAddr.Value, tablePointer); + + return tablePointer; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index 3086ad9a0f384d..f84ae929637aaa 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -24,6 +24,7 @@ public static void Register(ContractRegistry registry) registry.Register("c1", static t => new ComWrappers_1(t)); registry.Register("c1", static t => new SHash_1(t)); registry.Register("c1", static t => new Notifications_1(t)); + registry.Register("c1", static t => new CodeNotifications_1(t)); registry.Register("c1", static t => new SignatureDecoder_1(t)); registry.Register("c1", static t => new BuiltInCOM_1(t)); registry.Register("c1", static t => new ConditionalWeakTable_1(t)); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/JITNotification.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/JITNotification.cs new file mode 100644 index 00000000000000..61980d489b42f4 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/JITNotification.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class JITNotification : IData +{ + static JITNotification IData.Create(Target target, TargetPointer address) + => new JITNotification(target, address); + + private readonly Target _target; + private readonly Target.TypeInfo _type; + private readonly TargetPointer _address; + + private ushort _state; + private TargetNUInt _clrModule; + private uint _methodToken; + + public JITNotification(Target target, TargetPointer address) + { + _target = target; + _type = target.GetTypeInfo(DataType.JITNotification); + _address = address; + + _state = target.ReadField(address, _type, nameof(State)); + _clrModule = target.ReadNUIntField(address, _type, nameof(ClrModule)); + _methodToken = target.ReadField(address, _type, nameof(MethodToken)); + } + + public ushort State + { + get => _state; + set => _state = _target.WriteField(_address, _type, nameof(State), value); + } + + public TargetNUInt ClrModule + { + get => _clrModule; + set => _clrModule = _target.WriteNUIntField(_address, _type, nameof(ClrModule), value); + } + + public uint MethodToken + { + get => _methodToken; + set => _methodToken = _target.WriteField(_address, _type, nameof(MethodToken), value); + } + + public bool IsFree => _state == 0; + + public void Clear() + { + State = 0; + ClrModule = new TargetNUInt(0); + MethodToken = 0; + } + + public void WriteEntry(TargetPointer module, uint methodToken, ushort state) + { + ClrModule = new TargetNUInt(module.Value); + MethodToken = methodToken; + State = state; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodDefinition.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodDefinition.cs index b57007986f698e..4482925441ddae 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodDefinition.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodDefinition.cs @@ -393,10 +393,52 @@ int IXCLRDataMethodDefinition.EndEnumExtents(ulong handle) => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.EndEnumExtents(handle) : HResults.E_NOTIMPL; int IXCLRDataMethodDefinition.GetCodeNotification(uint* flags) - => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.GetCodeNotification(flags) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + ICodeNotifications codeNotif = _target.Contracts.CodeNotifications; + + try + { + if (flags is null) + throw new ArgumentNullException(nameof(flags)); + + *flags = CodeNotificationFlagsConverter.ToCom(codeNotif.GetCodeNotification(_module, _token)); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // No #if DEBUG validation: GetCodeNotification is a read, but both cDAC and + // legacy DAC allocate the table on-demand when called, which would cause + // dual-allocation. Validation is safe at a higher layer when a dump is used. + + return hr; + } int IXCLRDataMethodDefinition.SetCodeNotification(uint flags) - => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.SetCodeNotification(flags) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + ICodeNotifications codeNotif = _target.Contracts.CodeNotifications; + + try + { + if (!CodeNotificationFlagsConverter.IsValid(flags)) + throw new ArgumentException("Invalid code notification flags", nameof(flags)); + + codeNotif.SetCodeNotification(_module, _token, CodeNotificationFlagsConverter.FromCom(flags)); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // No #if DEBUG validation: SetCodeNotification is a write operation. + // Both the cDAC and legacy DAC independently allocate and write to + // g_pNotificationTable via AllocVirtual, causing dual-write corruption. + + return hr; + } int IXCLRDataMethodDefinition.Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer) => LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.Request(reqCode, inBufferSize, inBuffer, outBufferSize, outBuffer) : HResults.E_NOTIMPL; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs index 5467735eaa0319..ad17d76f917c95 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs @@ -20,6 +20,8 @@ public sealed unsafe partial class ClrDataModule : ICustomQueryInterface, IXCLRD private readonly TargetPointer _address; private readonly Target _target; + internal TargetPointer Address => _address; + private bool _extentsSet; private CLRDataModuleExtent[] _extents = new CLRDataModuleExtent[2]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/CodeNotificationFlagsConverter.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/CodeNotificationFlagsConverter.cs new file mode 100644 index 00000000000000..38d06bdcb06174 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/CodeNotificationFlagsConverter.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +/// +/// Translates between the COM-side bit field +/// (a raw uint exposed through xclrdata.idl) and the contract-side +/// enum. The two enums happen to share the same bit +/// values today, but each side is free to evolve independently — translation must be +/// explicit per bit, not a numeric cast. +/// +internal static class CodeNotificationFlagsConverter +{ + private const uint AllValidComFlags = + (uint)(CLRDataMethodCodeNotification.CLRDATA_METHNOTIFY_GENERATED + | CLRDataMethodCodeNotification.CLRDATA_METHNOTIFY_DISCARDED); + + /// + /// Returns true if every set bit in is a recognized + /// value. + /// + public static bool IsValid(uint flags) => (flags & ~AllValidComFlags) == 0; + + /// + /// Convert a raw COM bitmask to the + /// contract enum, mapping each defined bit explicitly. + /// + public static CodeNotificationKind FromCom(uint flags) + { + CodeNotificationKind result = CodeNotificationKind.None; + if ((flags & (uint)CLRDataMethodCodeNotification.CLRDATA_METHNOTIFY_GENERATED) != 0) + result |= CodeNotificationKind.Generated; + if ((flags & (uint)CLRDataMethodCodeNotification.CLRDATA_METHNOTIFY_DISCARDED) != 0) + result |= CodeNotificationKind.Discarded; + return result; + } + + /// + /// Convert a contract value back to the raw COM + /// bitmask, mapping each defined bit + /// explicitly. + /// + public static uint ToCom(CodeNotificationKind kind) + { + uint result = 0; + if ((kind & CodeNotificationKind.Generated) != 0) + result |= (uint)CLRDataMethodCodeNotification.CLRDATA_METHNOTIFY_GENERATED; + if ((kind & CodeNotificationKind.Discarded) != 0) + result |= (uint)CLRDataMethodCodeNotification.CLRDATA_METHNOTIFY_DISCARDED; + return result; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IXCLRData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IXCLRData.cs index d7fb9a1bdad086..b8065b21707cd1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IXCLRData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IXCLRData.cs @@ -40,6 +40,14 @@ public enum CLRDataByNameFlag : uint CLRDATA_BYNAME_CASE_INSENSITIVE = 1 } +[Flags] +public enum CLRDataMethodCodeNotification : uint +{ + CLRDATA_METHNOTIFY_NONE = 0x00000000, + CLRDATA_METHNOTIFY_GENERATED = 0x00000001, + CLRDATA_METHNOTIFY_DISCARDED = 0x00000002, +} + public unsafe struct EXCEPTION_RECORD64 { public const int ExceptionMaximumParameters = 15; @@ -273,30 +281,30 @@ int GetTypeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[] tokens, - [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags); + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[]? tokens, + [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags); [PreserveSig] int SetTypeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[] tokens, - [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags, + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[]? tokens, + [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags, uint singleFlags); [PreserveSig] int GetCodeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef*/ uint[] tokens, - [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags); + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef*/ uint[]? tokens, + [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags); [PreserveSig] int SetCodeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef */ uint[] tokens, - [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags, + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef */ uint[]? tokens, + [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags, uint singleFlags); [PreserveSig] int GetOtherNotificationFlags(uint* flags); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs index 7e7fbac013a022..2f4d5c07a4d03e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs @@ -34,9 +34,6 @@ internal static class LegacyFallbackHelper // Loader heap traversal — not yet implemented in the cDAC (PR #125129). nameof(ISOSDacInterface.TraverseLoaderHeap), - - // IXCLRDataMethodDefinition — SetCodeNotification not yet implemented (needs INotifications contract). - nameof(IXCLRDataMethodDefinition.SetCodeNotification), }; // Files whose methods are all allowed to fall back. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs index 81a6bca6539398..83636b1cb209fb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs @@ -682,22 +682,48 @@ int IXCLRDataProcess.SetAllTypeNotifications(IXCLRDataModule? mod, uint flags) => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.SetAllTypeNotifications(mod, flags) : HResults.E_NOTIMPL; int IXCLRDataProcess.SetAllCodeNotifications(IXCLRDataModule? mod, uint flags) - => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.SetAllCodeNotifications(mod, flags) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (!CodeNotificationFlagsConverter.IsValid(flags)) + throw new ArgumentException("Invalid code notification flags"); + + TargetPointer moduleAddr = TargetPointer.Null; + if (mod is not null) + { + if (mod is not ClrDataModule cdm) + throw new ArgumentException(); + moduleAddr = cdm.Address; + } + + _target.Contracts.CodeNotifications.SetAllCodeNotifications(moduleAddr, CodeNotificationFlagsConverter.FromCom(flags)); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // No #if DEBUG validation: SetAllCodeNotifications is a write operation. + // Both the cDAC and legacy DAC independently write to g_pNotificationTable. + + return hr; + } int IXCLRDataProcess.GetTypeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[] tokens, - [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags) + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[]? tokens, + [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags) => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetTypeNotifications(numTokens, mods, singleMod, tokens, flags) : HResults.E_NOTIMPL; int IXCLRDataProcess.SetTypeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[] tokens, - [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags, + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[]? tokens, + [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags, uint singleFlags) => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.SetTypeNotifications(numTokens, mods, singleMod, tokens, flags, singleFlags) : HResults.E_NOTIMPL; @@ -705,18 +731,107 @@ int IXCLRDataProcess.GetCodeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef*/ uint[] tokens, - [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags) - => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetCodeNotifications(numTokens, mods, singleMod, tokens, flags) : HResults.E_NOTIMPL; + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef*/ uint[]? tokens, + [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags) + { + int hr = HResults.S_OK; + ICodeNotifications codeNotif = _target.Contracts.CodeNotifications; + + try + { + // Match legacy DAC (daccess.cpp ClrDataAccess::GetCodeNotifications): + // tokens and flags are both required; exactly one of mods/singleMod must be non-null. + if (tokens is null || flags is null || + (mods is null && singleMod is null) || + (mods is not null && singleMod is not null)) + throw new ArgumentException(); + + TargetPointer moduleAddr = TargetPointer.Null; + if (singleMod is not null) + { + if (singleMod is not ClrDataModule singleCdm) + throw new ArgumentException(); + moduleAddr = singleCdm.Address; + } + + for (uint i = 0; i < numTokens; i++) + { + if (singleMod is null) + moduleAddr = GetModuleAddress(mods[i]); + + flags[i] = CodeNotificationFlagsConverter.ToCom(codeNotif.GetCodeNotification(moduleAddr, tokens[i])); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // No #if DEBUG validation: GetCodeNotifications is a read, but both cDAC and + // legacy DAC allocate the table on-demand when called, which would cause + // dual-allocation. Validation is safe at a higher layer when a dump is used. + + return hr; + } int IXCLRDataProcess.SetCodeNotifications( uint numTokens, /*IXCLRDataModule*/ void** mods, IXCLRDataModule? singleMod, - [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef */ uint[] tokens, - [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[] flags, + [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef */ uint[]? tokens, + [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags, uint singleFlags) - => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.SetCodeNotifications(numTokens, mods, singleMod, tokens, flags, singleFlags) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + + try + { + if (tokens is null || + (mods is null && singleMod is null) || + (mods is not null && singleMod is not null)) + throw new ArgumentException(); + + // Validate flags. + if (flags is not null) + { + for (uint check = 0; check < numTokens; check++) + { + if (!CodeNotificationFlagsConverter.IsValid(flags[check])) + throw new ArgumentException("Invalid code notification flags"); + } + } + else if (!CodeNotificationFlagsConverter.IsValid(singleFlags)) + { + throw new ArgumentException("Invalid code notification flags"); + } + + TargetPointer moduleAddr = TargetPointer.Null; + if (singleMod is not null) + { + if (singleMod is not ClrDataModule singleCdm) + throw new ArgumentException(); + moduleAddr = singleCdm.Address; + } + + for (uint i = 0; i < numTokens; i++) + { + if (singleMod is null) + moduleAddr = GetModuleAddress(mods[i]); + + uint f = flags is not null ? flags[i] : singleFlags; + _target.Contracts.CodeNotifications.SetCodeNotification(moduleAddr, tokens[i], CodeNotificationFlagsConverter.FromCom(f)); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // No #if DEBUG validation: SetCodeNotifications is a write operation. + // Both the cDAC and legacy DAC independently write to g_pNotificationTable. + + return hr; + } int IXCLRDataProcess.GetOtherNotificationFlags(uint* flags) { @@ -863,4 +978,14 @@ int IXCLRDataProcess2.SetGcNotification(GcEvtArgs gcEvtArgs) #endif return hr; } + + private static TargetPointer GetModuleAddress(void* comModulePtr) + { + if (System.Runtime.InteropServices.ComWrappers.TryGetObject((nint)comModulePtr, out object? obj)) + { + if (obj is ClrDataModule cdm) + return cdm.Address; + } + throw new ArgumentException("Could not resolve module address from COM pointer"); + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 39ac4310e3283a..47eb8bff46456c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using Microsoft.Diagnostics.DataContractReader.Data; using Microsoft.Diagnostics.DataContractReader.Contracts; @@ -46,6 +47,8 @@ private readonly struct Configuration public delegate int ReadFromTargetDelegate(ulong address, Span bufferToFill); public delegate int WriteToTargetDelegate(ulong address, Span bufferToWrite); public delegate int GetTargetThreadContextDelegate(uint threadId, uint contextFlags, Span bufferToFill); + public delegate int AllocVirtualDelegate(ulong size, out ulong allocatedAddress); + private static readonly UTF8Encoding strictUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private static readonly UTF8Encoding looseUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); @@ -56,6 +59,7 @@ private readonly struct Configuration /// A callback to read memory blocks at a given address from the target /// A callback to write memory blocks at a given address to the target /// A callback to fetch a thread's context + /// A callback to allocate virtual memory in the target /// Registration actions that populate the contract registry (e.g., ) /// The target object. /// If a target instance could be created, true; otherwise, false. @@ -64,10 +68,11 @@ public static bool TryCreate( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, GetTargetThreadContextDelegate getThreadContext, + AllocVirtualDelegate allocVirtual, Action[] contractRegistrations, [NotNullWhen(true)] out ContractDescriptorTarget? target) { - DataTargetDelegates dataTargetDelegates = new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext); + DataTargetDelegates dataTargetDelegates = new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, allocVirtual); if (TryReadAllContractDescriptors( contractDescriptor, dataTargetDelegates, @@ -89,6 +94,7 @@ public static bool TryCreate( /// A callback to read memory blocks at a given address from the target /// A callback to write memory blocks at a given address to the target /// A callback to fetch a thread's context + /// A callback to allocate virtual memory in the target /// Whether the target is little-endian /// The size of a pointer in bytes in the target process. /// Registration actions that populate the contract registry (e.g., ) @@ -99,6 +105,7 @@ public static ContractDescriptorTarget Create( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, GetTargetThreadContextDelegate getThreadContext, + AllocVirtualDelegate allocVirtual, bool isLittleEndian, int pointerSize, Action[]? contractRegistrations = null) @@ -112,7 +119,7 @@ public static ContractDescriptorTarget Create( PointerData = globalPointerValues } ], - new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext), + new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, allocVirtual), contractRegistrations ?? []); } @@ -461,6 +468,22 @@ public override void Write(ulong address, T value) throw new InvalidOperationException($"Failed to write {typeof(T)} at 0x{address:x8}."); } + public override void WritePointer(ulong address, TargetPointer value) + { + if (_config.PointerSize == 8) + Write(address, value.Value); + else + Write(address, checked((uint)value.Value)); + } + + public override void WriteNUInt(ulong address, TargetNUInt value) + { + if (_config.PointerSize == 8) + Write(address, value.Value); + else + Write(address, checked((uint)value.Value)); + } + private static bool TryWrite(ulong address, bool isLittleEndian, DataTargetDelegates dataTargetDelegates, T value) where T : unmanaged, IBinaryInteger, IMinMaxValue { Span buffer = stackalloc byte[sizeof(T)]; @@ -508,6 +531,17 @@ public override void WriteBuffer(ulong address, Span buffer) throw new InvalidOperationException($"Failed to write {buffer.Length} bytes at 0x{address:x8}."); } + public override TargetPointer AllocateMemory(uint size) + { + int hr = _dataTargetDelegates.AllocVirtual(size, out ulong allocatedAddress); + if (hr < 0) + throw Marshal.GetExceptionForHR(hr) ?? new InvalidOperationException($"Failed to allocate {size} bytes in the target process (HRESULT: 0x{hr:x8})."); + if (allocatedAddress == 0) + throw new OutOfMemoryException($"Failed to allocate {size} bytes in the target process (AllocVirtual returned S_OK but no address)."); + + return new TargetPointer(allocatedAddress); + } + private bool TryWriteBuffer(ulong address, Span buffer) { return _dataTargetDelegates.WriteToTarget(address, buffer) >= 0; @@ -864,7 +898,8 @@ public void Clear() private readonly struct DataTargetDelegates( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, - GetTargetThreadContextDelegate getThreadContext) + GetTargetThreadContextDelegate getThreadContext, + AllocVirtualDelegate allocVirtual) { public int ReadFromTarget(ulong address, Span buffer) { @@ -882,5 +917,9 @@ public int WriteToTarget(ulong address, Span buffer) { return writeToTarget(address, buffer); } + public int AllocVirtual(ulong size, out ulong allocatedAddress) + { + return allocVirtual(size, out allocatedAddress); + } } } diff --git a/src/native/managed/cdac/inc/cdac_reader.h b/src/native/managed/cdac/inc/cdac_reader.h index c13ac0e44fcb74..fab54ab2c28aae 100644 --- a/src/native/managed/cdac/inc/cdac_reader.h +++ b/src/native/managed/cdac/inc/cdac_reader.h @@ -12,7 +12,9 @@ extern "C" // Initialize the cDAC reader // descriptor: the address of the descriptor in the target process // read_from_target: a callback that reads memory from the target process +// write_to_target: a callback that writes memory to the target process // read_thread_context: a callback that reads the context of a thread in the target process +// alloc_virtual: optional callback that allocates memory in the target process (may be NULL) // read_context: a context pointer that will be passed to callbacks // handle: returned opaque the handle to the reader. This should be passed to other functions in this API. int cdac_reader_init( @@ -20,6 +22,7 @@ int cdac_reader_init( int(*read_from_target)(uint64_t, uint8_t*, uint32_t, void*), int(*write_to_target)(uint64_t, const uint8_t*, uint32_t, void*), int(*read_thread_context)(uint32_t, uint32_t, uint32_t, uint8_t*, void*), + int(*alloc_virtual)(uint32_t, uint64_t*, void*), void* read_context, /*out*/ intptr_t* handle); diff --git a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs index a8312a3678c6b7..c46f944f5ca81d 100644 --- a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs +++ b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs @@ -18,9 +18,34 @@ private static unsafe int Init( delegate* unmanaged readFromTarget, delegate* unmanaged writeToTarget, delegate* unmanaged readThreadContext, + delegate* unmanaged allocVirtual, void* delegateContext, IntPtr* handle) { + // Build the allocVirtual delegate if the caller provided a callback + ContractDescriptorTarget.AllocVirtualDelegate allocDelegate = (ulong size, out ulong allocatedAddress) => + { + allocatedAddress = 0; + return HResults.E_NOTIMPL; + }; + + if (allocVirtual != null) + { + allocDelegate = (ulong size, out ulong allocatedAddress) => + { + if (size > uint.MaxValue) + { + allocatedAddress = 0; + return HResults.E_INVALIDARG; + } + + fixed (ulong* addrPtr = &allocatedAddress) + { + return allocVirtual((uint)size, addrPtr, delegateContext); + } + }; + } + // TODO: [cdac] Better error code/details if (!ContractDescriptorTarget.TryCreate( descriptor, @@ -45,6 +70,7 @@ private static unsafe int Init( return readThreadContext(threadId, contextFlags, (uint)buffer.Length, bufferPtr, delegateContext); } }, + allocDelegate, [Contracts.CoreCLRContracts.Register], out ContractDescriptorTarget? target)) return -1; @@ -153,6 +179,9 @@ private static unsafe int CLRDataCreateInstanceImpl(Guid* pIID, IntPtr /*ICLRDat ICLRContractLocator contractLocator = legacyTarget as ICLRContractLocator ?? throw new ArgumentException( $"{nameof(pLegacyTarget)} does not implement {nameof(ICLRContractLocator)}", nameof(pLegacyTarget)); + // Try to get ICLRDataTarget2 for memory allocation support (optional) + ICLRDataTarget2? dataTarget2 = legacyTarget as ICLRDataTarget2; + ulong contractAddress; int hr = contractLocator.GetContractDescriptor(&contractAddress); if (hr != 0) @@ -161,6 +190,28 @@ private static unsafe int CLRDataCreateInstanceImpl(Guid* pIID, IntPtr /*ICLRDat $"{nameof(ICLRContractLocator)} failed to fetch the contract descriptor with HRESULT: 0x{hr:x}."); } + // Build the allocVirtual delegate if the target supports ICLRDataTarget2 + ContractDescriptorTarget.AllocVirtualDelegate allocVirtual = (ulong size, out ulong allocatedAddress) => + { + allocatedAddress = 0; + return HResults.E_NOTIMPL; + }; + + if (dataTarget2 is not null) + { + // Windows virtual memory allocation flags used by ICLRDataTarget2::AllocVirtual. + const uint MEM_COMMIT = 0x1000; + const uint PAGE_READWRITE = 0x04; + + allocVirtual = (ulong size, out ulong allocatedAddress) => + { + ClrDataAddress addr; + int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr); + allocatedAddress = (ulong)addr; + return result; + }; + } + if (!ContractDescriptorTarget.TryCreate( contractAddress, (address, buffer) => @@ -186,6 +237,7 @@ private static unsafe int CLRDataCreateInstanceImpl(Guid* pIID, IntPtr /*ICLRDat return dataTarget.GetThreadContext(threadId, contextFlags, (uint)bufferToFill.Length, bufferPtr); } }, + allocVirtual, [Contracts.CoreCLRContracts.Register], out ContractDescriptorTarget? target)) { diff --git a/src/native/managed/cdac/tests/CodeNotificationsTests.cs b/src/native/managed/cdac/tests/CodeNotificationsTests.cs new file mode 100644 index 00000000000000..8319fb94cf11c8 --- /dev/null +++ b/src/native/managed/cdac/tests/CodeNotificationsTests.cs @@ -0,0 +1,323 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class CodeNotificationsTests +{ + // JITNotification struct layout for 64-bit LE: + // offset 0: state (ushort, 2 bytes) + // offset 2: 6 bytes padding + // offset 8: clrModule (ulong, 8 bytes) + // offset 16: methodToken (uint, 4 bytes) + // offset 20: 4 bytes padding + // total size: 24 bytes + private const int StateOffset = 0; + private const int ClrModuleOffset = 8; + private const int MethodTokenOffset = 16; + private const int EntrySize = 24; + private const uint TableCapacity = 10; + private const ulong TableAddress = 0x1_0000; + private const ulong TablePointerAddress = 0x2_0000; + + private static ICodeNotifications CreateContractWithJITTable() + { + var arch = new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = true }; + var helpers = new TargetTestHelpers(arch); + + var typeFields = new Dictionary + { + [nameof(Data.JITNotification.State)] = new Target.FieldInfo { Offset = StateOffset }, + [nameof(Data.JITNotification.ClrModule)] = new Target.FieldInfo { Offset = ClrModuleOffset }, + [nameof(Data.JITNotification.MethodToken)] = new Target.FieldInfo { Offset = MethodTokenOffset }, + }; + var types = new Dictionary + { + [DataType.JITNotification] = new Target.TypeInfo { Fields = typeFields, Size = EntrySize }, + }; + + // Allocate table memory: (TableCapacity + 1) entries for bookkeeping + actual entries + int totalTableSize = EntrySize * ((int)TableCapacity + 1); + byte[] tableData = new byte[totalTableSize]; + + // Initialize bookkeeping at index 0: length=0 (capacity now comes from the global). + helpers.Write(tableData.AsSpan(MethodTokenOffset), (uint)0); + + byte[] tablePointerData = new byte[8]; + helpers.WritePointer(tablePointerData.AsSpan(), TableAddress); + + var builder = new TestPlaceholderTarget.Builder(arch); + builder.MemoryBuilder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = TableAddress, + Data = tableData, + Name = "JITNotificationTable" + }); + builder.MemoryBuilder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = TablePointerAddress, + Data = tablePointerData, + Name = "JITNotificationTablePointer" + }); + + builder.AddTypes(types); + builder.AddGlobals( + (Constants.Globals.JITNotificationTable, TablePointerAddress), + (Constants.Globals.JITNotificationTableSize, TableCapacity) + ); + builder.AddContract(version: "c1"); + + var target = builder.Build(); + return target.Contracts.CodeNotifications; + } + + [Fact] + public void SetCodeNotification_NewEntry_CanBeRead() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module = new(0xAABB_CCDD); + uint token = 0x0600_0001; + + contract.SetCodeNotification(module, token, CodeNotificationKind.Generated); + + CodeNotificationKind result = contract.GetCodeNotification(module, token); + Assert.Equal(CodeNotificationKind.Generated, result); + } + + [Fact] + public void GetCodeNotification_NotFound_ReturnsNone() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module = new(0xDEAD); + uint token = 0x0600_9999; + + CodeNotificationKind result = contract.GetCodeNotification(module, token); + Assert.Equal(CodeNotificationKind.None, result); + } + + [Fact] + public void SetCodeNotification_Update_ChangesFlags() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module = new(0x1234); + uint token = 0x0600_0001; + + contract.SetCodeNotification(module, token, CodeNotificationKind.Generated); + Assert.Equal(CodeNotificationKind.Generated, contract.GetCodeNotification(module, token)); + + contract.SetCodeNotification(module, token, CodeNotificationKind.Discarded); + Assert.Equal(CodeNotificationKind.Discarded, contract.GetCodeNotification(module, token)); + } + + [Fact] + public void SetCodeNotification_ClearEntry_ReturnsNone() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module = new(0x1234); + uint token = 0x0600_0001; + + contract.SetCodeNotification(module, token, CodeNotificationKind.Generated); + Assert.Equal(CodeNotificationKind.Generated, contract.GetCodeNotification(module, token)); + + contract.SetCodeNotification(module, token, CodeNotificationKind.None); + Assert.Equal(CodeNotificationKind.None, contract.GetCodeNotification(module, token)); + } + + [Fact] + public void SetCodeNotification_MultipleEntries_IndependentlyReadable() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module1 = new(0x1000); + TargetPointer module2 = new(0x2000); + uint token1 = 0x0600_0001; + uint token2 = 0x0600_0002; + + contract.SetCodeNotification(module1, token1, CodeNotificationKind.Generated); + contract.SetCodeNotification(module2, token2, CodeNotificationKind.Discarded); + + Assert.Equal(CodeNotificationKind.Generated, contract.GetCodeNotification(module1, token1)); + Assert.Equal(CodeNotificationKind.Discarded, contract.GetCodeNotification(module2, token2)); + } + + [Fact] + public void SetAllCodeNotifications_ClearsAllEntries() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module = new(0x1000); + uint token1 = 0x0600_0001; + uint token2 = 0x0600_0002; + + contract.SetCodeNotification(module, token1, CodeNotificationKind.Generated); + contract.SetCodeNotification(module, token2, CodeNotificationKind.Generated); + + contract.SetAllCodeNotifications(TargetPointer.Null, CodeNotificationKind.None); + + Assert.Equal(CodeNotificationKind.None, contract.GetCodeNotification(module, token1)); + Assert.Equal(CodeNotificationKind.None, contract.GetCodeNotification(module, token2)); + } + + [Fact] + public void SetAllCodeNotifications_FilterByModule_ClearsOnlyMatchingEntries() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module1 = new(0x1000); + TargetPointer module2 = new(0x2000); + uint token = 0x0600_0001; + + contract.SetCodeNotification(module1, token, CodeNotificationKind.Generated); + contract.SetCodeNotification(module2, token, CodeNotificationKind.Generated); + + contract.SetAllCodeNotifications(module1, CodeNotificationKind.None); + + Assert.Equal(CodeNotificationKind.None, contract.GetCodeNotification(module1, token)); + Assert.Equal(CodeNotificationKind.Generated, contract.GetCodeNotification(module2, token)); + } + + [Fact] + public void SetAllCodeNotifications_UpdateFlags_ChangesAllMatching() + { + ICodeNotifications contract = CreateContractWithJITTable(); + TargetPointer module = new(0x1000); + uint token1 = 0x0600_0001; + uint token2 = 0x0600_0002; + + contract.SetCodeNotification(module, token1, CodeNotificationKind.Generated); + contract.SetCodeNotification(module, token2, CodeNotificationKind.Generated); + + contract.SetAllCodeNotifications(TargetPointer.Null, CodeNotificationKind.Discarded); + + Assert.Equal(CodeNotificationKind.Discarded, contract.GetCodeNotification(module, token1)); + Assert.Equal(CodeNotificationKind.Discarded, contract.GetCodeNotification(module, token2)); + } + + // --- Null Table / Lazy Allocation Tests --- + + private static ICodeNotifications CreateContractWithNullTable(TestPlaceholderTarget.AllocateMemoryDelegate? allocateMemory = null) + { + var arch = new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = true }; + + var typeFields = new Dictionary + { + [nameof(Data.JITNotification.State)] = new Target.FieldInfo { Offset = StateOffset }, + [nameof(Data.JITNotification.ClrModule)] = new Target.FieldInfo { Offset = ClrModuleOffset }, + [nameof(Data.JITNotification.MethodToken)] = new Target.FieldInfo { Offset = MethodTokenOffset }, + }; + var types = new Dictionary + { + [DataType.JITNotification] = new Target.TypeInfo { Fields = typeFields, Size = EntrySize }, + }; + + byte[] tablePointerData = new byte[8]; + + var builder = new TestPlaceholderTarget.Builder(arch); + builder.MemoryBuilder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = TablePointerAddress, + Data = tablePointerData, + Name = "JITNotificationTablePointer" + }); + + builder.AddTypes(types); + builder.AddGlobals( + (Constants.Globals.JITNotificationTable, TablePointerAddress), + (Constants.Globals.JITNotificationTableSize, TableCapacity) + ); + builder.AddContract(version: "c1"); + + if (allocateMemory is not null) + builder.UseAllocateMemory(allocateMemory); + + return builder.Build().Contracts.CodeNotifications; + } + + [Fact] + public void GetCodeNotification_NullTable_ReturnsNone() + { + ICodeNotifications contract = CreateContractWithNullTable(); + CodeNotificationKind result = contract.GetCodeNotification(new TargetPointer(0x1000), 0x0600_0001); + Assert.Equal(CodeNotificationKind.None, result); + } + + [Fact] + public void SetAllCodeNotifications_NullTable_NoOp() + { + ICodeNotifications contract = CreateContractWithNullTable(); + contract.SetAllCodeNotifications(TargetPointer.Null, CodeNotificationKind.None); + } + + [Fact] + public void SetCodeNotification_NullTable_ClearIsNoOp() + { + ICodeNotifications contract = CreateContractWithNullTable(); + contract.SetCodeNotification(new TargetPointer(0x1000), 0x0600_0001, CodeNotificationKind.None); + } + + [Fact] + public void SetCodeNotification_NullTable_NoAllocator_Throws() + { + ICodeNotifications contract = CreateContractWithNullTable(allocateMemory: null); + Assert.Throws(() => + contract.SetCodeNotification(new TargetPointer(0x1000), 0x0600_0001, CodeNotificationKind.Generated)); + } + + [Fact] + public void SetCodeNotification_NullTable_LazyAllocates_ThenWorks() + { + var arch = new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = true }; + + int totalTableSize = EntrySize * ((int)TableCapacity + 1); + byte[] allocatedTableData = new byte[totalTableSize]; + const ulong AllocatedTableAddress = 0x3_0000; + + var typeFields = new Dictionary + { + [nameof(Data.JITNotification.State)] = new Target.FieldInfo { Offset = StateOffset }, + [nameof(Data.JITNotification.ClrModule)] = new Target.FieldInfo { Offset = ClrModuleOffset }, + [nameof(Data.JITNotification.MethodToken)] = new Target.FieldInfo { Offset = MethodTokenOffset }, + }; + var types = new Dictionary + { + [DataType.JITNotification] = new Target.TypeInfo { Fields = typeFields, Size = EntrySize }, + }; + + byte[] tablePointerData = new byte[8]; + + var builder = new TestPlaceholderTarget.Builder(arch); + builder.MemoryBuilder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = TablePointerAddress, + Data = tablePointerData, + Name = "JITNotificationTablePointer" + }); + builder.MemoryBuilder.AddHeapFragment(new MockMemorySpace.HeapFragment + { + Address = AllocatedTableAddress, + Data = allocatedTableData, + Name = "AllocatedJITNotificationTable" + }); + + builder.AddTypes(types); + builder.AddGlobals( + (Constants.Globals.JITNotificationTable, TablePointerAddress), + (Constants.Globals.JITNotificationTableSize, TableCapacity) + ); + builder.AddContract(version: "c1"); + builder.UseAllocateMemory((size) => new TargetPointer(AllocatedTableAddress)); + + var target = builder.Build(); + ICodeNotifications contract = target.Contracts.CodeNotifications; + + TargetPointer module = new(0xAABB_CCDD); + uint token = 0x0600_0001; + + contract.SetCodeNotification(module, token, CodeNotificationKind.Generated); + + CodeNotificationKind result = contract.GetCodeNotification(module, token); + Assert.Equal(CodeNotificationKind.Generated, result); + } +} diff --git a/src/native/managed/cdac/tests/ContractDescriptor/ContractDescriptorBuilder.cs b/src/native/managed/cdac/tests/ContractDescriptor/ContractDescriptorBuilder.cs index ebd0ffb84fbbba..de5129cdabfdaa 100644 --- a/src/native/managed/cdac/tests/ContractDescriptor/ContractDescriptorBuilder.cs +++ b/src/native/managed/cdac/tests/ContractDescriptor/ContractDescriptorBuilder.cs @@ -205,6 +205,6 @@ public bool TryCreateTarget(DescriptorBuilder descriptor, [NotNullWhen(true)] ou _created = true; ulong contractDescriptorAddress = descriptor.CreateSubDescriptor(ContractDescriptorAddr, JsonDescriptorAddr, ContractPointerDataAddr); MockMemorySpace.MemoryContext memoryContext = GetMemoryContext(); - return ContractDescriptorTarget.TryCreate(contractDescriptorAddress, memoryContext.ReadFromTarget, memoryContext.WriteToTarget, (_, _, _) => throw new NotImplementedException("Tests do not provide GetTargetThreadContext"), [Contracts.CoreCLRContracts.Register], out target); + return ContractDescriptorTarget.TryCreate(contractDescriptorAddress, memoryContext.ReadFromTarget, memoryContext.WriteToTarget, (_, _, _) => throw new NotImplementedException("Tests do not provide GetTargetThreadContext"), (ulong _, out ulong _) => throw new NotImplementedException("Tests do not provide AllocVirtual"), [Contracts.CoreCLRContracts.Register], out target); } } diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs index fe1a05ab99bca5..0ff147922e18ff 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs @@ -113,6 +113,7 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, _host.ReadFromTarget, writeToTarget: static (_, _) => -1, _host.GetThreadContext, + allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), [Contracts.CoreCLRContracts.Register], out _target); diff --git a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs index 90272321d86f87..0dfa59aed222fd 100644 --- a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs @@ -25,13 +25,15 @@ internal class TestPlaceholderTarget : Target internal delegate int ReadFromTargetDelegate(ulong address, Span buffer); internal delegate int WriteToTargetDelegate(ulong address, Span buffer); + internal delegate TargetPointer AllocateMemoryDelegate(uint size); private readonly ReadFromTargetDelegate _dataReader; private readonly WriteToTargetDelegate? _dataWriter; + private readonly AllocateMemoryDelegate? _allocateMemory; private static readonly UTF8Encoding strictUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private static readonly UTF8Encoding looseUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); - public TestPlaceholderTarget(MockTarget.Architecture arch, ReadFromTargetDelegate reader, Dictionary types = null, (string Name, ulong Value)[] globals = null, (string Name, string Value)[] globalStrings = null, WriteToTargetDelegate? writer = null) + public TestPlaceholderTarget(MockTarget.Architecture arch, ReadFromTargetDelegate reader, Dictionary types = null, (string Name, ulong Value)[] globals = null, (string Name, string Value)[] globalStrings = null, WriteToTargetDelegate? writer = null, AllocateMemoryDelegate? allocateMemory = null) { IsLittleEndian = arch.IsLittleEndian; PointerSize = arch.Is64Bit ? 8 : 4; @@ -40,6 +42,7 @@ public TestPlaceholderTarget(MockTarget.Architecture arch, ReadFromTargetDelegat _typeInfoCache = types ?? []; _dataReader = reader; _dataWriter = writer; + _allocateMemory = allocateMemory; _globals = globals ?? []; _globalStrings = globalStrings ?? []; } @@ -80,6 +83,7 @@ internal class Builder private readonly List> _contractSetups = new(); private Action _registrations = CoreCLRContracts.Register; private ReadFromTargetDelegate? _readerOverride; + private AllocateMemoryDelegate? _allocateMemory; public Builder(MockTarget.Architecture arch) { @@ -114,6 +118,12 @@ public Builder UseReader(ReadFromTargetDelegate reader) return this; } + public Builder UseAllocateMemory(AllocateMemoryDelegate allocateMemory) + { + _allocateMemory = allocateMemory; + return this; + } + public Builder UseRegistrations(Action registrations) { _registrations = registrations; @@ -147,7 +157,8 @@ public TestPlaceholderTarget Build() _types, _globals.ToArray(), _globalStrings.ToArray(), - memoryContext.WriteToTarget); + memoryContext.WriteToTarget, + _allocateMemory); var registry = new TestContractRegistry(); registry.SetTarget(target); @@ -219,6 +230,14 @@ public override void WriteBuffer(ulong address, Span buffer) throw new InvalidOperationException($"Failed to write {buffer.Length} bytes at 0x{address:x8}."); } + public override TargetPointer AllocateMemory(uint size) + { + if (_allocateMemory is null) + return base.AllocateMemory(size); // throws NotImplementedException + + return _allocateMemory(size); + } + public override string ReadUtf8String(ulong address, bool strict = false) { // Read bytes until we find the null terminator @@ -350,6 +369,22 @@ public override void Write(ulong address, T value) WriteBuffer(address, buffer); } + public override void WritePointer(ulong address, TargetPointer value) + { + if (PointerSize == 8) + Write(address, value.Value); + else + Write(address, checked((uint)value.Value)); + } + + public override void WriteNUInt(ulong address, TargetNUInt value) + { + if (PointerSize == 8) + Write(address, value.Value); + else + Write(address, checked((uint)value.Value)); + } + #region subclass reader helpers /// diff --git a/src/tools/StressLogAnalyzer/src/Program.cs b/src/tools/StressLogAnalyzer/src/Program.cs index 4ff3fc162bdf09..b7195a218f4afb 100644 --- a/src/tools/StressLogAnalyzer/src/Program.cs +++ b/src/tools/StressLogAnalyzer/src/Program.cs @@ -494,6 +494,7 @@ ContractDescriptorTarget CreateTarget() => ContractDescriptorTarget.Create( (address, buffer) => ReadFromMemoryMappedLog(address, buffer, header), (address, buffer) => throw new NotImplementedException("StressLogAnalyzer does not provide WriteToTarget implementation"), (threadId, contextFlags, bufferToFill) => throw new NotImplementedException("StressLogAnalyzer does not provide GetTargetThreadContext implementation"), + (ulong size, out ulong allocatedAddress) => throw new NotImplementedException("StressLogAnalyzer does not provide AllocVirtual implementation"), true, nuint.Size, [CoreCLRContracts.Register]); From c9732618a40f64ba23bb8b568a18c4de8c85e1c8 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 27 Apr 2026 16:48:03 -0400 Subject: [PATCH 2/3] Document SetCodeNotifications divergence from legacy DAC Move the no-upfront-capacity-check note off the ICodeNotifications contract interface XML doc and onto the IXCLRDataProcess.SetCodeNotifications implementation, where the divergence actually lives: the legacy DAC checks numTokens against table size up front and returns E_OUTOFMEMORY atomically, while the cDAC writes entries one at a time and may leave partial writes before returning E_FAIL on overflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/ICodeNotifications.cs | 5 ----- .../SOSDacImpl.IXCLRDataProcess.cs | 8 ++++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs index 90d061ed5017e7..16ec62e2a8dfce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeNotifications.cs @@ -23,11 +23,6 @@ public enum CodeNotificationKind : uint /// The table is an allowlist of (module, method token) pairs that causes the runtime to /// raise DEBUG_CODE_NOTIFICATION events when the specified methods are JIT-compiled /// or discarded. -/// -/// This contract doesn't currently offer a capacity check, so consumers won't be able to -/// confirm in advance whether a batch of notification updates will all succeed. If the -/// in-target table fills up, throws a -/// with HResult == E_FAIL. /// public interface ICodeNotifications : IContract { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs index 83636b1cb209fb..f26c7bc72a0e62 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs @@ -782,6 +782,14 @@ int IXCLRDataProcess.SetCodeNotifications( [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags, uint singleFlags) { + // Behavior difference from the legacy DAC: the legacy DAC performs an upfront + // capacity check (numTokens > table size returns E_OUTOFMEMORY with no writes + // performed). The cDAC's CodeNotifications contract does not expose a capacity + // check, so this batch wrapper writes entries one at a time. If the in-target + // table fills up part-way through, entries written before the overflow remain + // set and the first failing per-entry SetCodeNotification surfaces a COMException + // with HResult == E_FAIL, which is mapped to the returned hr below. Callers that + // depend on atomic batch semantics should size their batches conservatively. int hr = HResults.S_OK; try From 8ff49df5a9a94b6f13be333c1b651cc0320e5ebe Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 27 Apr 2026 17:29:27 -0400 Subject: [PATCH 3/3] Address copilot reviewer feedback: trim trailing free entries and assert lazy-alloc size - CodeNotifications_1.SetCodeNotification clear path: replace single Length-- with a trailing-trim while-loop. The single decrement was insufficient when an interior hole already existed adjacent to the cleared tail entry, leaving Length inflated past the highest active index. - CodeNotificationsTests.SetCodeNotification_NullTable_LazyAllocates_ThenWorks: capture the requested allocation size in the AllocateMemory delegate and assert it equals EntrySize * (TableCapacity + 1) to guard the byte-size computation against regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/CodeNotifications_1.cs | 12 ++++++++++-- .../managed/cdac/tests/CodeNotificationsTests.cs | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs index 3f20fa3d6f8de6..db843bb9874708 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeNotifications_1.cs @@ -29,10 +29,18 @@ void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodTok { Data.JITNotification entry = v.GetEntry(foundIndex); entry.Clear(); - if (foundIndex == v.Length - 1) + + // Trim all trailing free entries so Length stays the smallest value satisfying + // "Length > index of every active entry". A single decrement is insufficient + // because earlier clears can leave an interior hole adjacent to the just-cleared + // tail entry (e.g., set A,B,C; clear B, then clear C should land at Length=1). + uint newLength = v.Length; + while (newLength > 0 && v.GetEntry(newLength - 1).IsFree) { - v.Length--; + newLength--; } + + v.Length = newLength; } return; diff --git a/src/native/managed/cdac/tests/CodeNotificationsTests.cs b/src/native/managed/cdac/tests/CodeNotificationsTests.cs index 8319fb94cf11c8..da9fd91ef26046 100644 --- a/src/native/managed/cdac/tests/CodeNotificationsTests.cs +++ b/src/native/managed/cdac/tests/CodeNotificationsTests.cs @@ -307,7 +307,12 @@ public void SetCodeNotification_NullTable_LazyAllocates_ThenWorks() (Constants.Globals.JITNotificationTableSize, TableCapacity) ); builder.AddContract(version: "c1"); - builder.UseAllocateMemory((size) => new TargetPointer(AllocatedTableAddress)); + uint? requestedAllocationSize = null; + builder.UseAllocateMemory((size) => + { + requestedAllocationSize = size; + return new TargetPointer(AllocatedTableAddress); + }); var target = builder.Build(); ICodeNotifications contract = target.Contracts.CodeNotifications; @@ -319,5 +324,6 @@ public void SetCodeNotification_NullTable_LazyAllocates_ThenWorks() CodeNotificationKind result = contract.GetCodeNotification(module, token); Assert.Equal(CodeNotificationKind.Generated, result); + Assert.Equal((uint)(EntrySize * (TableCapacity + 1)), requestedAllocationSize); } }