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) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-14 11:22:06 -05:00
co-authored by Claude Opus 5
parent b5b2efab28
commit f86b604015
2 changed files with 49 additions and 0 deletions
+39
View File
@@ -1483,6 +1483,45 @@ void
// //
if (!IsCondemned()) 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<Node*> iterator(player_group->groupMembers);
Player *player;
while ((player = (Player*) iterator.ReadAndNext()) != NULL)
{
if (player->GetPlayerVehicle() == this)
{
player->ClearPlayerVehicle();
}
}
}
entity_manager->deathRow->Add(this); entity_manager->deathRow->Add(this);
SetCondemnedFlag(); SetCondemnedFlag();
NeverExecute(); NeverExecute();
+10
View File
@@ -331,6 +331,16 @@ public:
playerVehicle = player_vehicle; 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* Mission*
GetMission() GetMission()
{ {