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();