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) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-27 00:03:31 -05:00
co-authored by Claude Opus 5
parent c7dcdf26eb
commit e28dcbc161
2 changed files with 55 additions and 1 deletions
+31
View File
@@ -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; ii<player_count; ++ii)
{
+24 -1
View File
@@ -643,8 +643,31 @@ Reservoir::Reservoir(
if ((owner->simulationFlags & 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.