From 2fe58b8b8959ca01673c228cef7c175eac319884 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 12 Aug 2026 23:23:23 -0500 Subject: [PATCH] #170 root-caused + fixed: the #163 SteamAPI_Shutdown was the crash -- at round end every peer's close events are ALWAYS in flight, and Shutdown tears client state down while steamclient's own service thread dispatches them (the field stack: a steamclient worker calling literal NULL, zero btl4 frames, 7 hits / 3 machines / one module-relative stack). BTSteamNet_ShutdownAll is now a QUIESCE: steamActive gated FIRST, linger closes ('round over' not timeouts), one SNS drain, 60ms grace, NO SteamAPI_Shutdown -- and every exit path dies via TerminateProcess (menu-quit no longer returns through the CRT; it uses BTOrderlyDie like everything else, so the 883 detach deadlock stays dead too). Status callback guarded for the dying window. Crash filter now names foreign DLL frames (module+offset via VirtualQuery/GetModuleFileName) so the next foreign-thread crash arrives pre-symbolized. Exit contract: steam-networking.md --- context/open-questions.md | 5 ++++ context/steam-networking.md | 26 ++++++++++++++++++++ engine/MUNGA_L4/L4STEAMNET.cpp | 43 +++++++++++++++++++++++++++++---- game/btl4main.cpp | 44 ++++++++++++++++++++++++++-------- game/glass/btl4console.cpp | 6 ++--- 5 files changed, 106 insertions(+), 18 deletions(-) diff --git a/context/open-questions.md b/context/open-questions.md index e0671d5..3e891fe 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -1208,6 +1208,11 @@ consistent with internals-only, see #103). Logs: `scratchpad/night16/` + Oracle menu-relaunch boundary. Intermittent (10 relaunches -> 3 crashes in one log). [T4 hypothesis: callback left registered across teardown; possibly a #163/#156 teardown-ORDER regression.] Players report it as "crash while joining lobby" -- it is not the join. + **ROOT-CAUSED + FIXED same night [T2]:** the first #163 fix's `SteamAPI_Shutdown()` inside + `BTSteamNet_ShutdownAll` raced steamclient's service thread (round-end = a burst of peer + closes always in flight). ShutdownAll is now a QUIESCE (no Shutdown; linger closes; seam + gated first) and every exit path dies via TerminateProcess -- see [[steam-networking]] + §THE EXIT CONTRACT. Field verify Friday; the crash filter now names foreign DLL frames. - **#171 -- ballistic/missile hitscan era question (HOLD until group consensus).** Era witnesses (Oracle, Lynx, Sauron; Dplotta softer; Draco DISSENTS) recall AFC/Gauss as instant-hit, no lead, no flight animation, single-panel damage, no splash. 913 field observations: locked diff --git a/context/steam-networking.md b/context/steam-networking.md index add4fc4..081f1df 100644 --- a/context/steam-networking.md +++ b/context/steam-networking.md @@ -60,6 +60,32 @@ reset on death), #69 (Sunder V1 → Denkou naming). token / transport layer. Everything above the seam is the same arcade mesh — the three replication symptoms this night produced would all reproduce on the relay. +## THE EXIT CONTRACT (#163 -> #170, settled 2026-08-12) [T2 field-proven] +Process-exit topology for every glass/steam process: **flush -> quiesce -> TerminateProcess.** +`BTSteamNet_ShutdownAll` (L4STEAMNET.cpp) is a QUIESCE: gate `steamActive` FIRST (stops +marshal/game-thread seam re-entry), `CloseConnection(..., linger=true)` on every connection +("round over", not a peer-side timeout), close listeners, one `sockets->RunCallbacks()` drain, +`Sleep(60)` grace. **It deliberately never calls `SteamAPI_Shutdown()`, and no exit path may +return through the CRT** (menu-quit included -- it dies via `BTOrderlyDie`, btl4console.cpp). + +The two-failure history, because each fix's reason constrains the other: +- **883 (no teardown at all):** bare `ExitProcess` ran DLL_PROCESS_DETACH with live SNS threads + -> intermittent loader-lock deadlock -> "frozen view of how the game ended" + the menu child + hung in `SteamAPI_Init` against the wedged half-dead sibling (#163, night15). +- **913 (full `SteamAPI_Shutdown` before TerminateProcess):** at round end every peer closes + simultaneously, so status events are ALWAYS in flight; Shutdown tears client state down while + steamclient's own service thread dispatches them -> that thread calls literal NULL + (0xc0000005 access=8 target=0x0, BaseThreadInitThunk root, ZERO btl4 frames) in the window + before TerminateProcess (#170, night16: 7 hits / 3 machines / one module-relative stack). +- **The stable point:** TerminateProcess kills all threads atomically with every pointer still + valid (no detach, no dispatch window); the Steam client GCs the session on IPC pipe break -- + the standard crashed-game path (883 relaunched all night with no Shutdown ever called). The + child-hang needed a WEDGED sibling, which TerminateProcess makes structurally impossible. + +Forensics rider: the crash filter (btl4main.cpp `BTCrashFilter`) now prints foreign frames as +`+0xOFFSET` (VirtualQuery AllocationBase + GetModuleFileName), so a future +foreign-thread crash names its DLL instead of an unresolvable ASLR address. + ## Key Relationships - Rides: [[glass-cockpit]] miniconsole (marshal has a Steam-wire branch) · Extends: [[multiplayer]] (the arcade mesh, unchanged above the seam) diff --git a/engine/MUNGA_L4/L4STEAMNET.cpp b/engine/MUNGA_L4/L4STEAMNET.cpp index 578e61e..67790ca 100644 --- a/engine/MUNGA_L4/L4STEAMNET.cpp +++ b/engine/MUNGA_L4/L4STEAMNET.cpp @@ -155,6 +155,10 @@ static void static void __cdecl OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t *status) { + if (!steamActive) + { + return; // #170: the quiesce window -- the process is dying; + } // no accepts, no table writes, just survive the call ISteamNetworkingSockets *sockets = SteamNetworkingSockets(); switch (status->m_info.m_eState) @@ -634,7 +638,8 @@ unsigned long long } // -// #163 -- the ORDERED transport shutdown for process exit (2026-08-11). +// #163/#170 -- the ORDERED transport QUIESCE for process exit +// (2026-08-11, corrected 2026-08-12 -- see the #170 block at the tail). // // The port never called SteamAPI_Shutdown(): every round-end died via bare // ExitProcess with live SNS connections and Steam callback threads, and the @@ -655,6 +660,10 @@ void { return; } + steamActive = 0; // #170 FIRST: gate every seam entry + // (Pump/Send/Recv/Accept) against + // re-entry from the marshal or game + // thread while the table is torn down ISteamNetworkingSockets *sockets = SteamNetworkingSockets(); for (int i = 0; i < MaxConnections; ++i) { @@ -662,7 +671,9 @@ void && connections[i].connection != k_HSteamNetConnection_Invalid) { sockets->CloseConnection(connections[i].connection, 0, - "round over", false); + "round over", true); // #170 linger: let the close packet + // actually transmit -- peers should + // read "round over", not a timeout } connections[i].inUse = 0; } @@ -674,7 +685,29 @@ void listenSockets[p] = k_HSteamListenSocket_Invalid; } } - SteamAPI_RunCallbacks(); // let the closes flush client-side - steamActive = 0; - SteamAPI_Shutdown(); + sockets->RunCallbacks(); // one SNS drain; our status callback + // stays REGISTERED and this module + // stays LOADED, so any late dispatch + // lands in live (guarded) code + Sleep(60); // bounded grace for the lingered closes + // + // #170 -- DELIBERATELY NO SteamAPI_Shutdown(). The first #163 fix called + // it here, and that OVER-CORRECTED into tonight's field crash: at round + // end every peer closes simultaneously, so ClosedByPeer/ProblemDetected + // status events are ALWAYS in flight when this runs -- and Shutdown tears + // the client state down while steamclient's own service thread is still + // dispatching them. Field signature (night16, 7 hits / 3 machines): a + // steamclient-internal thread (BaseThreadInitThunk root, ZERO btl4 + // frames) calling literal NULL (0xc0000005 access=8 target=0x0) in the + // window between Shutdown and TerminateProcess. The contract now + // [T2 field-proven]: quiesce only; EVERY caller must die via + // TerminateProcess (never a CRT return -- that re-opens the DLL-detach + // deadlock this function no longer prevents). TerminateProcess kills + // all threads atomically with every pointer still valid; the Steam + // client GCs the session on IPC pipe break (the standard crashed-game + // path -- build 883 relaunched all night without a single Shutdown + // call). The 883 child-hang-in-SteamAPI_Init needed a WEDGED + // (detach-deadlocked, half-dead) sibling; TerminateProcess makes wedges + // structurally impossible, so the child boots clean without Shutdown. + // } diff --git a/game/btl4main.cpp b/game/btl4main.cpp index 7ddc1a8..6136192 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -216,7 +216,28 @@ static LONG WINAPI if (eip >= base && eip < base + 0x2000000UL) std::cout << "btl4+0x" << std::hex << (eip - base); else - std::cout << "0x" << std::hex << eip; + { + // + // #170: name the owning MODULE -- a foreign-DLL stack is + // unresolvable after the fact without its (ASLR'd) base; the + // steam-worker NULL-call needed this and didn't have it. + // + MEMORY_BASIC_INFORMATION fmbi; + char dll_path[MAX_PATH]; + if (VirtualQuery((void *)eip, &fmbi, sizeof(fmbi)) != 0 + && fmbi.AllocationBase != NULL + && GetModuleFileNameA((HMODULE)fmbi.AllocationBase, + dll_path, sizeof(dll_path)) != 0) + { + const char *dll_name = dll_path; + for (const char *s = dll_path; *s; ++s) + if (*s == '\\' || *s == '/') dll_name = s + 1; + std::cout << dll_name << "+0x" << std::hex + << (eip - (unsigned long)fmbi.AllocationBase); + } + else + std::cout << "0x" << std::hex << eip; + } MEMORY_BASIC_INFORMATION mbi; if (ebp == 0 || (ebp & 3) != 0 || VirtualQuery((void *)ebp, &mbi, sizeof(mbi)) == 0 @@ -1241,17 +1262,20 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone) { BTLastrunNote("clean exit: player quit from the menu"); -#ifdef BT_STEAM - // #163: the menu keeps the Steam transport up for the lobby; - // returning through the CRT would run DLL detach with live - // Steam threads -- the same intermittent deadlock the - // round-end relaunch hit. Tear it down deliberately first. + // + // #163/#170: the menu keeps the Steam transport up for the + // lobby, and a CRT return would run DLL detach with live + // Steam threads (the 883 wedge). ShutdownAll is now a + // QUIESCE -- it deliberately does NOT stop those threads + // (SteamAPI_Shutdown mid-dispatch was the #170 NULL-call + // crash) -- so this path must die exactly like every other + // exit: flush, quiesce, TerminateProcess. One topology. + // { - extern void BTSteamNet_ShutdownAll(); - BTSteamNet_ShutdownAll(); + extern void BTOrderlyDie(void); + BTOrderlyDie(); // never returns } -#endif - return 0; // quit from the menu + return 0; // unreachable (TerminateProcess) } char fe_arguments[192]; char fe_value[64]; diff --git a/game/glass/btl4console.cpp b/game/glass/btl4console.cpp index e5e189b..65bfe46 100644 --- a/game/glass/btl4console.cpp +++ b/game/glass/btl4console.cpp @@ -181,9 +181,9 @@ static BOOL CALLBACK return TRUE; } -static void - BTOrderlyDie(void) -{ +void + BTOrderlyDie(void) // #170: non-static -- the menu-quit +{ // path in btl4main dies through here { extern void BTMatchLogClose(); BTMatchLogClose(); // complete receipts on disk (the