Skip to content

Add cDAC JIT notification contract with lazy table allocation#127114

Open
max-charlamb wants to merge 3 commits intomainfrom
cdac/jit-notification-contract
Open

Add cDAC JIT notification contract with lazy table allocation#127114
max-charlamb wants to merge 3 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 and the four IXCLRDataMethodDefinition fallbacks. Adds a new dedicated ICodeNotifications contract (c1) covering GetCodeNotification, SetCodeNotification, and SetAllCodeNotifications.

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. 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) read internally 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 is the "no notifications" state: On Windows, g_pNotificationTable starts as NULL until the first non-trivial SetCodeNotification call. The contract treats a NULL table as equivalent to "no entries":
    • GetCodeNotificationNone (same observable state as "method not registered"). Deliberate simplification from the legacy DAC, which returned E_OUTOFMEMORY via JITNotifications::IsActive(); the information to the caller is identical in both cases.
    • SetAllCodeNotifications → no-op.
    • SetCodeNotification with None → no-op (nothing to clear).
    • SetCodeNotification with flags → lazy-allocates the table via ICLRDataTarget2::AllocVirtual, then sets.
  • Table-full failure: SetCodeNotification throws COMException with HResult == E_FAIL when no free slot is available, matching the legacy DAC's SetNotification failure path.
  • No upfront batch capacity check: IXCLRDataProcess::SetCodeNotifications does not reject oversize batches upfront. If a batch would overflow the in-target table, the contract writes as many entries as fit and then throws E_FAIL from the first SetCodeNotification that cannot allocate a slot. This deliberately diverges from the legacy DAC (which rejected with E_OUTOFMEMORY before writing anything); rather than expose a capacity guarantee that could drift from the runtime's actual behavior, the contract surfaces the same failure through the normal per-entry path. Invalid flag combinations still throw ArgumentException (→ E_INVALIDARG).
  • SetAllCodeNotifications length trim: Deliberately diverges from JITNotifications::SetAllNotifications in src/coreclr/vm/util.cpp. The native algorithm decrements Length for every freed slot in [0, OldLength), which can trim Length below the index of still-active entries from other modules, making them unreachable via TryFindEntry. The cDAC trims only trailing free entries. Documented with an explanatory comment in both code and docs.

Changes

Target Abstraction

  • Added AllocateMemory(uint size) virtual method to Target (defaults to NotImplementedException).
  • Added WriteNUInt(ulong, TargetNUInt) abstract method to Target; WriteNUIntField/WritePointerField extensions now delegate to Target.WriteNUInt/WritePointer instead of inlining pointer-size branching.
  • Added AllocVirtualDelegate (non-nullable) to ContractDescriptorTarget.TryCreate/Create; callers that cannot allocate supply an E_NOTIMPL-returning or throwing delegate.
  • 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 and caching.
  • TableView holds a Data.JITNotification IData for the sentinel slot (index 0) and routes length reads/writes through it; capacity is read from the JITNotificationTableSize global.
  • Handles null table, lazy allocation, entry search, insert, update, clear, length bookkeeping, table-full → E_FAIL.

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); added a CodeNotificationFlagsConverter between the COM flag bits and the CodeNotificationKind enum.
  • Removed SetCodeNotification and the three IXCLRDataMethodDefinition entries (StartEnumInstances, GetName, HasClassOrMethodInstantiation) from the LegacyFallbackHelper allowlist.

Tests

  • 20+ notification tests across NotificationsTests (event decoding) and CodeNotificationsTests (table ops, null-table, lazy-allocate, module filtering, batch, table-full).
  • TestPlaceholderTarget extended with an AllocateMemoryDelegate hook and a WriteNUInt override.
  • ContractDescriptorBuilder, DumpTestBase, and StressLogAnalyzer updated for the new non-nullable AllocVirtualDelegate creation API.

Documentation

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

Fixes #126760

This comment was marked as outdated.

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 thread src/native/managed/cdac/tests/NotificationsTests.cs Outdated
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

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 thread src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs
max-charlamb pushed a commit that referenced this pull request Apr 20, 2026
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>
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 thread src/native/managed/cdac/tests/CodeNotificationsTests.cs
Comment thread src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs
Comment thread src/coreclr/vm/datadescriptor/datadescriptor.inc Outdated
Comment thread src/native/managed/cdac/tests/NotificationsTests.cs
Copilot AI review requested due to automatic review settings April 21, 2026 18:02
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 27 out of 27 changed files in this pull request and generated 9 comments.

