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:
co-authored by
Claude Fable 5
parent
6ad1e074cc
commit
a0cec48e3f
@@ -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)
|
## Diagnostic recipe (the standard loop)
|
||||||
1. Read the RAW decomp `reference/decomp/all/part_*.c` for the `FUN_xxxx`.
|
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 +
|
2. Map `FUN_`/`DAT_`/`this+0xNN` to engine symbols via BT headers + WinTesla MUNGA source +
|
||||||
|
|||||||
@@ -6,10 +6,24 @@
|
|||||||
#include "..\munga\audio.h"
|
#include "..\munga\audio.h"
|
||||||
#include "openal/al.h"
|
#include "openal/al.h"
|
||||||
|
|
||||||
|
// SourceSet capacity (Gitea #12 death-crash fix): the OpenAL sources array
|
||||||
|
// must hold as many voices as a preset can carry. channelSet.count is set
|
||||||
|
// from GetAudioVoiceCount() (the streamed voiceCount), and a full-zone preset
|
||||||
|
// legitimately carries up to MAX_PRESET_SAMPLES (== 25, L4AUDLVL.h) sample
|
||||||
|
// zones -- one OpenAL source each (RequestAudioChannels alGenSources into
|
||||||
|
// sources[0..count-1], SetupPatch/PlayNote index sources[i] parallel to the
|
||||||
|
// samples[] zones). This array was 5 wide, so the death-boom preset (>5
|
||||||
|
// 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
|
struct SourceSet
|
||||||
{
|
{
|
||||||
int count;
|
int count;
|
||||||
ALuint sources[5];
|
ALuint sources[AUDIO_SOURCESET_CAPACITY];
|
||||||
};
|
};
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MIDI types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MIDI types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -672,7 +672,20 @@ L4AudioSource::L4AudioSource(
|
|||||||
):
|
):
|
||||||
AudioSource(stream, entity)
|
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();
|
L4AudioSourceX();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ struct SAMPLEINFO
|
|||||||
// AllExplosion (bank2 p125) authors 25 layered zones -- the largest preset.
|
// AllExplosion (bank2 p125) authors 25 layered zones -- the largest preset.
|
||||||
const int MAX_PRESET_SAMPLES = 25;
|
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
|
struct PRESETINFO
|
||||||
{
|
{
|
||||||
int sampleNum;
|
int sampleNum;
|
||||||
|
|||||||
@@ -1294,6 +1294,13 @@ Logical
|
|||||||
//Do we have enough?
|
//Do we have enough?
|
||||||
int requested = source_request->count;
|
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;
|
bool failed = true;
|
||||||
|
|
||||||
alGetError();
|
alGetError();
|
||||||
|
|||||||
Reference in New Issue
Block a user