From 184fca92bd463d81da44e4bcc86206795b61634b Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Mon, 20 Apr 2026 16:45:54 -0400 Subject: [PATCH 1/3] Fix x64 data breakpoint handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/excep.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 49b323075cda1b..7b56b97efc2dbb 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -5491,6 +5491,46 @@ AdjustContextForJITHelpers( if (IsIPInMarkedJitHelper(ip)) { Thread::VirtualUnwindToFirstManagedCallFrame(pContext); + + // 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) + { + 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); + + if (pMD->GetMethodTable() != s_pCastHelpersMT) + break; + + Thread::VirtualUnwindCallFrame(pContext); + } + return TRUE; } #else From f1c59c6e749a82cf949aa30a812029a2b59aa001 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Tue, 21 Apr 2026 20:41:33 -0400 Subject: [PATCH 2/3] Revert "Fix x64 data breakpoint handling" This reverts commit 184fca92bd463d81da44e4bcc86206795b61634b. --- src/coreclr/vm/excep.cpp | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 7b56b97efc2dbb..49b323075cda1b 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -5491,46 +5491,6 @@ AdjustContextForJITHelpers( if (IsIPInMarkedJitHelper(ip)) { Thread::VirtualUnwindToFirstManagedCallFrame(pContext); - - // 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) - { - 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); - - if (pMD->GetMethodTable() != s_pCastHelpersMT) - break; - - Thread::VirtualUnwindCallFrame(pContext); - } - return TRUE; } #else From 7e9e5c0ae18129ffa16b27134fd538f766e1fd9c Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Wed, 22 Apr 2026 13:59:14 -0400 Subject: [PATCH 3/3] Fix data breakpoint frame pointer mismatch TriggerDataBreakpoint plants a deferred native patch when a data breakpoint fires inside a write barrier. The patch was created with GetFP() (the base pointer register), but MatchPatch computes frame pointers via GetRegdisplayStackMark which uses SP on AMD64 and PCTAddr on x86. This mismatch caused MatchPatch to reject the patch, so the deferred data breakpoint never fired. Fix by using LEAF_MOST_FRAME to bypass the frame pointer check in MatchPatch. This is safe because the patch is thread-bound (MatchPatch validates the thread) and is a one-shot patch, so the recursive false-match scenario the frame check guards against does not apply. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/ee/controller.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/debug/ee/controller.cpp b/src/coreclr/debug/ee/controller.cpp index 4c7fb46c96971a..55f9fcb4601755 100644 --- a/src/coreclr/debug/ee/controller.cpp +++ b/src/coreclr/debug/ee/controller.cpp @@ -9709,7 +9709,8 @@ bool DebuggerContinuableExceptionBreakpoint::SendEvent(Thread *thread, bool fIpC { LOG((LF_CORDB, LL_INFO10000, "D::DDBP: HIT DATA BREAKPOINT INSIDE WRITE BARRIER...\n")); DebuggerDataBreakpoint *pDataBreakpoint = new (interopsafe) DebuggerDataBreakpoint(thread); - pDataBreakpoint->AddAndActivateNativePatchForAddress((CORDB_ADDRESS_TYPE*)GetIP(&contextToAdjust), FramePointer::MakeFramePointer(GetFP(&contextToAdjust)), true, DPT_DEFAULT_TRACE_TYPE); + // Use LEAF_MOST_FRAME to bypass the frame pointer check in MatchPatch. + pDataBreakpoint->AddAndActivateNativePatchForAddress((CORDB_ADDRESS_TYPE*)GetIP(&contextToAdjust), LEAF_MOST_FRAME, true, DPT_DEFAULT_TRACE_TYPE); } else {