From a0cec48e3feabb4b7ea347dac9d09c32b8b814cd Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 20 Jul 2026 14:51:35 -0500 Subject: [PATCH] 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 --- context/reconstruction-gotchas.md | 47 +++++++++++++++++++++++++++++++ engine/MUNGA_L4/L4AUDHDW.h | 16 ++++++++++- engine/MUNGA_L4/L4AUDIO.cpp | 15 +++++++++- engine/MUNGA_L4/L4AUDLVL.h | 7 +++++ engine/MUNGA_L4/L4AUDRND.cpp | 7 +++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index dc11e27..51ca2b7 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -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 (i5 +// voices) overwrote adjacent heap on channel acquisition -- corrupting the +// next AudioComponent's vtable / the heap guard bytes and crashing seconds +// later at the death Explosion's audio-entity teardown (DestroyEntityAudio- +// Objects) or the next sequenced virtual dispatch. L4AUDLVL.h static_asserts +// MAX_PRESET_SAMPLES <= AUDIO_SOURCESET_CAPACITY to keep the two in lockstep. +const int AUDIO_SOURCESET_CAPACITY = 25; + struct SourceSet { int count; - ALuint sources[5]; + ALuint sources[AUDIO_SOURCESET_CAPACITY]; }; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MIDI types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/engine/MUNGA_L4/L4AUDIO.cpp b/engine/MUNGA_L4/L4AUDIO.cpp index 942dc41..15fd021 100644 --- a/engine/MUNGA_L4/L4AUDIO.cpp +++ b/engine/MUNGA_L4/L4AUDIO.cpp @@ -672,7 +672,20 @@ L4AudioSource::L4AudioSource( ): AudioSource(stream, entity) { - channelSet.count = GetAudioVoiceCount(); + // Gitea #12: never let the voice count exceed the SourceSet capacity -- a + // malformed/oversized preset stream would otherwise overflow sources[] on + // channel acquisition and corrupt the heap (the death-crash class). The + // array is now sized to the largest legitimate preset (AUDIO_SOURCESET_ + // CAPACITY == MAX_PRESET_SAMPLES), so this clamp is defence-in-depth. + int voices = GetAudioVoiceCount(); + if (voices > AUDIO_SOURCESET_CAPACITY) + { + DEBUG_STREAM << "[audio] WARNING: voiceCount " << voices + << " exceeds SourceSet capacity " << AUDIO_SOURCESET_CAPACITY + << " -- clamped (Gitea #12)\n" << std::flush; + voices = AUDIO_SOURCESET_CAPACITY; + } + channelSet.count = voices; L4AudioSourceX(); } diff --git a/engine/MUNGA_L4/L4AUDLVL.h b/engine/MUNGA_L4/L4AUDLVL.h index 934aea8..d7a9df8 100644 --- a/engine/MUNGA_L4/L4AUDLVL.h +++ b/engine/MUNGA_L4/L4AUDLVL.h @@ -40,6 +40,13 @@ struct SAMPLEINFO // AllExplosion (bank2 p125) authors 25 layered zones -- the largest preset. const int MAX_PRESET_SAMPLES = 25; +// Gitea #12: the SourceSet OpenAL-source array must hold one source per zone, +// so its capacity must cover the largest preset. If MAX_PRESET_SAMPLES ever +// grows past the SourceSet array, channel acquisition would overflow the heap +// (the death-crash class) -- lock the two together at compile time. +static_assert(MAX_PRESET_SAMPLES <= AUDIO_SOURCESET_CAPACITY, + "SourceSet.sources[] too small for MAX_PRESET_SAMPLES voices (Gitea #12)"); + struct PRESETINFO { int sampleNum; diff --git a/engine/MUNGA_L4/L4AUDRND.cpp b/engine/MUNGA_L4/L4AUDRND.cpp index 8077135..8e3415b 100644 --- a/engine/MUNGA_L4/L4AUDRND.cpp +++ b/engine/MUNGA_L4/L4AUDRND.cpp @@ -1294,6 +1294,13 @@ Logical //Do we have enough? int requested = source_request->count; + // Gitea #12: hard cap at the SourceSet capacity so alGenSources can never + // write past sources[] (the death-crash heap overflow). count is set from + // the streamed voiceCount and is normally already clamped in the + // L4AudioSource ctor; this guards the acquisition site itself. + if (requested > AUDIO_SOURCESET_CAPACITY) + requested = AUDIO_SOURCESET_CAPACITY; + bool failed = true; alGetError();