#93: fix the end-of-round PerformAndWatch crash -- the STATIC projectile
pool held dangling entity pointers across round teardown Root cause (disasm-pinned on the shipped 4.11.659 exe): RajelAran's crash (call to 0x65676769 = ASCII 'igge', EBP-walk return btl4+0x2b91a) is the tgt->Dispatch vtable call in BTUpdateProjectiles' NON-MECH impact branch (mech4.cpp:1598; the site matches the disasm literally -- the 0x12/0x64 TakeDamageMessage ctor, the EntityID::Null ternary, the getenv gate after). The trap: the mech branch is guarded by BTIsRegisteredMech(tgt) -- but at round teardown a destroyed mech is DEREGISTERED, so a round still in flight (the pool is a static array that outlives the round; missiles are slow, #84) holding it as p.target now FAILS the mech check and falls into the !BTIsRegisteredMech branch, which treats the freed mech as a cultural icon and dispatches into freed memory. The liveness check itself routed the dangling pointer into the unguarded branch. Freed heap reused by a string -> vtable slot +0x10 read 'igge' -> call 0x65676769. (The EBP walker explains the stack shape: the faulting call pushed its return address on ESP, but the walk reads [EBP+4] = PerformAndWatch's frame.) Fix -- scrub at the source of truth: - BTProjectilesDropEntity(e): every pool entry drops a dying entity from p.target/p.shooter; called from ~Mech and ~CulturalIcon (the only free paths for targetable entities). Flight + impact code is already null-guarded on both fields. - BTProjectilesClearAll(): kills all rounds at RunMissions exit (cross- mission hygiene for the static pool). Verified: 6 short-mission cycles (45s missions with a spawned dummy + autofire, mission expiring mid-combat) -- 0 exceptions, 6/6 clean 'RunMissions returned'. The original crash was a heap-reuse race with no deterministic repro; the scrub eliminates the dangling-pointer class by construction. (Bench note: blind autofire never launches missiles -- the launcher needs a target lock -- so the exact in-flight race was not re-created; the non-regression + the pinned mechanism carry the verdict.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
This commit is contained in:
co-authored by
Claude Fable 5
parent
ba4af90d6b
commit
ac7c46b87a
@@ -450,6 +450,11 @@ CulturalIcon::~CulturalIcon()
|
||||
break;
|
||||
}
|
||||
}
|
||||
// #93: an icon can be a projectile's picked target -- scrub the static
|
||||
// projectile pool so a round in flight never dispatches into a freed icon
|
||||
// (same teardown race as the mech case; see mech4.cpp BTProjectilesDropEntity).
|
||||
extern void BTProjectilesDropEntity(void *e);
|
||||
BTProjectilesDropEntity(this);
|
||||
}
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
|
||||
@@ -1539,6 +1539,13 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
|
||||
app_manager->RunMissions();
|
||||
std::cout << "[boot] RunMissions returned (mission loop exited)." << std::endl << std::flush;
|
||||
// #93: the projectile pool is static -- kill any rounds still in flight so
|
||||
// nothing dangles across the mission boundary (the per-entity dtor scrub
|
||||
// covers the in-mission teardown race; this covers everything else).
|
||||
{
|
||||
extern void BTProjectilesClearAll(void);
|
||||
BTProjectilesClearAll();
|
||||
}
|
||||
|
||||
// MATCHLOG AUTO-UPLOAD (2026-07-22): in relay mode, send this peer's
|
||||
// match forensic log back to the operator's relay (saved under the
|
||||
|
||||
@@ -2151,6 +2151,9 @@ Mech::~Mech()
|
||||
|
||||
extern void BTDeregisterMech(Entity *m); // task #46 live-mech registry
|
||||
BTDeregisterMech((Entity *)this);
|
||||
extern void BTProjectilesDropEntity(void *e); // #93: scrub the STATIC projectile
|
||||
BTProjectilesDropEntity(this); // pool -- a round in flight must not
|
||||
// outlive its target/shooter pointer
|
||||
extern void BTUnstashClipState(const Mech *m); // task #59 clip-set registry
|
||||
BTUnstashClipState(this);
|
||||
|
||||
|
||||
@@ -847,6 +847,41 @@ struct BTProjectile {
|
||||
};
|
||||
static BTProjectile gProjectiles[64];
|
||||
|
||||
//###########################################################################
|
||||
// Projectile-pool entity scrub (#93 -- Rajel's end-of-round crash). The pool
|
||||
// is STATIC and outlives entities: at round teardown a destroyed mech is
|
||||
// DEREGISTERED, so a round still in flight holding it as p.target fails
|
||||
// BTIsRegisteredMech and falls into the NON-MECH impact branch -- which
|
||||
// happily Dispatch()es into the freed object (its vtable slot read reused
|
||||
// string bytes: the 0x65676769 "igge" call target, symbolized + disasm-pinned
|
||||
// to the tgt->Dispatch at the icon branch). The liveness check itself ROUTED
|
||||
// the dangling pointer into the unguarded branch. Fix at the source of
|
||||
// truth: every entity scrubs itself out of the pool as it dies (~Mech and
|
||||
// ~CulturalIcon call this), and mission exit clears the pool outright.
|
||||
//###########################################################################
|
||||
void BTProjectilesDropEntity(void *e)
|
||||
{
|
||||
for (int i = 0; i < 64; ++i)
|
||||
{
|
||||
BTProjectile &p = gProjectiles[i];
|
||||
if (!p.active)
|
||||
continue;
|
||||
if (p.target == (Entity *)e)
|
||||
p.target = 0; // flies on unguided; impact hits nothing
|
||||
if (p.shooter == (Entity *)e)
|
||||
{
|
||||
p.shooter = 0; // null-guarded at every use
|
||||
p.weaponSubsys = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BTProjectilesClearAll(void)
|
||||
{
|
||||
for (int i = 0; i < 64; ++i)
|
||||
gProjectiles[i].active = 0;
|
||||
}
|
||||
|
||||
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# #93 bench: mission teardown with missiles IN FLIGHT (3 cycles).
|
||||
set -x
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
for i in 1 2 3; do
|
||||
rm -f td_$i.log
|
||||
( export BT_PLATFORM=glass BT_START_INSIDE=1 BT_DEV_GAUGES=1 \
|
||||
BT_LOG=td_$i.log BT_AFFINITY=0x03 \
|
||||
BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_PROJ_LOG=1 BT_SPAWN_ENEMY=1
|
||||
../build/Release/btl4.exe -egg SHORTMSN.EGG &
|
||||
wait )&
|
||||
BGPID=$!
|
||||
# mission length 45s + load ~20s + menu relaunch; give it 100s then kill the relaunch
|
||||
sleep 100
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
echo "=== cycle $i ==="
|
||||
grep -c "crash" td_$i.log
|
||||
grep -c "RunMissions returned" td_$i.log
|
||||
grep -c "projectile" td_$i.log
|
||||
done
|
||||
echo "=== DONE ==="
|
||||
Reference in New Issue
Block a user