From e28dcbc1610dc0196fde7149cfa4d952b8e51cba Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 27 Jul 2026 00:03:31 -0500 Subject: [PATCH] the #35-class sweep + the games-night load crash: four lifecycle-window null-derefs guarded THE CLASS (established by Gitea #35's 515 fix, confirmed growing tonight): code the binary could run bare because a pod's mission world always existed, crashing in the port's MP join/teardown/respawn windows where mission, player, or a roster link is briefly NULL. Swept every GetMissionPlayer()/ GetPlayerVehicle()/GetCurrentMission() chain in the tree (23+7 sites). GUARDED (the 515 pattern -- authentic behavior whenever the object exists, skip/degrade + loud log when mid-teardown): 1. DPLRenderer::SortAndReloadNameBitmaps -- runs in the round-STOP path (the end-of-round circle), raw mission+player+entity-manager derefs. 2. DPLRenderer::LoadOrdinalBitmaps -- called from (1); raw GetMissionPlayer()->GetInstance() evaluated on EVERY machine (the camera- director test derefs before it branches). 3. DPLRenderer::LoadNameBitmaps -- same window, raw GetPlayerCount() chain. 4. Reservoir::Reservoir master-gate block -- THE CAPTURED GAMES-NIGHT CRASH ([crash] btl4+0x4990d = this ctor inlined into CreateReservoirSubsystem, AV reading NULL+0x1d0): linkedSinks.Resolve() derefed raw; +0x1d0 is exactly the masterScale FILD of the resolved master heat-sink bank (heatSinkCount @0x1D0 -- the offset the crash named). Observed trigger: a DUPLICATE-PILOT egg (the seat-ghost bug put one identity in two seats) poisoning the ID registry, so Resolve() nulled during viewpoint-entity construction and at least two pods died loading the same egg. Now: loud '[spawn] FATAL-AVOIDED' + degrade to the inactive-copy shape. The duplicate-pilot ROOT CAUSE is the seat-ghost fix (tracked separately); this guard makes the failure survivable and self-identifying either way. CLASSIFIED SAFE, not touched: btl4mppr/btplayer/mechmppr/btl4gau3 chains (guarded or null-tolerant by design); APP.cpp's five Check(player) sites (mission state machine -- player exists by construction, original engine ordering); CAMMGR/CULTURAL GetCurrentMission chains (construction-time). VERIFIED: build clean (0 errors, no new unresolved externs); solo smoke on the guarded exe -- zero FATAL-AVOIDED lines (guards silent on the healthy path), subsystems simulating, no faults. STILL OWED: a full launch->stop rig cycle (exercises the guarded round-stop path live) and the duplicate-pilot-egg repro -- both blocked on port 1500 while the operator's console session is up. Co-Authored-By: Claude Opus 5 (1M context) --- engine/MUNGA_L4/L4VIDEO.cpp | 31 +++++++++++++++++++++++ game/reconstructed/heatfamily_reslice.cpp | 25 +++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 43ddfb9..01a8657 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -9963,6 +9963,22 @@ void void DPLRenderer::SortAndReloadNameBitmaps() { + // + // NULL-GUARD (2026-07-26, the #35-class sweep). This resort runs in the + // round-STOP path (the end-of-round "circle") and derefs the current + // mission and the mission player raw -- the binary could, because a pod's + // mission world always existed when it ran. The port's MP stop/teardown + // windows (double-stop, abort-during-stop) can leave either briefly NULL + // -- the exact shape of the 515 audio-clip crash. The refresh is + // cosmetic; skip it when the world is mid-teardown. + // + if (application == 0 + || application->GetCurrentMission() == 0 + || application->GetMissionPlayer() == 0 + || application->GetEntityManager() == 0) + { + return; + } // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Get the Entity Group of Players @@ -9993,6 +10009,15 @@ void DPLRenderer::SortAndReloadNameBitmaps() // void DPLRenderer::LoadOrdinalBitmaps() { + // NULL-GUARD (2026-07-26, the #35-class sweep): runs from the round-stop + // resort above; player/mission can be mid-teardown NULL in the port's MP + // windows. Ordinals are the camera-director's cosmetic overlay -- skip. + if (application == 0 + || application->GetMissionPlayer() == 0 + || application->GetCurrentMission() == 0) + { + return; + } // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Check if we have a Director on this machine, @@ -10062,6 +10087,12 @@ unsigned int* // void DPLRenderer::LoadNameBitmaps() { + // NULL-GUARD (2026-07-26, the #35-class sweep): same teardown window as + // SortAndReloadNameBitmaps above. + if (application == 0 || application->GetCurrentMission() == 0) + { + return; + } int player_count = application->GetCurrentMission()->GetPlayerCount(); for (int ii=0; iisimulationFlags & SegmentCopyMask) == 0 && (owner->simulationFlags & MasterHeatSinkFlag) != 0) { - SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78 + // + // NULL-GUARD (2026-07-27, the games-night load crash: [crash] + // btl4+0x4990d = this ctor inlined into CreateReservoirSubsystem, + // AV reading NULL+0x1d0 = the masterScale FILD below with link==0). + // The binary derefs the resolved master bank raw -- in a pod the + // linked AggregateHeatSink always resolves. In the port's MP a + // poisoned roster (observed trigger: a DUPLICATE-pilot egg from the + // seat-ghost bug -- one identity in two seats -- corrupting the ID + // registry) can make Resolve() return NULL during viewpoint-entity + // construction, and every pod loading that egg dies. Fail LOUD and + // degrade to the inactive-copy shape (the else branch) instead: + // a mech with an unscaled reservoir beats six crashed clients. + // HeatSink *link = (HeatSink *)linkedSinks.Resolve(); // FUN_00417ab4(this+0x59) + if (link == 0) + { + DEBUG_STREAM << "[spawn] FATAL-AVOIDED: Reservoir(subsystem " + << subsystem_ID << ") master heat-sink bank did NOT resolve " + << "-- poisoned roster? (duplicate pilot / ID collision). " + << "Reservoir runs UNSCALED as an inactive copy.\n" << std::flush; + simulationFlags |= 2; // the else-branch shape + Check_Fpu(); + return; + } + SetPerformance(&Reservoir::CoolantSimulation); // this[7..9] = PTR 0050e6b4 -> @4aef78 link->Attach(this); // (**(link+0x1d8+4))(link+0x1d8, this) // Gitea #7 decode correction: the binary loads `*(int *)(link+0x1d0)` // and FILDs it -- `(float10)*(int *)(iVar1 + 0x1d0)` @4af408 -- i.e.