Skip to content

ETWReloggerTraceEventSource: access violation in tdh!CEventBase::Release on finalizer thread after Dispose #2411

@kartikkukreja

Description

@kartikkukreja

Summary

After disposing an ETWReloggerTraceEventSource, the process intermittently crashes with an access violation in tdh!CEventBase::Release on the .NET finalizer thread. The crash is caused by the GC's RCW (Runtime Callable Wrapper) cleanup attempting to Release() a COM ITraceEvent object that has already been freed.

TraceEvent version

Microsoft.Diagnostics.Tracing.TraceEvent 3.2.2

Environment

  • Windows 11 (26100), x64
  • .NET 10

Reproduction

The crash is intermittent under normal conditions but becomes 100% reproducible when GFlags full page heap is enabled for the host process (gflags /p /enable MyApp.exe /full). Page heap turns the silent heap corruption into an immediate access violation at the point of the invalid memory access.

Minimal reproduction pattern:

// Run this repeatedly — crashes intermittently (deterministically with page heap)
for (int i = 0; i < 50; i++)
{
    using var relogger = new ETWReloggerTraceEventSource(inputEtlPath, outputEtlPath);
    relogger.Dynamic.All += ev =>
    {
        if (ShouldKeep(ev))
            relogger.WriteEvent(ev);
    };
    relogger.Process();
    // After the using block, the relogger is disposed.
    // At some later GC cycle, the finalizer thread crashes in tdh!CEventBase::Release.
}

Crash analysis

Full crash dump analysis via !analyze -v:

FAILURE_BUCKET_ID:  INVALID_POINTER_READ_AVRF_c0000005_tdh.dll!CEventBase::Release
SYMBOL_NAME:        tdh!CEventBase::Release+21
EXCEPTION_CODE:     c0000005 (Access violation)

Stack trace (from the .NET finalizer thread):

ntdll!ExpInterlockedPushEntrySList+0xd     ← read from freed memory
tdh!CEventBase::Release+0x21              ← IUnknown::Release on freed ITraceEvent COM object
coreclr!RCW::ReleaseAllInterfaces+0x96    ← .NET RCW cleanup
coreclr!RCW::ReleaseAllInterfacesCallBack+0x58
coreclr!RCW::Cleanup+0x57
coreclr!RCWCleanupList::ReleaseRCWListRaw+0x16
coreclr!RCWCleanupList::ReleaseRCWListInCorrectCtx+0x76
coreclr!RCWCleanupList::CleanupAllWrappers+0xdf
coreclr!SyncBlockCache::CleanupSyncBlocks+0xf6
coreclr!DoExtraWorkForFinalizer+0x35
coreclr!FinalizerThread::FinalizerThreadWorker+0x134

Analysis

ETWReloggerTraceEventSource uses COM interop (CTraceRelogger, ITraceEvent) to wrap the Windows ITraceRelogger API. The library already does the right thing in several places:

  • ReloggerCallbacks.OnEvent calls Marshal.FinalReleaseComObject(source.m_curITraceEvent) after each event callback to eagerly release ITraceEvent COM objects (source)
  • Dispose(true) calls Marshal.FinalReleaseComObject(m_relogger) on the relogger itself (source)

However, the crash indicates that some ITraceEvent RCW objects survive past disposal and are later collected by the GC. When the finalizer thread runs RCW::ReleaseAllInterfaces, it calls Release() on the underlying tdh!CEventBase COM object, which has already been freed.

Possible escape paths for ITraceEvent RCWs that bypass the FinalReleaseComObject cleanup:

  1. ITraceEvent.Clone() at line 127 — creates a new COM object that is passed to Inject but may not be explicitly released
  2. m_relogger.CreateEventInstance() at line 148 and line 327 — creates new COM objects that may not be tracked
  3. Exception paths in OnEvent — if an exception occurs before FinalReleaseComObject is reached, m_curITraceEvent retains a reference that becomes a dangling RCW after the relogger is disposed

Workaround

Adding GC.Collect() + GC.WaitForPendingFinalizers() immediately after disposing the ETWReloggerTraceEventSource forces the RCW cleanup to happen in a controlled state. This appears to resolve the crash in our testing (5/5 iterations pass with page heap, vs 0/5 before the fix), though it is not a proper fix.

using var relogger = new ETWReloggerTraceEventSource(inputEtlPath, outputEtlPath);
relogger.Dynamic.All += ev => { /* ... */ };
relogger.Process();

// Workaround: flush finalizer queue to clean up orphaned ITraceEvent RCWs
// before the underlying COM objects become invalid
GC.Collect();
GC.WaitForPendingFinalizers();

Suggested fix

Ensure all ITraceEvent COM objects created during the relogger's lifetime are released via Marshal.FinalReleaseComObject before Dispose returns. Specifically:

  1. Release the return value of ITraceEvent.Clone() and CreateEventInstance() after Inject calls
  2. Wrap the OnEvent callback body in a try/finally to guarantee FinalReleaseComObject runs even if the user callback throws
  3. Consider calling GC.Collect + GC.WaitForPendingFinalizers at the end of Dispose(true) as a safety net

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions