Death-crash FIXED: 25-voice explosion preset overflowed the 5-slot audio SourceSet (Gitea #12)

The weekend's crash family root-caused under cdb: SourceSet.sources[5]
receives the AllExplosion death preset's 25 streamed voices --
RequestAudioChannels wrote 20 OpenAL source ids past the array, smashing
the neighbouring heap object (Release: a vtable overwritten with a source
id -> delayed silent AV at AudioControlEvent::Send; Debug: CRT heap-
corruption abort in the death Explosion's audio teardown).  Explains all
three #12 crash flavors (solo enemy kill -- stack-confirmed; MP self-
death; MP peer PEER_DOWN cascade -- same generic teardown).

Fix: sources[] sized to AUDIO_SOURCESET_CAPACITY=25, static_assert
lockstep with MAX_PRESET_SAMPLES, + defence-in-depth clamps at the ctor
and acquisition sites.  Soak: 26+ deaths across glass AND pod builds
under cdb, zero faults, full wreck lifecycle every time.  Gotchas S21:
the fixed-array-vs-streamed-count overflow class + sweep note.

Awaiting human verification: the MP death-and-survive session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-20 14:51:35 -05:00
co-authored by Claude Fable 5
parent 6ad1e074cc
commit a0cec48e3f
5 changed files with 90 additions and 2 deletions
+47
View File
@@ -433,6 +433,53 @@ VehicleDead handler's disasm before changing). [T2]
---
## 21. Port fixed-array too small for the real data → heap overflow (the DEATH-CRASH, Gitea #12)
**Symptom class (Gitea #12 death-crash):** a SILENT hard crash a few seconds AFTER a mech DEATH
(no WER, no crash markers) — solo on a killed dummy, on the local player's own MP death, and on a
surviving peer when a player disconnects (PEER_DOWN). Combat runs fine for minutes; the crash is
deterministic *on the first kill*. Under cdb the stack is always in the **audio-entity teardown**
of the death Explosion: `FryDeathRow → Explosion::~Explosion → Entity::~Entity →
…NotifyOfEntityDestruction → RendererOrigin::RemoveUninterestingEntity →
L4AudioRenderer::NotifyOfBecomingUninterestingEntity → DestroyEntityAudioObjects →
SocketIterator::DeletePlugs → ~Dynamic3DPatchSource → operator delete` = **HEAP CORRUPTION
DETECTED after Normal block** (Debug); or a garbage vtable dispatch in `AudioControlSequence::
RunSequence → AudioControlEvent::Send` (`call [eax+0x1c]`, vtable == 0x16) (Release). Both are the
same corruption manifesting at the next free / next virtual call.
**Cause:** a WinTesla-port fixed array sized for an assumption the real gamedata violates.
`L4AUDHDW.h SourceSet { int count; ALuint sources[5]; }` — the OpenAL source array was **5** wide
(an "AWE 4-voice" assumption), but `channelSet.count = GetAudioVoiceCount()` = the streamed
per-preset `voiceCount`, and the **AllExplosion preset (bank2 p125 — the death boom) authors 25
layered zones** (`MAX_PRESET_SAMPLES = 25`, L4AUDLVL.h, one OpenAL source per zone). On channel
acquisition `RequestAudioChannels` runs `for (i<count) alGenSources(1, sources+i)` → writes
`sources[5..24]` **past the end of the 5-element array**, clobbering the adjacent heap object's
vtable / the debug heap guard bytes. Nothing faults until that neighbour is next *used* (the
running sequence's virtual `Send` — Release) or *freed* (the death Explosion's audio teardown —
Debug), seconds later. The `Playing hit an error: 40961` (`AL_INVALID_NAME`) burst right after the
wreck swap is the same corruption (the source-id array trashed). This is NOT the held #16
DPLEyeRenderable/boresight work (exonerated — never on the stack), and NOT the `rev=-64` mppr
value (a `ControlsButton` is "negative for release" by design, CONTROLS.h:26 — benign; the
`< 1` test at mechmppr.cpp:870 reads it correctly).
**Fix (2026-07-20):** size `SourceSet.sources[]` to `AUDIO_SOURCESET_CAPACITY = 25`
(== `MAX_PRESET_SAMPLES`; L4AUDLVL.h `static_assert`s the two in lockstep), so the largest
legitimate preset fits. Plus defence-in-depth clamps (a malformed stream can't overflow):
`channelSet.count` in the `L4AudioSource` ctor and `requested` in `RequestAudioChannels`. The
teardown path is generic to ANY entity leaving the interest set, so the one fix covers all three
#12 death flavors (solo kill, MP self-death, MP peer PEER_DOWN — all fire the same 25-voice
explosion preset through the same teardown). Files: `engine/MUNGA_L4/L4AUDHDW.h`,
`L4AUDLVL.h`, `L4AUDIO.cpp`, `L4AUDRND.cpp`. [T2 — solo stack-confirmed + fix survives 10+ kills
under cdb; MP flavors explained-by-mechanism, live-MP repro pending]
**Rule:** a port fixed-size array fed from streamed gamedata must be sized to the DATA's real
maximum (dump it — here the `MAX_PRESET_SAMPLES` comment already named the 25-zone worst case),
not a guessed hardware limit; and index loops driven by a streamed `count` must clamp to capacity.
Grep the port for sibling `[N]` arrays filled from a resource-derived count (channel/voice/zone/
segment tables) — the same class of latent overflow.
---
## Diagnostic recipe (the standard loop)
1. Read the RAW decomp `reference/decomp/all/part_*.c` for the `FUN_xxxx`.
2. Map `FUN_`/`DAT_`/`this+0xNN` to engine symbols via BT headers + WinTesla MUNGA source +