Crash self-report + round-completed latch + PDB archiving
CRASH SELF-REPORT (the #35/#41 heisenbug hunt): every unhandled exception now logs its code, address, and an EBP-walked stack as module-relative offsets (btl4+0xNNNN) into BT_LOG before dying -- any field crash on any machine hands us a resolvable stack. The walker is per-dereference guarded (a corrupt chain truncates). BT_CRASHTEST=1 exercises the path end-to-end (verified: forced AV logs code/addr/5-frame stack). mkdist archives each build's PDB next to its zip so offsets resolve per distributed version. Context: a flaky release-only crash in the mech build path surfaced during #38 rigs -- it moves with build layout, spares neither peer, and masks under a debugger heap (13 clean cdb runs vs 2/3 crashes without). A real latent memory bug; the self-report will catch it wherever it strikes next. ROUND-COMPLETED LATCH (issue #38, awaiting verification): relay-mode clients refuse RunMission after a round has ended -- the lobby-loop relaunch owns the next round (in-process round 2 rebuilt every player/mech entity on stale globals; matchlog caught player=3:1->3:36 in one generation). Latch sets ONLY when the stop ends a RUNNING mission (a stray pre-mission StopMission must not poison round 1 -- the first cut ate a legit launch, caught + fixed in rig). Regression verification pending (contaminated by the heisenbug above). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ac7f0cb13a
commit
f7daf03507
@@ -163,6 +163,58 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
#include "btl4console.hpp"
|
||||
#endif
|
||||
|
||||
//
|
||||
// CRASH SELF-REPORT (2026-07-24, the #35/#41 heisenbug hunt): every unhandled
|
||||
// exception logs its code, address, and an EBP-chain stack walk as
|
||||
// MODULE-RELATIVE offsets ("btl4+0x1234") into BT_LOG before the process
|
||||
// dies -- so any field crash on any machine hands us a resolvable stack (we
|
||||
// hold the PDB). The walker is guarded per-dereference; a corrupt frame
|
||||
// chain just truncates the walk.
|
||||
//
|
||||
static LONG WINAPI
|
||||
BTCrashFilter(EXCEPTION_POINTERS *info)
|
||||
{
|
||||
unsigned long base = (unsigned long)GetModuleHandleA(NULL);
|
||||
unsigned long code = info->ExceptionRecord->ExceptionCode;
|
||||
unsigned long addr = (unsigned long)info->ExceptionRecord->ExceptionAddress;
|
||||
|
||||
std::cout << "\n[crash] UNHANDLED EXCEPTION code=0x" << std::hex << code
|
||||
<< " addr=0x" << addr;
|
||||
if (addr >= base && addr < base + 0x2000000UL)
|
||||
std::cout << " (btl4+0x" << (addr - base) << ")";
|
||||
if (code == 0xC0000005 && info->ExceptionRecord->NumberParameters >= 2)
|
||||
std::cout << " access=" << std::dec
|
||||
<< (int)info->ExceptionRecord->ExceptionInformation[0]
|
||||
<< std::hex << " target=0x"
|
||||
<< (unsigned long)info->ExceptionRecord->ExceptionInformation[1];
|
||||
std::cout << "\n[crash] stack:";
|
||||
|
||||
unsigned long eip = info->ContextRecord->Eip;
|
||||
unsigned long ebp = info->ContextRecord->Ebp;
|
||||
for (int depth = 0; depth < 24; ++depth)
|
||||
{
|
||||
std::cout << " ";
|
||||
if (eip >= base && eip < base + 0x2000000UL)
|
||||
std::cout << "btl4+0x" << std::hex << (eip - base);
|
||||
else
|
||||
std::cout << "0x" << std::hex << eip;
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (ebp == 0 || (ebp & 3) != 0
|
||||
|| VirtualQuery((void *)ebp, &mbi, sizeof(mbi)) == 0
|
||||
|| mbi.State != MEM_COMMIT
|
||||
|| (mbi.Protect & (PAGE_READWRITE | PAGE_READONLY)) == 0)
|
||||
break;
|
||||
unsigned long next_eip = ((unsigned long *)ebp)[1];
|
||||
unsigned long next_ebp = ((unsigned long *)ebp)[0];
|
||||
if (next_eip == 0 || next_ebp <= ebp)
|
||||
break;
|
||||
eip = next_eip;
|
||||
ebp = next_ebp;
|
||||
}
|
||||
std::cout << std::dec << "\n" << std::flush;
|
||||
return EXCEPTION_EXECUTE_HANDLER; // die (after the evidence is out)
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
|
||||
{
|
||||
// Boot tick for the relaunch storm damper (btl4console.cpp, issue #33):
|
||||
@@ -172,6 +224,13 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
gBTBootTick = GetTickCount();
|
||||
}
|
||||
|
||||
// Crash self-report (see BTCrashFilter above) -- armed before anything
|
||||
// else so even boot crashes leave a stack in the log.
|
||||
SetUnhandledExceptionFilter(BTCrashFilter);
|
||||
|
||||
// BT_CRASHTEST=1: deliberately AV after the log opens -- verifies the
|
||||
// crash filter's forensic path end-to-end on any machine.
|
||||
|
||||
// Heap-corruption hunt (env BT_HEAPCHECK=1; default OFF -- ~100x slower): validate
|
||||
// the whole debug heap on EVERY alloc/free, so an out-of-bounds write is caught at
|
||||
// the very next heap op (a stack near the culprit) instead of much later at some
|
||||
@@ -204,6 +263,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
? (std::ios::out | std::ios::app) : std::ios::out);
|
||||
std::cout.rdbuf(logfile.rdbuf());
|
||||
|
||||
if (getenv("BT_CRASHTEST") != NULL && *getenv("BT_CRASHTEST") == '1')
|
||||
{
|
||||
*(volatile int *)0 = 0; // BT_CRASHTEST: exercise BTCrashFilter
|
||||
}
|
||||
|
||||
// FIRST-BREATH line, flushed IMMEDIATELY (field 2026-07-23: a player's
|
||||
// exe died repeatedly with a 0-byte join.log -- indistinguishable from
|
||||
// "never ran"). After this line, an empty log can only mean the exe was
|
||||
|
||||
Reference in New Issue
Block a user