Comment thread docs/design/datacontracts/CodeNotifications.md
Comment thread src/native/managed/cdac/tests/TestPlaceholderTarget.cs Outdated
Comment thread docs/design/datacontracts/CodeNotifications.md Outdated
max-charlamb pushed a commit that referenced this pull request Apr 22, 2026
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>
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

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 30 out of 30 changed files in this pull request and generated 4 comments.

Comment thread src/native/managed/cdac/tests/CodeNotificationsTests.cs Outdated
Comment thread docs/design/datacontracts/CodeNotifications.md Outdated
@max-charlamb max-charlamb force-pushed the cdac/jit-notification-contract branch from bfa14cc to 851dc52 Compare April 27, 2026 20:31
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>
@github-actions

This comment has been minimized.

…ert 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>
Copilot AI review requested due to automatic review settings April 27, 2026 21:30
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 30 out of 30 changed files in this pull request and generated 3 comments.

Comment on lines +208 to +210
ClrDataAddress addr;
int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr);
allocatedAddress = (ulong)addr;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

In CLRDataCreateInstanceImpl, the allocVirtual delegate casts size to uint and uses an uninitialized ClrDataAddress addr. If size ever exceeds uint.MaxValue this will truncate, and if AllocVirtual fails without writing addr the returned allocatedAddress may contain garbage. Please mirror the safer pattern used earlier in Init: validate the size fits in uint, initialize addr to 0, and only set allocatedAddress on success (or explicitly zero it on failure).

Suggested change
ClrDataAddress addr;
int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr);
allocatedAddress = (ulong)addr;
allocatedAddress = 0;
if (size > uint.MaxValue)
{
return HResults.E_INVALIDARG;
}
ClrDataAddress addr = 0;
int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr);
if (result >= 0)
{
allocatedAddress = (ulong)addr;
}

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19

