Skip to content

WavStreamInstance::seek picks the vorbis path via the codec union — stb_vorbis_seek runs on live drwav*/drflac*/drmp3* and corrupts the heap #405

Description

@ivanpaulovich

WavStreamInstance::seek() selects the decoder path by testing the codec union pointer instead of the parsed filetype:

// src/audiosource/wav/soloud_wavstream.cpp
result WavStreamInstance::seek(double aSeconds, float* mScratch, unsigned int mScratchSize)
{
    if (mCodec.mOgg)   // <-- mCodec is a UNION: non-null for drwav*/drflac*/drmp3* too
    {
        int pos = (int)floor(mBaseSamplerate * aSeconds);
        stb_vorbis_seek(mCodec.mOgg, pos);
        ...

mCodec is a union of stb_vorbis* / drwav* / drflac* / drmp3*, so for any non-ogg WavStream with a live decoder this condition is true and stb_vorbis_seek() runs on a foreign decoder object. getAudio() and rewind() correctly switch (mParent->mFiletype); seek() is the one branch that doesn't.

Impact (observed in production, macOS): a looping WAV-typed WavStream reaches end-of-stream, mixBus_internal's loop path calls voice->seek(...) on the audio thread, and stb_vorbis_seek walks the drwav as if it were a vorbis handle. In our crashes it ended in stb_vorbis_stream_length_in_samples()'s error path writing f->total_samples = 0xffffffff into the foreign allocation — corrupting whatever object the heap had placed at that offset (we chased a "gesture recognizer" crash for days before tracing it back here). Any looping .wav/.flac/.mp3 stream triggers it on the first loop; sanitizers don't flag it because both objects are live.

Repro sketch:

SoLoud::Soloud soloud; soloud.init();
SoLoud::WavStream ws;
ws.load("anything.wav");         // filetype = WAVSTREAM_WAV
int h = soloud.play(ws);
soloud.setLooping(h, true);
// ...let it play to the end of the file; the loop-seek runs
// stb_vorbis_seek() on the live drwav* -> heap corruption / UB.

Fix (matches getAudio/rewind):

 result WavStreamInstance::seek(double aSeconds, float* mScratch, unsigned int mScratchSize)
 {
-    if (mCodec.mOgg)
+    if (mParent->mFiletype == WAVSTREAM_OGG && mCodec.mOgg)
     {

Non-ogg streams then fall through to the generic AudioSourceInstance::seek() (rewind + skip), which also makes looping WAV streams actually loop — with the current code the vorbis mis-seek fails, while (seek == SO_NO_ERROR) breaks, and the looping voice silently dies at its first loop point.

Happy to send this as a PR if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions