From 97a5d7e9ae2ff4cfba5aaf2c6dd5354a3d8ed449 Mon Sep 17 00:00:00 2001 From: uwezkhan <114483941+uwezkhan@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:29:24 +0530 Subject: [PATCH] Merge pull request #30395 from uwezkhan:onnx-qconv-scale-bias-bound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dnn: bound per-channel w_scale to bias length in onnx parseQConv 🤖🤖🤖 - #30395 OpenCV Extra: https://github.com/opencv/opencv_extra/pull/1445 `QLinearConv` import folds a constant int32 bias to float in `parseQConv` (`modules/dnn/src/onnx/onnx_importer2.cpp`): each bias element is multiplied by its per-channel weight scale and written into `bias_float`, which is sized from the bias tensor. The fold loop runs over the length of `w_scale`, but `w_scale` and the bias are independent inputs of the model, so a 1-D `w_scale` longer than the bias writes past `bias_float`. It is reached from `readNetFromONNX` on any `.onnx` carrying such a `QLinearConv`, and the scalar `x_scale` read is likewise unguarded against an empty tensor. ASan on the default engine loading a model whose bias has one element and whose per-channel `w_scale` has 64: ``` ==ERROR: AddressSanitizer: heap-buffer-overflow ... READ of size 4 #0 ONNXImporter2::parseQConv(...) #1 ONNXImporter2::parseNode(...) #2 ONNXImporter2::parseGraph(...) #5 readNetFromONNX2(...) #6 readNetFromONNX(...) 0x... is located 0 bytes after 4-byte region allocated by Mat::convertTo in parseQConv ``` Require the per-channel `w_scale` to hold one entry per bias element before the loop, the per-axis quantization invariant that pairs one scale and one bias with each output channel, and check the scalar `x_scale` is non-empty before the read. A well-formed model is unaffected; a mismatched one now throws from the importer and the net loads empty instead of overrunning the buffer. ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake --- modules/dnn/src/onnx/onnx_importer2.cpp | 5 +++++ modules/dnn/test/test_onnx_importer.cpp | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/modules/dnn/src/onnx/onnx_importer2.cpp b/modules/dnn/src/onnx/onnx_importer2.cpp index 4e33cf59e1..d028e054ce 100644 --- a/modules/dnn/src/onnx/onnx_importer2.cpp +++ b/modules/dnn/src/onnx/onnx_importer2.cpp @@ -2604,12 +2604,17 @@ void ONNXImporter2::parseQConv(LayerParams& layerParams, const opencv_onnx::Node Mat bias_float; bias_int32.convertTo(bias_float, CV_32F); + CV_CheckGE(x_scale_mat.total(), (size_t)1, "DNN/ONNX/QLinearConv: x_scale is empty"); float x_sc = x_scale_mat.at(0); if (w_scale_mat.total() == 1) bias_float *= x_sc * w_scale_mat.at(0); else + { + CV_CheckEQ(w_scale_mat.total(), bias_float.total(), + "DNN/ONNX/QLinearConv: per-channel w_scale length must match the bias length"); for (int i = 0; i < (int)w_scale_mat.total(); i++) bias_float.at(i) *= x_sc * w_scale_mat.at(i); + } bias_arg = netimpl->newConstArg(bn + "/bias_float", bias_float); } diff --git a/modules/dnn/test/test_onnx_importer.cpp b/modules/dnn/test/test_onnx_importer.cpp index 7fc8b2ae6d..755ff1f907 100644 --- a/modules/dnn/test/test_onnx_importer.cpp +++ b/modules/dnn/test/test_onnx_importer.cpp @@ -3897,4 +3897,26 @@ INSTANTIATE_TEST_CASE_P(/**/, Test_ONNX_MHA_Lowering, testing::Values( "multi_head_attention_key_padding_mask_attention_bias" )); +// QLinearConv folds a const int32 bias to float by multiplying each bias element +// by the matching per-channel weight scale, writing into a buffer sized from the +// bias tensor. w_scale and the bias are independent model fields, so a w_scale +// longer than the bias wrote past the bias buffer during import. The mismatched +// pair has to be rejected instead of overrunning the buffer. +// Test data in opencv_extra under the same branch name as this patch. +TEST(Test_ONNX_importer, parse_qconv_bias_scale_mismatch) +{ + Net net; + EXPECT_NO_THROW(net = readNetFromONNX(_tf("models/qconv_bias_scale_mismatch.onnx"))); + EXPECT_TRUE(net.empty()); +} + +// The same bias fold reads the scalar x_scale unconditionally, so an empty +// x_scale has to be rejected instead of being read past its end. +TEST(Test_ONNX_importer, parse_qconv_empty_x_scale) +{ + Net net; + EXPECT_NO_THROW(net = readNetFromONNX(_tf("models/qconv_empty_x_scale.onnx"))); + EXPECT_TRUE(net.empty()); +} + }} // namespace