Skip to content

Add cDAC JIT notification contract with lazy table allocation#127114

Draft
max-charlamb wants to merge 9 commits intomainfrom
cdac/jit-notification-contract
Draft

Add cDAC JIT notification contract with lazy table allocation#127114
max-charlamb wants to merge 9 commits intomainfrom
cdac/jit-notification-contract

Conversation

@max-charlamb
Copy link
Copy Markdown
Member

@max-charlamb max-charlamb commented Apr 19, 2026

Note

This PR was created with assistance from GitHub Copilot.

Summary

Implement the cDAC JIT code notification contract, removing the legacy DAC allowlist fallback for SetCodeNotification. Adds a new dedicated ICodeNotifications contract (c1) covering SetCodeNotification, GetCodeNotification, SetAllCodeNotifications, and GetCodeNotificationCapacity.

Key Design Decisions

  • Separate ICodeNotifications contract: JIT code notification table management is split out of the existing read-only INotifications contract (which now only decodes debugger events). INotifications = read-only event decoding; ICodeNotifications = mutable in-target table management.
  • Lazy allocation via AllocVirtual: On Windows, the JIT notification table is NOT pre-allocated at startup. Instead, when a debugger first requests code notifications, the cDAC lazily allocates the table using ICLRDataTarget2::AllocVirtual. This avoids a ~24 KB per-process memory cost for apps that never use JIT notifications.
  • Capacity comes from a global, not from the table: Capacity is a compile-time invariant (1000) exposed via the JITNotificationTableSize global descriptor. Slot 0 of the table stores only the length (in its methodToken field); clrModule is unused. This avoids coupling the runtime's table-allocation path to the cDAC's direct-read model — no runtime changes are needed to seed bookkeeping fields.
  • Null table handling: When the table pointer is NULL (no debugger has requested notifications yet):
    • GetCodeNotification → throws InvalidOperationExceptionE_OUTOFMEMORY (matches legacy DAC)
    • SetAllCodeNotifications → no-op
    • SetCodeNotification with NONE → no-op
    • SetCodeNotification with flags → lazy-allocates the table, then sets
  • Batch capacity rejection: IXCLRDataProcess::SetCodeNotifications rejects upfront with E_OUTOFMEMORY when numTokens exceeds table capacity, matching legacy DAC and preventing partial writes on batch overflow.

Changes

Target Abstraction

  • Added AllocateMemory(uint size) virtual method to Target (defaults to NotSupportedException)
  • Added AllocVirtualDelegate and new TryCreate overload to ContractDescriptorTarget
  • Wired ICLRDataTarget2::AllocVirtual through CLRDataCreateInstance and the cdac_reader_init entrypoint
  • Extended cdac_reader.h / cdac.cpp / cdacstress.cpp to pass an optional alloc_virtual callback

Runtime

  • New data descriptor type JITNotification + globals JITNotificationTable, JITNotificationTableSize
  • New contract registration: CDAC_GLOBAL_CONTRACT(CodeNotifications, c1)
  • No changes to InitializeJITNotificationTable — default-construction zeroes slot 0's methodToken (length) naturally, and capacity lives in the global

Contract Implementation

  • New ICodeNotifications abstraction and CodeNotifications_1 implementation
  • New Data.JITNotification IData class with typed read/write helpers
  • Handles null table, lazy allocation, entry search, insert, update, clear, length bookkeeping

Legacy COM Wrappers

  • Implemented IXCLRDataMethodDefinition::GetCodeNotification/SetCodeNotification on ClrDataMethodDefinition
  • Implemented IXCLRDataProcess::SetCodeNotifications/GetCodeNotifications/SetAllCodeNotifications on SOSDacImpl
  • Replaced magic numbers with named constants (CLRDATA_METHNOTIFY_GENERATED/DISCARDED)
  • Removed SetCodeNotification from the LegacyFallbackHelper allowlist

Tests

  • 26 notification tests total (10 parse tests in NotificationsTests, 16 table-ops tests in CodeNotificationsTests)
  • TestPlaceholderTarget extended with an AllocateMemoryDelegate hook for lazy-allocation tests
  • Coverage: get/set/clear, multi-entry, update, module filtering, invalid flags, capacity API, null-table throws, null-table no-op paths, missing-allocator failure, lazy-allocate-then-read

Documentation

  • New docs/design/datacontracts/CodeNotifications.md with APIs, data descriptors, and pseudocode
  • Trimmed docs/design/datacontracts/Notifications.md to event decoding and cross-linked to CodeNotifications.md

Fixes #126760

Max Charlamb and others added 2 commits April 18, 2026 18:47
Add SetCodeNotification/GetCodeNotification/SetAllCodeNotifications APIs
to the INotifications contract, enabling the cDAC to manage the JIT
notification table directly instead of falling back to the legacy DAC.

