Merge pull request #30395 from uwezkhan:onnx-qconv-scale-bias-bound

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
This commit is contained in:
uwezkhan
2026-10-21 14:29:24 +05:30
committed by GitHub
parent a233ea41f1
commit 97a5d7e9ae
2 changed files with 27 additions and 0 deletions

View File

@@ -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<float>(0);
if (w_scale_mat.total() == 1)
bias_float *= x_sc * w_scale_mat.at<float>(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<float>(i) *= x_sc * w_scale_mat.at<float>(i);
}
bias_arg = netimpl->newConstArg(bn + "/bias_float", bias_float);
}

View File

@@ -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