Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/coreclr/vm/excep.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5491,6 +5491,46 @@ AdjustContextForJITHelpers(
if (IsIPInMarkedJitHelper(ip))
{
Thread::VirtualUnwindToFirstManagedCallFrame(pContext);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We have other helpers that have internal managed code between the user code and a write barrier. For example, BulkMoveWithWriteBarrier. What is the invariant that the debugger expects here?

This looks fragile to filter it here. Can this filtering be done in the higher-level debugger instead? The higher level debugger has a better idea what' a user code.

// After unwinding from the native write barrier to the first managed frame, check
// whether that frame is a managed helper that itself called the write barrier and
// keep unwinding until we reach user code.
//
// CastHelpers.StelemRef (CORINFO_HELP_ARRADDR_ST) and its callees StelemRef_Helper
// and StelemRef_Helper_NoCacheLookup all call RuntimeHelpers.WriteBarrier. The call
// chain can be up to 3 levels deep:
// user -> StelemRef -> StelemRef_Helper -> StelemRef_Helper_NoCacheLookup -> WriteBarrier
//
// Prior to the change that inlined CORINFO_HELP_ARRADDR_ST, StelemRef tail-called
// the write barrier so its frame was already destroyed and the unwind went directly
// to user code. With a regular call, the StelemRef family frames remain on the stack.
//
// We identify these helpers by their MethodTable (CastHelpers class), which is stable
// across tiered recompilation.
static MethodTable* s_pCastHelpersMT = nullptr;
while (true)
{
Comment on lines +5510 to +5512
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

static MethodTable* s_pCastHelpersMT is updated via an unsynchronized check-then-set. If multiple threads hit data breakpoints concurrently, this becomes a C++ data race. Consider removing the cache (data breakpoints are rare) or switching to an atomic/volatile pattern (e.g., VolatilePtr/VolatileLoad+VolatileStore or InterlockedCompareExchange).

Copilot uses AI. Check for mistakes.
PCODE currentIP = GetIP(pContext);
if (!ExecutionManager::IsManagedCode(currentIP))
break;

EECodeInfo codeInfo(currentIP);
if (!codeInfo.IsValid())
break;

MethodDesc* pMD = codeInfo.GetMethodDesc();
if (pMD == nullptr)
break;

if (s_pCastHelpersMT == nullptr)
s_pCastHelpersMT = CoreLibBinder::GetExistingClass(CLASS__CASTHELPERS);

Comment on lines +5526 to +5527
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

CoreLibBinder::GetExistingClass(CLASS__CASTHELPERS) will _ASSERTE(pMT != NULL) if CastHelpers hasn't been loaded yet (e.g., very early startup / debugger attaches early). Since this path can run on a data breakpoint, please switch to a non-asserting lookup (e.g., CoreLibBinder::GetClassIfExist(CLASS__CASTHELPERS) or g_pCastHelpers once initialized) and handle the null case by breaking out of the loop.

Suggested change
s_pCastHelpersMT = CoreLibBinder::GetExistingClass(CLASS__CASTHELPERS);
s_pCastHelpersMT = CoreLibBinder::GetClassIfExist(CLASS__CASTHELPERS);
if (s_pCastHelpersMT == nullptr)
break;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The local caching is useless. CoreLibBinder has similar cache already.

if (pMD->GetMethodTable() != s_pCastHelpersMT)
break;

Thread::VirtualUnwindCallFrame(pContext);
}

return TRUE;
}
#else
Expand Down
Loading