From f86b604015a09dfa1fcc7a94d85aa2e6da21c683 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 14 Aug 2026 11:22:06 -0500 Subject: [PATCH] A fried pod stops being anybody's vehicle Caught by the podium rig on the ninth launch, under full page heap, after eight clean ones - the first free named by the debugger rather than inferred, which is what that rig exists for. Player::playerVehicle is a bare Entity* that nothing ever cleared. When a vehicle is destroyed it goes on death row, and FryDeathRowTask deletes one entity per frame from it. The Player that was driving it keeps pointing at the freed block. A replicant Player runs GoToVehicle EVERY frame - that is its whole performance, reading playerVehicle->localOrigin to follow its pod around the map - so from the moment the vehicle was fried, every frame read freed memory. It normally gets away with it, because the block is still mapped and still holds plausible-looking garbage. That is its own bug hiding inside this one: a remote pod quietly following coordinates that are no longer anything. Under page heap the block is decommitted instead of merely stale, so it faults at once: ExceptionAddress: rpl4opt!Quaternion::operator= [inlined in Player::GoToVehicle+0xe] reading 31e08aea - a freed allocation whose free stack is operator delete <- VTV::`scalar deleting destructor' <- FryDeathRowTask::Execute while `this` was a live, busy RPPlayer built from a network packet (RPPlayer::Make <- Registry::MakeEntityMessageHandler <- InterestManager::NewDynamicEntityHandler) - a REMOTE player, exactly the one whose performance is GoToVehicle. So the vehicle stops being anybody's vehicle as it is condemned. Done there rather than in ~Entity on purpose: at condemn time the entity is still whole and every group is still standing, and it happens exactly once - a destructor doing this would also run during Shutdown, when the Players group may already be gone. NOT yet verified as fixed: the fault appeared once in nine launches, so showing it gone needs a run of twenty-odd under page heap, which needs the elevated gflags step. The trap log and the tally are kept in playtestlogs\podium-trap-2026-08-14 (gitignored), and the 752 MB dump is at %TEMP%\rp412-podium-repro\podB\. Whether this is ALSO the original podium teardown crash is untested. It fits - a VTV freed while others still hold it would be deleted a second time at Shutdown, giving ~JointedMover a segment table full of reused memory, which is the vtable-dword-holding-a-small-float signature those dumps had - but that is a hypothesis, and the trapped stack is not that stack. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA/ENTITY.cpp | 39 +++++++++++++++++++++++++++++++++++++++ MUNGA/PLAYER.h | 10 ++++++++++ 2 files changed, 49 insertions(+) diff --git a/MUNGA/ENTITY.cpp b/MUNGA/ENTITY.cpp index f74bbc4..26bf7f5 100644 --- a/MUNGA/ENTITY.cpp +++ b/MUNGA/ENTITY.cpp @@ -1483,6 +1483,45 @@ void // if (!IsCondemned()) { + // + //--------------------------------------------------------------- + // Stop being anybody's vehicle before going on the row. + // + // Player::playerVehicle is a bare pointer that nothing ever + // cleared. The task that empties this row deletes one entity per + // frame, and a replicant Player runs GoToVehicle EVERY frame - + // it reads playerVehicle->localOrigin to follow its pod around. + // So from the moment the vehicle was fried, every one of those + // frames read freed memory. It normally survives on the garbage + // still lying in the block, which is its own kind of bug: a + // remote pod following coordinates that are no longer anything. + // + // Caught by the podium rig under full page heap, where the block + // is decommitted instead of merely stale: an access violation in + // Player::GoToVehicle reading a VTV whose free stack was + // FryDeathRowTask::Execute -> VTV::`scalar deleting destructor'. + // + // Cleared here rather than in ~Entity: the entity is still whole + // at this point, the groups are all still standing, and it + // happens exactly once. A destructor doing this would run during + // Shutdown too, when the Players group may already be gone. + //--------------------------------------------------------------- + // + EntityGroup *player_group = entity_manager->FindGroup("Players"); + if (player_group != NULL) + { + ChainIteratorOf iterator(player_group->groupMembers); + Player *player; + + while ((player = (Player*) iterator.ReadAndNext()) != NULL) + { + if (player->GetPlayerVehicle() == this) + { + player->ClearPlayerVehicle(); + } + } + } + entity_manager->deathRow->Add(this); SetCondemnedFlag(); NeverExecute(); diff --git a/MUNGA/PLAYER.h b/MUNGA/PLAYER.h index 1794ce7..7f678a8 100644 --- a/MUNGA/PLAYER.h +++ b/MUNGA/PLAYER.h @@ -331,6 +331,16 @@ public: playerVehicle = player_vehicle; } + // + // The vehicle is going away. SetPlayerVehicle cannot say this - it + // Checks its argument, so it will not take NULL - and every reader + // of playerVehicle tests it for NULL first, so this is all they need + // to stop following a pod that no longer exists. + // + void + ClearPlayerVehicle() + {Check(this); playerVehicle = NULL;} + Mission* GetMission() {