Heap OOB Read in SoundTouch FIFOSampleBuffer::setChannels() on Mid-Stream Channel Chang

Olivier Laflamme

SoundTouch's FIFOSampleBuffer::setChannels() at FIFOSampleBuffer.cpp:72 recalculates samplesInBuffer for the new channel count but does not reset or adjust bufferPos. When the channel count increases while bufferPos > 0 (after receiveSamples() has partially consumed the buffer), ptrBegin() computes buffer + bufferPos * NEW_channels, which overshoots the allocated buffer. Subsequent calls to rewind() or ensureCapacity() invoke memmove/memcpy from this wild pointer, producing a heap-buffer-overflow read.

The vulnerability is triggered by calling SoundTouch::setChannels() while audio data remains in the processing pipeline. The public API does not document any requirement to clear buffers before changing the channel count. source/SoundTouch/FIFOSampleBuffer.cpp, setChannels() , rewind(), ensureCapacity() .

The bug was identified while fuzzing Firefox, where the bundled SoundTouch copy was updated via commit 83af3e64b68b. Firefox is not currently exploitable due to an 8-channel cap and RLBox wasm2c sandboxing.

The Missing Reset

FIFOSampleBuffer::setChannels() adjusts samplesInBuffer to preserve the byte count of buffered data under the new channel layout, but never touches bufferPos:

c++
// FIFOSampleBuffer.cpp:72-81
void FIFOSampleBuffer::setChannels(int numChannels)
{
    uint usedBytes;

    if (!verifyNumberOfChannels(numChannels)) return;

    usedBytes = channels * samplesInBuffer;
    channels = (uint)numChannels;
    samplesInBuffer = usedBytes / channels;
    // BUG: bufferPos is NOT reset or adjusted
}

The bufferPos field tracks how many samples have been consumed from the front of the buffer. It is advanced by receiveSamples(). (bufferPos += maxSamples) and only reset to zero by rewind() or clear(). After setChannels(), every subsequent access through ptrBegin() multiplies bufferPos by the new (larger) channel count:

c++
// FIFOSampleBuffer.cpp:148-152
SAMPLETYPE *FIFOSampleBuffer::ptrBegin()
{
    assert(buffer);
    return buffer + bufferPos * channels;  // channels is now the NEW value
}

When channels increases from 2 to 16, ptrBegin() returns buffer + bufferPos * 16 instead of buffer + bufferPos * 2, overshooting by bufferPos * (16 - 2) float elements into unallocated heap.

rewind() and ensureCapacity() both call ptrBegin() as a source address for memmove/memcpy:

c++
// FIFOSampleBuffer.cpp:87-94 — rewind()
void FIFOSampleBuffer::rewind()
{
    if (buffer && bufferPos)
    {
        memmove(buffer, ptrBegin(),                           // source: wild pointer
                sizeof(SAMPLETYPE) * channels * samplesInBuffer);
        bufferPos = 0;
    }
}
c++
// FIFOSampleBuffer.cpp:175-177 — ensureCapacity(), on reallocation path
if (samplesInBuffer)
{
    memcpy(temp, ptrBegin(),                                  // source: wild pointer
           samplesInBuffer * channels * sizeof(SAMPLETYPE));
}

Both read from the wild pointer. The subsequent putSamples() call triggers ptrEnd()ensureCapacity()rewind(), completing the chain from API call to OOB read.

SoundTouch::setChannels() at SoundTouch.cpp:138-145 propagates the channel change to 5 FIFOSampleBuffer instances:

Any of these with bufferPos > 0 at the time of the channel switch will trigger the OOB read. The PoC triggers through RateTransposer::inputBuffer. GStreamer reachability with the appsrcpitchfakesink that produced a real SIGSEGV. In this case the POC pushes 5 buffers of 2ch audio, renegotiate caps to 8ch, push 5 buffers of 8ch audio.

ASAN Output

