From 8072f96a260e9fff99af6be107eaf1699acb55e6 Mon Sep 17 00:00:00 2001 From: AzuLX Date: Mon, 5 Jan 2026 15:47:03 +0000 Subject: [PATCH 1/2] fix multi-thread breakpoint deletion race condition --- .../TitanEngine.Debugger.DebugLoop.cpp | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp b/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp index 51942cb..5ebfb09 100644 --- a/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp +++ b/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp @@ -589,11 +589,36 @@ __declspec(dllexport) void TITCALL DebugLoop() { if(DebugAttachedToProcess || !FirstBPX) //program generated a breakpoint exception { - DBGCode = DBG_EXCEPTION_NOT_HANDLED; - if(DBGCustomHandler->chBreakPoint != NULL) + ULONG_PTR exceptionAddress = (ULONG_PTR)DBGEvent.u.Exception.ExceptionRecord.ExceptionAddress; + unsigned char currentByte = 0xCC; + MemoryReadSafe(dbgProcessInformation.hProcess, (void*)exceptionAddress, ¤tByte, 1, nullptr); + + if(currentByte != 0xCC) { - myCustomHandler = (fCustomHandler)((LPVOID)DBGCustomHandler->chBreakPoint); - myCustomHandler(&DBGEvent.u.Exception.ExceptionRecord); + //breakpoint was deleted - the byte is no longer 0xCC + //reset IP to exception address and continue gracefully + DBGCode = DBG_CONTINUE; + hActiveThread = EngineOpenThread(THREAD_GETSETSUSPEND, false, DBGEvent.dwThreadId); + CONTEXT myDBGContext; + myDBGContext.ContextFlags = ContextControlFlags; + GetThreadContext(hActiveThread, &myDBGContext); +#if defined(_WIN64) + myDBGContext.Rip = exceptionAddress; +#else + myDBGContext.Eip = (DWORD)exceptionAddress; +#endif + SetThreadContext(hActiveThread, &myDBGContext); + EngineCloseHandle(hActiveThread); + } + else + { + //byte is still 0xCC - this is a real int3 in the original code!! + DBGCode = DBG_EXCEPTION_NOT_HANDLED; + if(DBGCustomHandler->chBreakPoint != NULL) + { + myCustomHandler = (fCustomHandler)((LPVOID)DBGCustomHandler->chBreakPoint); + myCustomHandler(&DBGEvent.u.Exception.ExceptionRecord); + } } } else //system breakpoint From 5cc80cf3d9810b77f37cda7ff436004b6310b746 Mon Sep 17 00:00:00 2001 From: AzuLX Date: Sat, 10 Jan 2026 12:36:40 +0000 Subject: [PATCH 2/2] track deleted breakpoints to handle stale events safely --- TitanEngine/Global.Breakpoints.cpp | 1 + TitanEngine/Global.Breakpoints.h | 2 + TitanEngine/Global.Debugger.cpp | 1 + TitanEngine/TitanEngine.Breakpoints.cpp | 1 + .../TitanEngine.Debugger.DebugLoop.cpp | 37 ++++++++++++------- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/TitanEngine/Global.Breakpoints.cpp b/TitanEngine/Global.Breakpoints.cpp index 2ceb7d0..a3db551 100644 --- a/TitanEngine/Global.Breakpoints.cpp +++ b/TitanEngine/Global.Breakpoints.cpp @@ -4,6 +4,7 @@ std::vector BreakPointBuffer; std::unordered_map MemoryBreakpointPages; +std::unordered_set recentlyDeletedBpx; ULONG_PTR dr7uint(DR7* dr7) { diff --git a/TitanEngine/Global.Breakpoints.h b/TitanEngine/Global.Breakpoints.h index b93d562..f0b090a 100644 --- a/TitanEngine/Global.Breakpoints.h +++ b/TitanEngine/Global.Breakpoints.h @@ -3,6 +3,7 @@ #include #include +#include #include "Global.Engine.Threading.h" #include "Global.Engine.h" @@ -11,6 +12,7 @@ extern std::vector BreakPointBuffer; extern std::unordered_map MemoryBreakpointPages; +extern std::unordered_set recentlyDeletedBpx; void uintdr7(ULONG_PTR dr7, DR7* ret); ULONG_PTR dr7uint(DR7* dr7); diff --git a/TitanEngine/Global.Debugger.cpp b/TitanEngine/Global.Debugger.cpp index 6d208c0..d50042b 100644 --- a/TitanEngine/Global.Debugger.cpp +++ b/TitanEngine/Global.Debugger.cpp @@ -89,6 +89,7 @@ void DebuggerReset() } std::vector().swap(BreakPointBuffer); std::unordered_map().swap(MemoryBreakpointPages); + recentlyDeletedBpx.clear(); } void ClearProcessList() diff --git a/TitanEngine/TitanEngine.Breakpoints.cpp b/TitanEngine/TitanEngine.Breakpoints.cpp index 92b25c0..e4465a8 100644 --- a/TitanEngine/TitanEngine.Breakpoints.cpp +++ b/TitanEngine/TitanEngine.Breakpoints.cpp @@ -303,6 +303,7 @@ __declspec(dllexport) bool TITCALL DeleteBPX(ULONG_PTR bpxAddress) FlushInstructionCache(dbgProcessInformation.hProcess, NULL, 0); VirtualProtectEx(dbgProcessInformation.hProcess, (LPVOID)bpxAddress, BreakPointBuffer.at(found).BreakPointSize, OldProtect, &OldProtect); BreakPointBuffer.erase(BreakPointBuffer.begin() + found); + recentlyDeletedBpx.insert(bpxAddress); return true; } diff --git a/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp b/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp index 5ebfb09..0b1af38 100644 --- a/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp +++ b/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp @@ -101,6 +101,8 @@ __declspec(dllexport) void TITCALL DebugLoop() memset(&DLLDebugFileName, 0, sizeof(DLLDebugFileName)); engineFileIsBeingDebugged = true; + uint32_t consecutiveTimeouts = 0; + while(!BreakDBG) //actual debug loop { bool synchronizedStep = false; @@ -124,10 +126,17 @@ __declspec(dllexport) void TITCALL DebugLoop() else { // Regular timeout, wait again + // After 2 consecutive timeouts, clear recently deleted breakpoints + consecutiveTimeouts++; + if(consecutiveTimeouts >= 2) + recentlyDeletedBpx.clear(); continue; } } + // Event received, reset timeout counter + consecutiveTimeouts = 0; + if(IsDbgReplyLaterSupported) { if(DBGEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) @@ -590,29 +599,29 @@ __declspec(dllexport) void TITCALL DebugLoop() if(DebugAttachedToProcess || !FirstBPX) //program generated a breakpoint exception { ULONG_PTR exceptionAddress = (ULONG_PTR)DBGEvent.u.Exception.ExceptionRecord.ExceptionAddress; - unsigned char currentByte = 0xCC; - MemoryReadSafe(dbgProcessInformation.hProcess, (void*)exceptionAddress, ¤tByte, 1, nullptr); - if(currentByte != 0xCC) + if(recentlyDeletedBpx.find(exceptionAddress) != recentlyDeletedBpx.end()) { - //breakpoint was deleted - the byte is no longer 0xCC - //reset IP to exception address and continue gracefully - DBGCode = DBG_CONTINUE; + //breakpoint was recently deleted - handle the stale event gracefully hActiveThread = EngineOpenThread(THREAD_GETSETSUSPEND, false, DBGEvent.dwThreadId); - CONTEXT myDBGContext; - myDBGContext.ContextFlags = ContextControlFlags; - GetThreadContext(hActiveThread, &myDBGContext); + if(hActiveThread != NULL) + { + CONTEXT myDBGContext; + myDBGContext.ContextFlags = ContextControlFlags; + GetThreadContext(hActiveThread, &myDBGContext); #if defined(_WIN64) - myDBGContext.Rip = exceptionAddress; + myDBGContext.Rip = exceptionAddress; #else - myDBGContext.Eip = (DWORD)exceptionAddress; + myDBGContext.Eip = (DWORD)exceptionAddress; #endif - SetThreadContext(hActiveThread, &myDBGContext); - EngineCloseHandle(hActiveThread); + SetThreadContext(hActiveThread, &myDBGContext); + EngineCloseHandle(hActiveThread); + DBGCode = DBG_CONTINUE; + } } else { - //byte is still 0xCC - this is a real int3 in the original code!! + //not a recently deleted breakpoint - pass to debuggee DBGCode = DBG_EXCEPTION_NOT_HANDLED; if(DBGCustomHandler->chBreakPoint != NULL) {