Summary
Since #36 (06e88d1, released in 0.2.4), CallbackResampler.read() can return a NumPy array whose data pointer refers to freed memory. It happens when the callback supplies 1-D (mono) arrays and a read returns fewer frames than requested, which happens routinely at the end of every stream. The array is writeable, so writing to it corrupts whatever the allocator has since put there.
Cause
In read() (src/samplerate.cpp), for _channels == 1 && _buffer_ndim == 1:
output = py::array_t<float, py::array::c_style>(
out_shape, static_cast<float *>(outbuf.ptr)); // copies into a NEW array
...
if (output_frames_gen < frames) {
...
return py::array_t<float, py::array::c_style>(
out_shape, strides, static_cast<float *>(outbuf.ptr), output);
}
The first statement copies the 2-D buffer into a new 1-D array. The returned view then combines the pointer of the original buffer (outbuf.ptr) with the copy (output) as its base. Nothing keeps the original array alive once read() returns and outbuf is released, so the view dangles.
Reproduction
Python 3.14.6, numpy 2.5.3, pip install samplerate==0.2.4:
import numpy as np, samplerate
x = np.arange(1, 168, dtype=np.float32)
chunks = iter([x, None])
with samplerate.CallbackResampler(lambda: next(chunks), 0.9, "linear") as cb:
y = cb.read(1000) # short read: only ~150 frames available
print(np.shares_memory(y, y.base)) # False: y's data is not inside its base
junk = [np.full(1000, 7.0, np.float32) for _ in range(50)]
print(y[:4]) # [7. 7. 7. 7.]: freed memory was reused
print(samplerate.resample(x, 0.9, "linear")[:4]) # [1. 1.111 2.222 3.333]
Impact
Wrong audio data, possible crashes, and heap corruption if the caller writes to the returned array. Any code that reads a mono stream to completion with CallbackResampler is affected. It isn't directly attacker-controlled, but it is memory-unsafe behaviour reachable from ordinary API use.
Fix
Take the pointer from the array used as the base:
return py::array_t<float, py::array::c_style>(
- out_shape, strides, static_cast<float *>(outbuf.ptr), output);
+ out_shape, strides, static_cast<float *>(output.request().ptr),
+ output);
A deterministic regression test: every truncated output must satisfy y.base is None or np.shares_memory(y, y.base). It fails on 0.2.4 and passes with the fix. We shipped this fix and test in the LedFx fork (samplerate-ledfx): LedFx#14
Disclosure note: that fork PR is public and describes the bug.
Summary
Since #36 (06e88d1, released in 0.2.4),
CallbackResampler.read()can return a NumPy array whose data pointer refers to freed memory. It happens when the callback supplies 1-D (mono) arrays and a read returns fewer frames than requested, which happens routinely at the end of every stream. The array is writeable, so writing to it corrupts whatever the allocator has since put there.Cause
In
read()(src/samplerate.cpp), for_channels == 1 && _buffer_ndim == 1:The first statement copies the 2-D buffer into a new 1-D array. The returned view then combines the pointer of the original buffer (
outbuf.ptr) with the copy (output) as its base. Nothing keeps the original array alive onceread()returns andoutbufis released, so the view dangles.Reproduction
Python 3.14.6, numpy 2.5.3,
pip install samplerate==0.2.4:Impact
Wrong audio data, possible crashes, and heap corruption if the caller writes to the returned array. Any code that reads a mono stream to completion with
CallbackResampleris affected. It isn't directly attacker-controlled, but it is memory-unsafe behaviour reachable from ordinary API use.Fix
Take the pointer from the array used as the base:
A deterministic regression test: every truncated output must satisfy
y.base is None or np.shares_memory(y, y.base). It fails on 0.2.4 and passes with the fix. We shipped this fix and test in the LedFx fork (samplerate-ledfx): LedFx#14Disclosure note: that fork PR is public and describes the bug.