plain text
AddressSanitizer:DEADLYSIGNAL
=================================================================
==8==ERROR: AddressSanitizer: SEGV on unknown address 0x52f0000772c0 (pc 0x7ffffe881881 bp 0x7ffffb578960 sp 0x7ffffb578928 T1)
==8==The signal is caused by a READ memory access.
    #0 0x7ffffe881881 in memcpy (/lib/x86_64-linux-gnu/libc.so.6+0xc4881)
    #1 0x7ffffb8ec07a in memcpy /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
    #2 0x7ffffb8ec07a in soundtouch::FIFOSampleBuffer::ensureCapacity(unsigned int) /poc/soundtouch/source/SoundTouch/FIFOSampleBuffer.cpp:177
    #3 0x7ffffb8ec07a in soundtouch::FIFOSampleBuffer::ensureCapacity(unsigned int) /poc/soundtouch/source/SoundTouch/FIFOSampleBuffer.cpp:159
    #4 0x7ffffb8ec32b in soundtouch::FIFOSampleBuffer::ptrEnd(unsigned int) /poc/soundtouch/source/SoundTouch/FIFOSampleBuffer.cpp:136
    #5 0x7ffffb8ec3f6 in soundtouch::FIFOSampleBuffer::putSamples(float const*, unsigned int) /poc/soundtouch/source/SoundTouch/FIFOSampleBuffer.cpp:101
    #6 0x7ffffb8f19ff in soundtouch::RateTransposer::processSamples(float const*, unsigned int) /poc/soundtouch/source/SoundTouch/RateTransposer.cpp:137
    #7 0x7ffffb8f19ff in soundtouch::RateTransposer::processSamples(float const*, unsigned int) /poc/soundtouch/source/SoundTouch/RateTransposer.cpp:132
    #8 0x7ffffb8f28c9 in soundtouch::FIFOSamplePipe::moveSamples(soundtouch::FIFOSamplePipe&) /poc/soundtouch/include/FIFOSamplePipe.h:93
    #9 0x7ffffb8f28c9 in soundtouch::SoundTouch::putSamples(float const*, unsigned int) /poc/soundtouch/source/SoundTouch/SoundTouch.cpp:306
    #10 0x7ffffb994faa  (/usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstsoundtouch.so+0x5faa)
    #11 0x7ffffecf786c  (/lib/x86_64-linux-gnu/libgstreamer-1.0.so.0+0x8f86c)
    #12 0x7ffffecfae08  (/lib/x86_64-linux-gnu/libgstreamer-1.0.so.0+0x92e08)
    #13 0x7ffffecfb22d in gst_pad_push (/lib/x86_64-linux-gnu/libgstreamer-1.0.so.0+0x9322d)
    #14 0x7ffffe75729c  (/lib/x86_64-linux-gnu/libgstbase-1.0.so.0+0x4129c)
    #15 0x7ffffed221d6  (/lib/x86_64-linux-gnu/libgstreamer-1.0.so.0+0xba1d6)
    #16 0x7ffffeb54703  (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x87703)
    #17 0x7ffffeb50b30  (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x83b30)
    #18 0x7ffffe851ac2  (/lib/x86_64-linux-gnu/libc.so.6+0x94ac2)
    #19 0x7ffffe8e2a83 in __clone (/lib/x86_64-linux-gnu/libc.so.6+0x125a83)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/lib/x86_64-linux-gnu/libc.so.6+0xc4881) in memcpy

Impact

When the channel increase is large (e.g., 2 to 16) and bufferPos is significant, ptrBegin() computes a pointer that overshoots far enough to land in unmapped memory. The memmove in rewind() or the memcpy in ensureCapacity() dereferences this wild pointer, producing a SIGSEGV. This is a reliable, deterministic crash. Potential write paths (putSamples, rewind, ensureCapacity, TDStretch::overlap*, RateTransposer::transpose*) confirms this is strictly a read primitive. All write destinations in rewind() and ensureCapacity() use buffer (the base of the allocation) as the destination, not the wild pointer. The OOB data is read from the wild source address and written to a valid destination.

Fix

Fixed in commit f738b1132ec1fd56efc90367898244cf52d9e6a5 ("Add sanity check to rate value. Clear buffers when change nr of channels") on 2026-04-19, modifying FIFOSampleBuffer.cpp to clear buffers when changing the channel count.

The suggested fix was to call rewind() before recalculating samplesInBuffer, consolidating data at the buffer start and resetting bufferPos to 0:

c++
void FIFOSampleBuffer::setChannels(int numChannels)
{
    uint usedBytes;

    if (!verifyNumberOfChannels(numChannels)) return;

    rewind();  // consolidate data at buffer start, reset bufferPos to 0

    usedBytes = channels * samplesInBuffer;
    channels = (uint)numChannels;
    samplesInBuffer = usedBytes / channels;
}

The maintainer chose the more aggressive approach clearing all buffered data on channel change.