Key changes:
- Runtime: Pre-allocate JIT notification table on all platforms (not just
  Unix), with proper bookkeeping initialization in slot 0
- Data descriptors: Expose JITNotification type and g_pNotificationTable
  global in the Notifications c1 contract
- Contract: Notifications_1 reads/writes entries directly in target memory
  (no host-side copy needed, unlike legacy DAC)
- Legacy layer: Wire up ClrDataMethodDefinition and SOSDacImpl batch APIs
  to use the contract, with DEBUG validation against legacy DAC
- Remove SetCodeNotification from legacy fallback allowlist
- Add 9 unit tests for JIT notification table operations
- Update Notifications.md design documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add AllocateMemory virtual method to Target abstraction with NotSupportedException default
- Add AllocVirtualDelegate and new TryCreate overload to ContractDescriptorTarget
- Wire ICLRDataTarget2.AllocVirtual through Entrypoints.cs
- Revert Windows pre-allocation: put InitializeJITNotificationTable back under TARGET_UNIX
- Handle null table in Notifications_1.cs: Get→NONE, SetAll→no-op, Set→lazy allocate
- Replace magic numbers in IsValidMethodCodeNotification with named constants
- Add 5 new tests: null table read, clear, no-allocator, lazy allocation
- Update Notifications.md to document lazy allocation behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements the cDAC JIT code notification APIs on the existing Notifications contract, including lazy allocation of the JIT notification table via ICLRDataTarget2.AllocVirtual, and removes the legacy DAC fallback allowlist entry for SetCodeNotification.

Changes:

  • Added JIT notification table support to INotifications (SetCodeNotification, GetCodeNotification, SetAllCodeNotifications) with null-table semantics and lazy allocation via Target.AllocateMemory.
  • Wired optional target allocation support end-to-end (ICLRDataTarget2.AllocVirtualContractDescriptorTargetTarget.AllocateMemory) and updated legacy COM wrappers to use the new contract APIs.
  • Added/expanded tests and documentation for the new contract behavior and table layout.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/native/managed/cdac/tests/TestPlaceholderTarget.cs Adds test-target hook to support Target.AllocateMemory for lazy allocation tests.
src/native/managed/cdac/tests/NotificationsTests.cs Adds extensive unit tests for JIT notification table get/set/clear, null-table behavior, and lazy allocation.
src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs Attempts to plumb ICLRDataTarget2.AllocVirtual into ContractDescriptorTarget.TryCreate.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs Adds AllocVirtualDelegate and a new TryCreate overload; implements AllocateMemory using AllocVirtual when available.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs Implements code notification APIs (Get/Set/SetAll*) using the cDAC Notifications contract (with DEBUG validation against legacy).
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/LegacyFallbackHelper.cs Removes SetCodeNotification from the legacy fallback allowlist.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs Exposes module address for use by code notification wrappers.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodDefinition.cs Implements IXCLRDataMethodDefinition.GetCodeNotification/SetCodeNotification via the contract.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/JITNotification.cs Adds contract data model for reading JITNotification entries.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Notifications_1.cs Implements JIT notification table logic, including lazy allocation and entry management.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs Adds new globals for table pointer and capacity.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs Adds Target.AllocateMemory(uint size) virtual API (default not supported).
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs Adds DataType.JITNotification.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/INotifications.cs Extends the Notifications contract interface with code notification APIs.
src/coreclr/vm/util.hpp Updates Unix table initialization to also initialize the bookkeeping slot (length/capacity).
src/coreclr/vm/datadescriptor/datadescriptor.inc Adds JITNotification type + new globals (JITNotificationTable, JITNotificationTableSize) to the contract descriptor.
docs/design/datacontracts/Notifications.md Documents the new APIs, table layout, and lazy allocation behavior.
Comments suppressed due to low confidence (1)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs:123

  • There is a stray /// </summary> XML doc line here that doesn't match any open <summary> tag (likely left behind when adding the new TryCreate overload). This will produce XML doc warnings/errors; remove it or restore the intended XML doc comment for the following member.
    /// </summary>
    /// <param name="contractDescriptor">The contract descriptor to use for this target</param>
    /// <param name="globalPointerValues">The values for any global pointers specified in the contract descriptor.</param>
    /// <param name="readFromTarget">A callback to read memory blocks at a given address from the target</param>
    /// <param name="writeToTarget">A callback to write memory blocks at a given address to the target</param>

Comment on lines +176 to +177
ClrDataAddress addr;
int result = dataTarget2.AllocVirtual(0, (uint)size, 0x1000 /* MEM_COMMIT */, 0x04 /* PAGE_READWRITE */, &addr);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

ClrDataAddress addr is not definitely assigned before being read to set allocatedAddress. This will fail compilation (use of unassigned local) and/or propagate garbage on allocation failure. Initialize addr (e.g., ClrDataAddress addr = 0;) and set allocatedAddress = 0 when AllocVirtual returns a failure HRESULT; also consider validating size fits in uint before casting.

