Repository navigation
Unprotected OBJECTREF in CallFilterFunclet asserts when a profiler monitors exceptions #132462
Description
Description
When using a checked build of the CoreCLR runtime with a profiler attached that is monitoring exceptions we're seeing an assert hit in CallFilterFunclet in src/coreclr/vm/exceptionhandling.cpp.
Specifically: _ASSERTE(Thread::IsObjRefValid(&objref))
CallFilterFunclet holds the throwable in a raw unprotected native OBJECTREF local across ExtInfo::MakeCallbacksRelatedToHandler. When a profiler has exception monitoring enabled the call enters GCX_PREEMP causing the thread to transition between cooperative states while the local is live and unprotected from being moved by the GC.
cc @kg -- who helped me with the investigation.
Reproduction Steps
- Build a checked runtime of CoreCLR
- Hook up a profiler that has exception monitoring enabled
COR_PRF_MONITOR_EXCEPTIONS
I had claude write up a repro case that shows this as I originally hit this problem while working on the Unity runtime.
Repro.cs:
using System;
internal static class Repro
{
private static bool Filter(Exception e)
{
// The body is irrelevant - what matters is that a filter funclet is invoked at all.
return e.Message == "repro";
}
private static void Throw() => throw new InvalidOperationException("repro");
private static int Main()
{
Console.WriteLine("before");
for (int i = 0; i < 10; i++)
{
try
{
Throw();
}
catch (InvalidOperationException e) when (Filter(e))
{
}
}
Console.WriteLine("after - no assert");
return 100;
}
}
ReproProfiler.cpp
// Minimal ICorProfilerCallback whose only action is to set COR_PRF_MONITOR_EXCEPTIONS.
// It implements no exception callbacks at all - merely having the flag set is enough,
// because CORProfilerTrackExceptions() then returns TRUE and
// EEToProfilerExceptionInterfaceWrapper::ExceptionSearchFilterEnter enters GCX_PREEMP
// around the (empty) callback.
//
// Build from a VS x64 developer prompt, with <runtime> = your dotnet/runtime checkout:
//
// cl /nologo /LD /EHsc /Zi /std:c++17 ReproProfiler.cpp ^
// /I <runtime>\src\coreclr\pal\prebuilt\inc ^
// /I <runtime>\src\coreclr\inc ^
// /link /OUT:ReproProfiler.dll
#include <windows.h>
#include <unknwn.h>
#include <cstdio>
#include "cor.h"
#include "corprof.h"
// The interface IIDs come from __uuidof - corprof.h declares each interface with
// MIDL_INTERFACE, which expands to DECLSPEC_UUID. No hand-written IID tables needed.
// {5F1C4A9E-9C1B-4B2E-9D3A-7E6F0B2C4D81} - set CORECLR_PROFILER to this.
static const GUID ReproProfilerClsid =
{ 0x5F1C4A9E, 0x9C1B, 0x4B2E, { 0x9D, 0x3A, 0x7E, 0x6F, 0x0B, 0x2C, 0x4D, 0x81 } };
// ICorProfilerCallback2 is the minimum the runtime accepts - CoCreateProfiler
// QIs for it (see EEToProfInterfaceImpl::Init). v3..v11 are optional.
class ReproProfiler final : public ICorProfilerCallback2
{
LONG m_refCount = 1;
ICorProfilerInfo* m_info = nullptr;
public:
// ---- IUnknown --------------------------------------------------------
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
{
if (ppv == nullptr)
return E_POINTER;
if (riid == __uuidof(IUnknown) ||
riid == __uuidof(ICorProfilerCallback) ||
riid == __uuidof(ICorProfilerCallback2))
{
*ppv = static_cast<ICorProfilerCallback2*>(this);
AddRef();
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_refCount); }
ULONG STDMETHODCALLTYPE Release() override
{
ULONG remaining = InterlockedDecrement(&m_refCount);
if (remaining == 0)
delete this;
return remaining;
}
// ---- The only two callbacks we actually implement ---------------------
HRESULT STDMETHODCALLTYPE Initialize(IUnknown* pICorProfilerInfoUnk) override
{
HRESULT hr = pICorProfilerInfoUnk->QueryInterface(__uuidof(ICorProfilerInfo), (void**)&m_info);
if (FAILED(hr))
{
printf("[repro-profiler] QueryInterface(ICorProfilerInfo) failed hr=0x%08x\n", hr);
fflush(stdout);
return hr;
}
hr = m_info->SetEventMask(COR_PRF_MONITOR_EXCEPTIONS);
printf("[repro-profiler] SetEventMask(COR_PRF_MONITOR_EXCEPTIONS) hr=0x%08x\n", hr);
fflush(stdout);
return hr;
}
HRESULT STDMETHODCALLTYPE Shutdown() override
{
printf("[repro-profiler] Shutdown\n");
fflush(stdout);
if (m_info != nullptr)
{
m_info->Release();
m_info = nullptr;
}
return S_OK;
}
// ---- Everything else is a no-op stub ----------------------------------
HRESULT STDMETHODCALLTYPE AppDomainCreationStarted(AppDomainID appDomainId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE AppDomainCreationFinished(AppDomainID appDomainId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE AppDomainShutdownStarted(AppDomainID appDomainId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE AppDomainShutdownFinished(AppDomainID appDomainId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE AssemblyLoadStarted(AssemblyID assemblyId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE AssemblyLoadFinished(AssemblyID assemblyId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE AssemblyUnloadStarted(AssemblyID assemblyId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE AssemblyUnloadFinished(AssemblyID assemblyId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ModuleLoadStarted(ModuleID moduleId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ModuleLoadFinished(ModuleID moduleId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ModuleUnloadStarted(ModuleID moduleId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ModuleUnloadFinished(ModuleID moduleId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ModuleAttachedToAssembly(ModuleID moduleId, AssemblyID AssemblyId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ClassLoadStarted(ClassID classId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ClassLoadFinished(ClassID classId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ClassUnloadStarted(ClassID classId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ClassUnloadFinished(ClassID classId, HRESULT hrStatus) override { return S_OK; }
HRESULT STDMETHODCALLTYPE FunctionUnloadStarted(FunctionID functionId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE JITCompilationStarted(FunctionID functionId, BOOL fIsSafeToBlock) override { return S_OK; }
HRESULT STDMETHODCALLTYPE JITCompilationFinished(FunctionID functionId, HRESULT hrStatus, BOOL fIsSafeToBlock) override { return S_OK; }
HRESULT STDMETHODCALLTYPE JITCachedFunctionSearchStarted(FunctionID functionId, BOOL *pbUseCachedFunction) override { return S_OK; }
HRESULT STDMETHODCALLTYPE JITCachedFunctionSearchFinished(FunctionID functionId, COR_PRF_JIT_CACHE result) override { return S_OK; }
HRESULT STDMETHODCALLTYPE JITFunctionPitched(FunctionID functionId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE JITInlining(FunctionID callerId, FunctionID calleeId, BOOL *pfShouldInline) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ThreadCreated(ThreadID threadId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ThreadDestroyed(ThreadID threadId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ThreadAssignedToOSThread(ThreadID managedThreadId, DWORD osThreadId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingClientInvocationStarted() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingClientSendingMessage(GUID *pCookie, BOOL fIsAsync) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingClientReceivingReply(GUID *pCookie, BOOL fIsAsync) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingClientInvocationFinished() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingServerReceivingMessage(GUID *pCookie, BOOL fIsAsync) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingServerInvocationStarted() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingServerInvocationReturned() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RemotingServerSendingReply(GUID *pCookie, BOOL fIsAsync) override { return S_OK; }
HRESULT STDMETHODCALLTYPE UnmanagedToManagedTransition(FunctionID functionId, COR_PRF_TRANSITION_REASON reason) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ManagedToUnmanagedTransition(FunctionID functionId, COR_PRF_TRANSITION_REASON reason) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RuntimeSuspendStarted(COR_PRF_SUSPEND_REASON suspendReason) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RuntimeSuspendFinished() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RuntimeSuspendAborted() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RuntimeResumeStarted() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RuntimeResumeFinished() override { return S_OK; }
HRESULT STDMETHODCALLTYPE RuntimeThreadSuspended(ThreadID threadId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RuntimeThreadResumed(ThreadID threadId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE MovedReferences(ULONG cMovedObjectIDRanges, ObjectID oldObjectIDRangeStart[ ], ObjectID newObjectIDRangeStart[ ], ULONG cObjectIDRangeLength[ ]) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ObjectAllocated(ObjectID objectId, ClassID classId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ObjectsAllocatedByClass(ULONG cClassCount, ClassID classIds[ ], ULONG cObjects[ ]) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ObjectReferences(ObjectID objectId, ClassID classId, ULONG cObjectRefs, ObjectID objectRefIds[ ]) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RootReferences(ULONG cRootRefs, ObjectID rootRefIds[ ]) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionThrown(ObjectID thrownObjectId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionSearchFunctionEnter(FunctionID functionId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionSearchFunctionLeave() override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionSearchFilterEnter(FunctionID functionId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionSearchFilterLeave() override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionSearchCatcherFound(FunctionID functionId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionOSHandlerEnter(UINT_PTR __unused) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionOSHandlerLeave(UINT_PTR __unused) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionUnwindFunctionEnter(FunctionID functionId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionUnwindFunctionLeave() override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionUnwindFinallyEnter(FunctionID functionId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionUnwindFinallyLeave() override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionCatcherEnter(FunctionID functionId, ObjectID objectId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionCatcherLeave() override { return S_OK; }
HRESULT STDMETHODCALLTYPE COMClassicVTableCreated(ClassID wrappedClassId, REFGUID implementedIID, void *pVTable, ULONG cSlots) override { return S_OK; }
HRESULT STDMETHODCALLTYPE COMClassicVTableDestroyed(ClassID wrappedClassId, REFGUID implementedIID, void *pVTable) override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionCLRCatcherFound() override { return S_OK; }
HRESULT STDMETHODCALLTYPE ExceptionCLRCatcherExecute() override { return S_OK; }
// ---- ICorProfilerCallback2 --------------------------------------------
HRESULT STDMETHODCALLTYPE ThreadNameChanged(ThreadID threadId, ULONG cchName, WCHAR name[ ]) override { return S_OK; }
HRESULT STDMETHODCALLTYPE GarbageCollectionStarted(int cGenerations, BOOL generationCollected[ ], COR_PRF_GC_REASON reason) override { return S_OK; }
HRESULT STDMETHODCALLTYPE SurvivingReferences(ULONG cSurvivingObjectIDRanges, ObjectID objectIDRangeStart[ ], ULONG cObjectIDRangeLength[ ]) override { return S_OK; }
HRESULT STDMETHODCALLTYPE GarbageCollectionFinished() override { return S_OK; }
HRESULT STDMETHODCALLTYPE FinalizeableObjectQueued(DWORD finalizerFlags, ObjectID objectID) override { return S_OK; }
HRESULT STDMETHODCALLTYPE RootReferences2(ULONG cRootRefs, ObjectID rootRefIds[ ], COR_PRF_GC_ROOT_KIND rootKinds[ ], COR_PRF_GC_ROOT_FLAGS rootFlags[ ], UINT_PTR rootIds[ ]) override { return S_OK; }
HRESULT STDMETHODCALLTYPE HandleCreated(GCHandleID handleId, ObjectID initialObjectId) override { return S_OK; }
HRESULT STDMETHODCALLTYPE HandleDestroyed(GCHandleID handleId) override { return S_OK; }
};
// ---- Class factory -------------------------------------------------------
class ReproClassFactory final : public IClassFactory
{
LONG m_refCount = 1;
public:
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
{
if (ppv == nullptr)
return E_POINTER;
if (riid == __uuidof(IUnknown) || riid == __uuidof(IClassFactory))
{
*ppv = static_cast<IClassFactory*>(this);
AddRef();
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_refCount); }
ULONG STDMETHODCALLTYPE Release() override
{
ULONG remaining = InterlockedDecrement(&m_refCount);
if (remaining == 0)
delete this;
return remaining;
}
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv) override
{
if (pUnkOuter != nullptr)
return CLASS_E_NOAGGREGATION;
ReproProfiler* profiler = new ReproProfiler();
HRESULT hr = profiler->QueryInterface(riid, ppv);
profiler->Release();
return hr;
}
HRESULT STDMETHODCALLTYPE LockServer(BOOL) override { return S_OK; }
};
// Exported via ReproProfiler.def - combaseapi.h already declares these, so a
// __declspec(dllexport) here would be a linkage mismatch.
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
if (ppv == nullptr)
return E_POINTER;
if (rclsid != ReproProfilerClsid)
{
*ppv = nullptr;
return CLASS_E_CLASSNOTAVAILABLE;
}
ReproClassFactory* factory = new ReproClassFactory();
HRESULT hr = factory->QueryInterface(riid, ppv);
factory->Release();
return hr;
}
STDAPI DllCanUnloadNow()
{
return S_FALSE;
}
BOOL WINAPI DllMain(HMODULE, DWORD, LPVOID) { return TRUE; }
Expected behavior
No assert fired.
Actual behavior
Assert fires due to GC hole.
Regression?
No response
Known Workarounds
I've fixed this on our side by GCPROTECT'ing the OBJECTREF throwable in CallFilterFunclet and CallCatchFunclet
CallFilterFunclet
ExInfo* pExInfo = (ExInfo*)pThread->GetExceptionState()->GetCurrentExceptionTracker();
OBJECTREF throwable = exceptionObj.Get();
+
+ GCPROTECT_BEGIN(throwable);
+
throwable = PossiblyUnwrapThrowable(throwable, pExInfo->m_frameIter.m_crawl.GetAssembly());
// Profiler, debugger and ETW events
pExInfo->MakeCallbacksRelatedToHandler(false, pThread, pMD, &pExInfo->m_CurrentClause, (DWORD_PTR)pFilterIP, spForDebugger);
+
+ GCPROTECT_END();
END_QCALL;
CallCatchFunclet
_ASSERTE(exInfo->m_sfCallerOfActualHandlerFrame == GetSP(pvRegDisplay->pCallerContext));
#endif
+ GCPROTECT_BEGIN(throwable);
+
throwable = PossiblyUnwrapThrowable(throwable, exInfo->m_frameIter.m_crawl.GetAssembly());
// Profiler, debugger and ETW events
exInfo->MakeCallbacksRelatedToHandler(false, pThread, pMD, &exInfo->m_ClauseForCatch, (DWORD_PTR)pHandlerIP, spForDebugger);
+
+ GCPROTECT_END();
+
SetIP(pvRegDisplay->pCurrentContext, dwResumePC);
Configuration
No response
Other information
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Aug 18, 2026
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Aug 18, 2026
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.
[CoreCLR] Protect exception object across funclet callbacks (#132482)