void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags)
{
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

SetCodeNotification accepts any CodeNotificationKind value and later casts it to ushort when writing the table. That means undefined bits can be silently truncated or persisted into the target table if a non-validated value is passed (e.g., by non-COM consumers). Consider validating that flags contains only the supported bits (Generated/Discarded) and throwing ArgumentException for invalid values to keep contract behavior well-defined and aligned with the documented COM-layer validation.

Suggested change
void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags)
{
private static void ValidateFlags(CodeNotificationKind flags)
{
CodeNotificationKind validFlags = CodeNotificationKind.Generated | CodeNotificationKind.Discarded;
if ((flags & ~validFlags) != 0)
{
throw new ArgumentException("Only Generated and Discarded notification flags are supported.", nameof(flags));
}
}
void ICodeNotifications.SetCodeNotification(TargetPointer module, uint methodToken, CodeNotificationKind flags)
{
ValidateFlags(flags);

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +103
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;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

SetAllCodeNotifications writes (ushort)flags into each entry without validating that flags contains only the supported bits. This can allow invalid values to be stored in the target table if the contract is used directly. Recommend validating flags (same mask used by SetCodeNotification) and throwing ArgumentException for unsupported bits.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #127114

Note

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

Holistic Assessment

Motivation: This PR adds a new ICodeNotifications cDAC contract that provides read/write access to the JIT code notification table, enabling the cDAC to handle SetCodeNotification/GetCodeNotification/SetAllCodeNotifications without falling back to the legacy DAC. This is a necessary step in the cDAC migration for debugger notification infrastructure. The motivation is clear and justified.

Approach: The approach is sound: a new contract (CodeNotifications_1) backed by a TableView abstraction over the in-target notification table, with lazy allocation support plumbed through a new AllocateMemory callback in the Target abstraction. The design properly handles NULL tables (Windows) vs pre-allocated tables (Unix), and deliberately diverges from the native code's buggy length-trimming algorithm. The flag conversion at the COM boundary is properly explicit per-bit rather than a numeric cast.

Summary: ⚠️ Needs Human Review. The core CodeNotifications contract implementation is well-structured and correct in the common paths. Three findings need human attention: (1) the table byte-size multiplication should use checked arithmetic to guard against corrupted target data, (2) an inconsistent narrowing cast in the CLRDataCreateInstanceImpl allocator path, and (3) the test suite lacks coverage for the table-full overflow path. The PR also bundles significant unrelated cleanup changes that a maintainer should confirm are intentional.


Detailed Findings

⚠️ Integer Overflow in Table Allocation — CodeNotifications_1.cs:236 (flagged by all 3 models)

uint tableByteSize = entrySize * (capacity + 1);

This multiplication is unchecked. While typical values are safe (24 × 1001 = 24024), capacity comes from a global read from the target process. Corrupted target data (e.g., capacity near uint.MaxValue) would silently wrap around, allocating a much smaller buffer than expected, and subsequent writes would corrupt target memory. The checked((int)tableByteSize) on line 239 catches some overflows but not all — a multiplication that wraps to a small positive uint would pass the checked cast.

Recommendation: Use checked(entrySize * (capacity + 1)) to catch overflow at the multiplication site.

⚠️ Inconsistent Narrowing Cast in CLRDataCreateInstanceImpl — Entrypoints.cs:209 (flagged by GPT)

int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr);

The Init path (lines 36–40) correctly checks size > uint.MaxValue before casting to uint, but the CLRDataCreateInstanceImpl path does not have this guard. Since AllocateMemory takes uint, the cast is practically safe today, but this is inconsistent with the other code path.

Recommendation: Add the same size > uint.MaxValue guard for consistency, or add a comment explaining why it's unnecessary.

⚠️ Missing Test: Table-Full Overflow Path (flagged by all 3 models)

SetCodeNotification throws COMException with E_FAIL when the table is full (lines 66–72 of CodeNotifications_1.cs), but no test exercises this path. This is a behavioral boundary that diverges from the legacy DAC.

Recommendation: Add a test that inserts TableCapacity entries and then attempts one more, asserting COMException with HResult == E_FAIL.

⚠️ No Length Validation Before Table Scans (flagged by GPT)

TryFindEntry (line 182), SetCodeNotification insert scan (line 57), and SetAllCodeNotifications (line 107) all iterate up to Length without validating Length <= Capacity. A corrupted or racy length value from the target could cause out-of-bounds reads from target memory. Advisory: consider clamping Length to Math.Min(Length, Capacity) in TryFindEntry and scan loops.

⚠️ Mixed Scope — Unrelated Changes Bundled

The PR bundles several changes unrelated to the CodeNotifications contract:

  • Removal of MarkDebuggerAttachPending/MarkDebuggerAttached from the Debugger contract and CORDebuggerControlFlags global
  • Removal of DACF_IGNORE_PDBS, EncCapable from Loader; MainProfilerProfInterface/NotificationProfilerCount from ProfControlBlock
  • Removal of Apple from RuntimeInfoOperatingSystem
  • Reversion of several DacDbiImpl methods to legacy fallback
  • Simplification of ClrDataModule MetaDataImport QI handling
  • PrecodeMachineDescriptor field name fixes

A maintainer should confirm these removals are intentional and coordinated with the base branch vs. accidental merge artifacts.

✅ Correct Trailing-Trim Algorithm (confirmed by all 3 models)

The trailing-trim while-loop in both SetCodeNotification (clear path, lines 38–41) and SetAllCodeNotifications (lines 137–141) correctly trims Length to the smallest value satisfying "Length > index of every active entry." The GetEntry() method correctly maps indices starting from the first actual entry (after the sentinel at basePointer), so trimming to newLength=0 when all entries are free is valid and does not affect the sentinel. This deliberately and correctly diverges from the native JITNotifications::SetAllNotifications which can orphan entries from other modules.

✅ WriteEntry Field Ordering — State Written Last

In JITNotification.WriteEntry (lines 57–62), fields are written in order: ClrModule, MethodToken, State. Since IsFree checks _state == 0, writing State last means a concurrent reader will see the entry as free until all fields are populated. This is the safest ordering for the live-process write scenario.

✅ Flag Conversion at COM Boundary (confirmed by all 3 models)

CodeNotificationFlagsConverter correctly uses explicit per-bit mapping with an IsValid check that rejects unrecognized flag bits. The CLRDataMethodCodeNotification enum is properly defined as a separate type from CodeNotificationKind, allowing independent evolution.

✅ Lazy Allocation Path

The lazy allocation in PrepareTable/AllocateTable correctly zero-fills the allocated table, writes the pointer back to the global, and propagates NotImplementedException when the target doesn't support allocation. The AllocateMemory callback is properly plumbed through all three initialization paths with proper QI for ICLRDataTarget2 and null-safe defaults.

✅ Native Side Changes

The JIT_NOTIFICATION_TABLE_SIZE constant properly replaces the magic number 1000/1001 in both util.hpp and daccess.cpp. The data descriptor entries for JITNotification correctly match the native struct layout. The CodeNotifications contract is properly registered as c1.

💡 AllocVirtualDelegate Size Type Mismatch

The AllocVirtualDelegate takes ulong size while Target.AllocateMemory takes uint size. The Init entrypoint correctly checks size > uint.MaxValue before casting. This is safe but the mismatch could confuse future maintainers.

Generated by Code Review for issue #127114 ·

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

3 participants