Suggested change
ClrDataAddress addr;
int result = dataTarget2.AllocVirtual(0, (uint)size, 0x1000 /* MEM_COMMIT */, 0x04 /* PAGE_READWRITE */, &addr);
if (size > uint.MaxValue)
{
allocatedAddress = 0;
return unchecked((int)0x80070057); // E_INVALIDARG
}
ClrDataAddress addr = 0;
int result = dataTarget2.AllocVirtual(0, (uint)size, 0x1000 /* MEM_COMMIT */, 0x04 /* PAGE_READWRITE */, &addr);
if (result != 0)
{
allocatedAddress = 0;
return result;
}

Copilot uses AI. Check for mistakes.
Comment on lines +754 to +755
flags[i] = _target.Contracts.Notifications.GetCodeNotification(moduleAddr, tokens[i]);
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

This indexes flags[i]/tokens[i] up to numTokens without validating that the managed arrays are at least numTokens long. If a caller passes shorter arrays, this will throw IndexOutOfRangeException and return an unexpected HRESULT. Add explicit checks like numTokens <= tokens.Length and numTokens <= flags.Length and return E_INVALIDARG on mismatch.

Copilot uses AI. Check for mistakes.
Comment on lines +811 to +815
for (uint check = 0; check < numTokens; check++)
{
if (!IsValidMethodCodeNotification(flags[check]))
throw new ArgumentException("Invalid code notification flags");
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

The validation loop indexes flags[check] up to numTokens without checking that flags.Length >= numTokens (and later flags[i] similarly). If the arrays are shorter than numTokens, this will throw and surface as an unexpected HRESULT. Add explicit length checks (and also validate tokens.Length >= numTokens) and return E_INVALIDARG when violated.

Copilot uses AI. Check for mistakes.
#if DEBUG
if (_legacyProcess is not null)
{
int hrLocal = _legacyProcess.SetCodeNotifications(numTokens, mods, singleMod, tokens, flags!, singleFlags);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

In the DEBUG validation block, the legacy call passes flags! even though the method logic allows flags to be null (using singleFlags instead). If flags is ever null, this will throw and break DEBUG builds. Ensure the DEBUG legacy call is only made with a non-null array (e.g., allocate a temporary array filled with singleFlags when needed).

Suggested change
int hrLocal = _legacyProcess.SetCodeNotifications(numTokens, mods, singleMod, tokens, flags!, singleFlags);
uint[] flagsForLegacy = flags;
if (flagsForLegacy is null)
{
flagsForLegacy = new uint[checked((int)numTokens)];
Array.Fill(flagsForLegacy, singleFlags);
}
int hrLocal = _legacyProcess.SetCodeNotifications(numTokens, mods, singleMod, tokens, flagsForLegacy, singleFlags);

Copilot uses AI. Check for mistakes.
… ICLRDataTarget2

- Remove #if DEBUG validation blocks from all five JIT notification
  methods (SetCodeNotification, SetAllCodeNotifications, SetCodeNotifications,
  GetCodeNotification, GetCodeNotifications). Write operations cause
  dual-write corruption when both cDAC and legacy DAC independently
  allocate and write to g_pNotificationTable. Read operations have
  HResult mismatches when table is NULL (cDAC returns S_OK+NONE,
  legacy DAC returns E_OUTOFMEMORY).

- Replace 'as ICLRDataTarget2' cast with explicit Marshal.QueryInterface
  and raw vtable function pointer call. The 'as' cast on a
  StrategyBasedComWrappers RCW may not reliably perform QI for
  derived [GeneratedComInterface] interfaces. The explicit QI approach
  mirrors the native DAC's QueryInterface pattern in daccess.cpp.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 19, 2026 03:22
@max-charlamb max-charlamb force-pushed the cdac/jit-notification-contract branch from c052fb3 to d951242 Compare April 19, 2026 03:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs:124

  • There’s a stray XML doc line /// </summary> after the new TryCreate overload. This produces malformed XML documentation (and can fail the build if warnings are treated as errors). Remove it or restore the missing opening documentation element for the following member.
    }
    /// </summary>
    /// <param name="contractDescriptor">The contract descriptor to use for this target</param>
    /// <param name="globalPointerValues">The values for any global pointers specified in the contract descriptor.</param>
    /// <param name="readFromTarget">A callback to read memory blocks at a given address from the target</param>
    /// <param name="writeToTarget">A callback to write memory blocks at a given address to the target</param>
    /// <param name="getThreadContext">A callback to fetch a thread's context</param>

Comment on lines +316 to +320
uint tableByteSize = entrySize * (capacity + 1);
TargetPointer tablePointer = _target.AllocateMemory(tableByteSize);

// Zero-initialize the entire table
byte[] zeros = new byte[tableByteSize];
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

AllocateTable uses uint tableByteSize and then new byte[tableByteSize]. Array lengths are int, so this won’t compile (and also needs an overflow check when casting). Consider using an int byte count (checked) for the managed buffer size, or allocate/write in chunks to avoid a large managed allocation.

Suggested change
uint tableByteSize = entrySize * (capacity + 1);
TargetPointer tablePointer = _target.AllocateMemory(tableByteSize);
// Zero-initialize the entire table
byte[] zeros = new byte[tableByteSize];
uint tableByteSize = checked(entrySize * checked(capacity + 1));
TargetPointer tablePointer = _target.AllocateMemory(tableByteSize);
// Zero-initialize the entire table
int managedTableByteSize = checked((int)tableByteSize);
byte[] zeros = new byte[managedTableByteSize];

Copilot uses AI. Check for mistakes.
Comment on lines +989 to +994
private static TargetPointer GetModuleAddress(void* comModulePtr)
{
StrategyBasedComWrappers cw = new();
object obj = cw.GetOrCreateObjectForComInstance((nint)comModulePtr, CreateObjectFlags.None);
if (obj is ClrDataModule cdm)
return cdm.Address;
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

GetModuleAddress constructs a new StrategyBasedComWrappers instance each time it’s called. Since GetCodeNotifications/SetCodeNotifications call this in a loop, this can create many ComWrappers instances and defeat per-instance caching (potentially creating multiple managed wrappers for the same COM pointer). Consider creating a single StrategyBasedComWrappers per API call (outside the loop) and reusing it when resolving module addresses.

Suggested change
private static TargetPointer GetModuleAddress(void* comModulePtr)
{
StrategyBasedComWrappers cw = new();
object obj = cw.GetOrCreateObjectForComInstance((nint)comModulePtr, CreateObjectFlags.None);
if (obj is ClrDataModule cdm)
return cdm.Address;
private static readonly StrategyBasedComWrappers s_comWrappers = new();
private static TargetPointer GetModuleAddress(void* comModulePtr)
{
object obj = s_comWrappers.GetOrCreateObjectForComInstance((nint)comModulePtr, CreateObjectFlags.None);
if (obj is ClrDataModule cdm)
return cdm.Address;

Copilot uses AI. Check for mistakes.
Comment on lines +304 to +308
[Fact]
public void SetCodeNotification_InvalidFlags_Throws()
{
INotifications contract = CreateContractWithJITTable();
TargetPointer module = new(0x1234);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

The PR description mentions test coverage for the “table full” case, but there doesn’t appear to be a test exercising the InvalidOperationException("JIT notification table is full") path. Consider adding a test that fills the table to capacity and verifies the expected failure behavior.

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

Add alloc_virtual callback parameter to cdac_reader_init so the CI test
path (cdac_reader_init -> cdac_reader_create_sos_interface) has access
to ICLRDataTarget2::AllocVirtual for lazy JIT notification table
allocation.

Changes:
- cdac_reader.h: Add alloc_virtual callback parameter (nullable)
- cdac.cpp: Add AllocVirtualCallback that QIs ICorDebugDataTarget for
  ICLRDataTarget2, probe at init time and pass callback or nullptr
- cdacstress.cpp: Pass nullptr for alloc_virtual (in-process stress)
- Entrypoints.cs: Accept alloc_virtual function pointer, wrap as
  AllocVirtualDelegate, call 6-parameter TryCreate overload
- Notifications.md: Update docs to reflect alloc_virtual availability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #127114

Note

This review was generated by Copilot using multi-model analysis (Claude Opus 4.6 primary, GPT-5.3-Codex and Claude Sonnet 4.5 sub-agents).

Holistic Assessment

Motivation: This PR is well-motivated. It migrates JIT notification table management from the legacy C++ DAC to the managed cDAC, consistent with the ongoing effort to replace the legacy DAC. The notification table protocol is faithfully reproduced, and the lazy allocation pattern for Windows (where the table starts null) is a sensible addition.

Approach: The approach is sound and follows established cDAC conventions — data descriptors in datadescriptor.inc, a contract implementation in Notifications_1, legacy integration in SOSDacImpl/ClrDataMethodDefinition, and unit tests. The AllocateMemory abstraction is cleanly layered through TargetContractDescriptorTarget → native callbacks.

Summary: ⚠️ Needs Human Review. The core contract implementation is correct and well-tested. However, there are several issues that a maintainer should evaluate: a potential COM resource management concern in GetModuleAddress, a broken XML doc comment introduced by the PR, a NotSupportedException that maps to a misleading HRESULT, and some test coverage gaps. None appear to be critical bugs, but they warrant human judgment before merge.


Detailed Findings

⚠️ GetModuleAddress creates a new StrategyBasedComWrappers on every call

Flagged by: all 3 models

SOSDacImpl.IXCLRDataProcess.cs:989-992:

private static TargetPointer GetModuleAddress(void* comModulePtr)
{
    StrategyBasedComWrappers cw = new();
    object obj = cw.GetOrCreateObjectForComInstance((nint)comModulePtr, CreateObjectFlags.None);

This is called in a loop inside both GetCodeNotifications (line 746) and SetCodeNotifications (line 817). Each invocation creates a new StrategyBasedComWrappers instance and a new managed wrapper for the same COM pointer. This may create orphaned RCWs or cause COM reference-counting issues. Consider using a shared/cached ComWrappers instance, or extracting the module address through an alternative mechanism that doesn't involve wrapping the COM pointer each time.

⚠️ NotSupportedException leaks unexpected HRESULT through generic catch

Flagged by: GPT, confirmed by primary review

When AllocateMemory is unavailable (no ICLRDataTarget2), the call chain is:
SetCodeNotificationAllocateTableTarget.AllocateMemory → throws NotSupportedException

The catch blocks in SOSDacImpl and ClrDataMethodDefinition map InvalidOperationExceptionE_OUTOFMEMORY and ArgumentExceptionE_INVALIDARG, but NotSupportedException falls through to the generic catch (Exception ex) { hr = ex.HResult; }, returning COR_E_NOTSUPPORTED (0x80131515). Callers likely expect E_OUTOFMEMORY or E_NOTIMPL. Consider adding an explicit catch for NotSupportedExceptionHResults.E_NOTIMPL.

Affected locations:

  • ClrDataMethodDefinition.cs:150-157 (SetCodeNotification)
  • SOSDacImpl.IXCLRDataProcess.cs:824-831 (SetCodeNotifications)
  • SOSDacImpl.IXCLRDataProcess.cs:667-689 (SetAllCodeNotifications)

⚠️ Stray /// </summary> XML doc tag on ContractDescriptorTarget.Create

ContractDescriptorTarget.cs:119:

    }
    /// </summary>   // ← orphaned closing tag, missing <summary> opener
    /// <param name="contractDescriptor">The contract descriptor to use for this target</param>

The new TryCreate overload was inserted between the existing Create method's <summary> opening tag and </summary> closing tag. The <summary> was consumed by the new overload's doc, leaving a stray </summary> before Create's <param> tags. This should be replaced with a proper <summary> block for the Create method.

⚠️ Missing test: table-full error path

Flagged by: GPT, Sonnet

Notifications_1.cs:139-140 throws InvalidOperationException("JIT notification table is full") but no test exercises this path. Since TableCapacity is only 10 in tests, it would be straightforward to add a test that fills the table and verifies the exception.

SetAllCodeNotifications correctly fixes a bug in native length recomputation

Flagged by: Sonnet, confirmed by primary review

The native SetAllNotifications (util.cpp:1142-1149) decrements length for every free entry in the table, not just trailing ones. With entries [USED(A), FREE, USED(B)] and a module-filtered clear of A, the native code would incorrectly set length=1, losing track of entry B at index 2.

The cDAC correctly walks backwards and stops at the first non-free entry (lines 234-243). This is the right behavior. Recommend documenting this as an intentional improvement over the native implementation.

✅ Bookkeeping layout and entry management match native semantics

The slot-0 bookkeeping convention (methodToken → length, clrModule → capacity), entry indexing starting at 1, free-slot reuse, and length-only-shrink-on-trailing-remove behavior all faithfully reproduce src/coreclr/vm/util.cpp:1068-1271. The Unix pre-allocation in util.hpp:599-611 now properly initializes these bookkeeping fields.

✅ Lazy allocation and null-table handling are correct

The contract correctly handles the Windows case (null table initially) with graceful degradation: GetCodeNotification → returns NONE, SetAllCodeNotifications → no-op, SetCodeNotification(NONE) → no-op, SetCodeNotification(non-zero) → lazy allocate.

✅ DEBUG validation correctly skipped for write operations

Write operations skip #if DEBUG comparison between cDAC and legacy DAC because both independently write to g_pNotificationTable (dual-write corruption). Read operations skip validation because the cDAC intentionally returns S_OK + NONE on null tables (improvement over legacy DAC's E_OUTOFMEMORY).

💡 Type inconsistency in CLRDATA_METHNOTIFY_* constants

Notifications_1.cs:11-13: CLRDATA_METHNOTIFY_NONE is ushort while CLRDATA_METHNOTIFY_GENERATED and CLRDATA_METHNOTIFY_DISCARDED are uint. Functionally correct but could be made consistent.

💡 No 32-bit architecture test coverage

All tests use Is64Bit = true. The WriteNUInt helper and pointer-size-dependent logic in AllocateTable have 32-bit code paths that are untested. Consider adding at least one 32-bit variant as a follow-up.

💡 Duplicated WriteNUInt pattern in AllocateTable

Notifications_1.cs:330-333 manually checks pointer size to write the global pointer, duplicating the existing WriteNUInt helper at lines 289-295. Could reuse WriteNUInt instead.

Generated by Code Review for issue #127114 ·

…nify constant types, reuse WriteNUInt

- Fix stray </summary> XML doc tag on ContractDescriptorTarget.Create
- Replace StrategyBasedComWrappers with ComWrappers.TryGetObject in GetModuleAddress
- Make CLRDATA_METHNOTIFY_NONE uint to match other constants
- Reuse WriteNUInt helper in AllocateTable instead of duplicating logic

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WriteToTargetDelegate writeToTarget,
GetTargetThreadContextDelegate getThreadContext)
GetTargetThreadContextDelegate getThreadContext,
AllocVirtualDelegate? allocVirtual = null)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Lets make this non-nullable, if we don't have a callback, we can create one that throws an exception.

{
return writeToTarget(address, buffer);
}
public bool HasAllocVirtual => allocVirtual is not null;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We can remove this and just call the method directly now that it is non-nullable.

if (!IsValidMethodCodeNotification(flags))
throw new ArgumentException("Invalid code notification flags", nameof(flags));

Target.TypeInfo jitNotifType = _target.GetTypeInfo(DataType.JITNotification);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Lets make JITNotification a IData class with write-back capabilities. We should avoid using TypeInfo inside the contracts.

…ble AllocVirtual

- JITNotification Data class now has write-back methods (WriteState,
  WriteClrModule, WriteMethodToken, Clear, WriteEntry) matching the
  established Module.WriteFlags pattern
- Notifications_1 refactored to use Data.JITNotification for all reads
  and writes instead of raw TypeInfo offset lookups
- AllocVirtual delegate made non-nullable in DataTargetDelegates with
  ThrowAllocVirtual as the default, removing HasAllocVirtual and null
  checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 20, 2026 00:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.

Comment on lines +195 to +200
allocVirtual = (ulong size, out ulong allocatedAddress) =>
{
ClrDataAddress addr;
int result = dataTarget2.AllocVirtual(0, (uint)size, 0x1000 /* MEM_COMMIT */, 0x04 /* PAGE_READWRITE */, &addr);
allocatedAddress = (ulong)addr;
return result;
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The AllocVirtual call uses raw constants (0x1000 for MEM_COMMIT and 0x04 for PAGE_READWRITE). Consider replacing these with named constants (or local const fields) to make the flags unambiguous and reduce the chance of accidental misuse.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +88
void INotifications.SetCodeNotification(TargetPointer module, uint methodToken, uint flags)
{
if (!IsValidMethodCodeNotification(flags))
throw new ArgumentException("Invalid code notification flags", nameof(flags));

uint entrySize = GetEntrySize();
TargetPointer tablePointer = ReadTablePointer();

// If table is null and we're clearing, nothing to do
if (tablePointer == TargetPointer.Null && flags == CLRDATA_METHNOTIFY_NONE)
return;

// If table is null and we're setting, lazily allocate
if (tablePointer == TargetPointer.Null)
{
tablePointer = AllocateTable(entrySize);
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

INotifications.SetCodeNotification currently allows module == TargetPointer.Null, but the underlying runtime implementation rejects a null module (see JITNotifications::SetNotification returning FALSE when clrModule == NULL). Consider validating module != TargetPointer.Null and throwing ArgumentException to avoid inserting entries with a null module pointer.

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +155
uint INotifications.GetCodeNotification(TargetPointer module, uint methodToken)
{
uint entrySize = GetEntrySize();
TargetPointer tablePointer = ReadTablePointer();

if (tablePointer == TargetPointer.Null)
return CLRDATA_METHNOTIFY_NONE;

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

INotifications.GetCodeNotification also treats module == TargetPointer.Null as a normal lookup key. If null modules are invalid inputs (as in the native JIT notification table), it would be better to reject TargetPointer.Null here too (e.g., throw ArgumentException) rather than returning a potentially misleading value.

Copilot uses AI. Check for mistakes.
Comment on lines 143 to +161
int IXCLRDataMethodDefinition.SetCodeNotification(uint flags)
=> LegacyFallbackHelper.CanFallback() && _legacyImpl is not null ? _legacyImpl.SetCodeNotification(flags) : HResults.E_NOTIMPL;
{
int hr = HResults.S_OK;
try
{
_target.Contracts.Notifications.SetCodeNotification(_module, _token, flags);
}
catch (System.ArgumentException)
{
hr = HResults.E_INVALIDARG;
}
catch (System.InvalidOperationException)
{
hr = HResults.E_OUTOFMEMORY;
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

SetCodeNotification can throw NotSupportedException when AllocateMemory isn't available (e.g., target doesn't support ICLRDataTarget2). The current catch-all will return NotSupportedException.HResult (COR_E_NOTSUPPORTED) to COM callers, which is typically not the expected HRESULT for an unsupported DAC API. Consider catching NotSupportedException explicitly and translating it to E_NOTIMPL (or another agreed HRESULT) at the COM boundary.

Copilot uses AI. Check for mistakes.
if (obj is ClrDataModule cdm)
return cdm.Address;
}
throw new InvalidOperationException("Could not resolve module address from COM pointer");
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

GetModuleAddress throws InvalidOperationException when the COM pointer can't be resolved. That exception type currently gets translated to E_OUTOFMEMORY by GetCodeNotifications/SetCodeNotifications, which is misleading. Consider throwing ArgumentException (or a COMException with E_INVALIDARG) here so callers reliably get E_INVALIDARG for bad module pointers.

Suggested change
throw new InvalidOperationException("Could not resolve module address from COM pointer");
throw new ArgumentException("Could not resolve module address from COM pointer", nameof(comModulePtr));

Copilot uses AI. Check for mistakes.
Comment on lines 667 to +693
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
{
TargetPointer moduleAddr = TargetPointer.Null;
if (mod is not null)
{
if (mod is not ClrDataModule cdm)
throw new ArgumentException();
moduleAddr = cdm.Address;
}

_target.Contracts.Notifications.SetAllCodeNotifications(moduleAddr, flags);
}
catch (System.ArgumentException)
{
hr = HResults.E_INVALIDARG;
}
catch (System.InvalidOperationException)
{
hr = HResults.E_OUTOFMEMORY;
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

SetAllCodeNotifications may indirectly trigger lazy table allocation, which can throw NotSupportedException when the target doesn't support allocation. This method currently falls through to the generic exception handler and returns NotSupportedException.HResult (COR_E_NOTSUPPORTED). Consider catching NotSupportedException explicitly and returning E_NOTIMPL (or an agreed HRESULT) to keep COM-facing behavior consistent.

Copilot uses AI. Check for mistakes.
Comment on lines 775 to +835
@@ -699,7 +779,66 @@ int IXCLRDataProcess.SetCodeNotifications(
[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
{
ArgumentNullException.ThrowIfNull(tokens);
if ((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 (!IsValidMethodCodeNotification(flags[check]))
throw new ArgumentException("Invalid code notification flags");
}
}
else if (!IsValidMethodCodeNotification(singleFlags))
{
throw new ArgumentException("Invalid code notification flags");
}

TargetPointer singleModuleAddr = TargetPointer.Null;
if (singleMod is not null)
{
if (singleMod is not ClrDataModule singleCdm)
throw new ArgumentException();
singleModuleAddr = singleCdm.Address;
}

for (uint i = 0; i < numTokens; i++)
{
TargetPointer moduleAddr = singleModuleAddr;
if (mods is not null)
{
moduleAddr = GetModuleAddress(mods[i]);
}

uint f = flags is not null ? flags[i] : singleFlags;
_target.Contracts.Notifications.SetCodeNotification(moduleAddr, tokens[i], f);
}
}
catch (System.ArgumentException)
{
hr = HResults.E_INVALIDARG;
}
catch (System.InvalidOperationException)
{
hr = HResults.E_OUTOFMEMORY;
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

SetCodeNotifications can also hit the lazy-allocation path and throw NotSupportedException when ICLRDataTarget2/AllocVirtual isn't available. As written, that will return COR_E_NOTSUPPORTED via the generic catch, which is an uncommon HRESULT for COM APIs here. Consider catching NotSupportedException explicitly and translating to E_NOTIMPL (or another agreed HRESULT).

Copilot uses AI. Check for mistakes.
Max Charlamb and others added 3 commits April 20, 2026 14:36
Extract SetCodeNotification, GetCodeNotification, and SetAllCodeNotifications
into a new ICodeNotifications contract, leaving INotifications focused on
read-only event decoding (TryParseNotification, SetGcNotification).

The two responsibilities are materially different: INotifications decodes
exception-record payloads raised by the runtime; ICodeNotifications writes
into an in-process allowlist and lazily allocates the table via AllocVirtual.

Also adds GetCodeNotificationCapacity() to the new contract in preparation
for an upfront capacity check in batch SOSDacImpl.SetCodeNotifications.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two DAC-parity fixes surfaced by self-review of PR #127114:

1. Batch IXCLRDataProcess::SetCodeNotifications now rejects upfront with
   E_OUTOFMEMORY when numTokens exceeds the table capacity. Matches the
   legacy DAC (daccess.cpp: 'numTokens > jn.GetTableSize()') and prevents
   partial writes when a batch overflows mid-loop.

2. ICodeNotifications.GetCodeNotification now throws InvalidOperationException
   (mapped to E_OUTOFMEMORY by wrappers) when the table has not been
   allocated, matching legacy DAC behavior (JITNotifications::IsActive()).
   Previously returned CLRDATA_METHNOTIFY_NONE, which was an unintentional
   behavioral divergence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the cDAC read the JIT notification table capacity from slot 0's
ClrModule field, which required the runtime to initialize that field at
startup on Unix (where the table is pre-allocated) so the cDAC would see a
valid capacity. This coupled runtime init to the cDAC's direct-read model.

The capacity is a compile-time invariant (1000) already exposed via the
JITNotificationTableSize global descriptor. Switch to reading the global,
which:
- Removes the runtime-side bookkeeping init (util.hpp change reverted).
- Removes the bookkeeping capacity write from AllocateTable; zero-filling
  the buffer is sufficient (length starts at 0 naturally).
- Leaves slot 0's ClrModule field unused.

Default-construction (Unix) and AllocVirtual zero-init (Windows) both give
length = 0 in slot 0 without any explicit writes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 20, 2026 21:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.

Comment on lines +293 to +298
var arch = new MockTarget.Architecture { IsLittleEndian = true, Is64Bit = true };
var helpers = new TargetTestHelpers(arch);

int totalTableSize = EntrySize * ((int)TableCapacity + 1);
byte[] allocatedTableData = new byte[totalTableSize];
const ulong AllocatedTableAddress = 0x3_0000;
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

Unused local helpers will produce compiler warning CS0219 and can break builds if warnings are treated as errors (common in CI). Remove the variable or use it for table initialization in this test.

Copilot uses AI. Check for mistakes.
if (obj is ClrDataModule cdm)
return cdm.Address;
}
throw new InvalidOperationException("Could not resolve module address from COM pointer");
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

GetModuleAddress throws InvalidOperationException, but callers (e.g., GetCodeNotifications) catch InvalidOperationException and translate it to E_OUTOFMEMORY. Failing to resolve the COM module pointer should map to E_INVALIDARG instead; throw ArgumentException (or a dedicated exception type) here, or adjust the catch blocks to avoid misclassifying this failure as OOM.

Suggested change
throw new InvalidOperationException("Could not resolve module address from COM pointer");
throw new ArgumentException("Could not resolve module address from COM pointer", nameof(comModulePtr));

Copilot uses AI. Check for mistakes.
Comment on lines +741 to +750
for (uint i = 0; i < numTokens; i++)
{
TargetPointer moduleAddr = singleModuleAddr;
if (mods is not null)
{
moduleAddr = GetModuleAddress(mods[i]);
}

flags[i] = _target.Contracts.CodeNotifications.GetCodeNotification(moduleAddr, tokens[i]);
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

This loop indexes mods[i], tokens[i], and flags[i] without validating that the provided arrays have lengths >= numTokens. If a caller passes inconsistent lengths, this will throw IndexOutOfRangeException and return an unintended HRESULT. Add explicit length checks for tokens, flags, and (when non-null) mods, and return E_INVALIDARG on mismatch.

Copilot uses AI. Check for mistakes.
Comment on lines +794 to +798
for (uint check = 0; check < numTokens; check++)
{
if (!IsValidMethodCodeNotification(flags[check]))
throw new ArgumentException("Invalid code notification flags");
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

When flags is non-null, this loop assumes flags.Length >= numTokens. If shorter, it will throw IndexOutOfRangeException (unexpected HRESULT). Validate flags.Length (and mods.Length when applicable) against numTokens up front and return E_INVALIDARG for inconsistent inputs.

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +201
allocVirtual = (ulong size, out ulong allocatedAddress) =>
{
ClrDataAddress addr;
int result = dataTarget2.AllocVirtual(0, (uint)size, 0x1000 /* MEM_COMMIT */, 0x04 /* PAGE_READWRITE */, &addr);
allocatedAddress = (ulong)addr;
return result;
};
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

This introduces Windows API magic numbers for MEM_COMMIT (0x1000) and PAGE_READWRITE (0x04). Replace them with named constants (e.g., from an existing interop/constants location in the repo, or local const uint fields) to make the intent clear and reduce the chance of flag misuse.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
Target.TypeInfo type = target.GetTypeInfo(DataType.JITNotification);

State = target.ReadField<ushort>(address, type, nameof(State));
ClrModule = target.ReadNUIntField(address, type, nameof(ClrModule));
MethodToken = target.ReadField<uint>(address, type, nameof(MethodToken));
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

JITNotification repeatedly calls target.GetTypeInfo(DataType.JITNotification) in the constructor and again in every write helper. In loops over the table (up to 1000 entries), this can add avoidable overhead. Consider caching the TypeInfo (and/or field offsets) once per contract instance and passing it into JITNotification, or storing it in the JITNotification instance so write helpers reuse the same TypeInfo.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cDAC] Track no-fallback allowlist — remaining unimplemented APIs

2 participants