Skip to content

Use-after-free in CallbackResampler.read() for mono 1-D input (samplerate 0.2.4) #41

Description

@shauneccles

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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