Validate runtime tensor ranks in CPU Conv, ConvInteger, QLinearConv, CropAndResize, Einsum, and AffineGrid kernels (#33517)
## Summary
When input tensor shapes are dynamic or omitted from `ValueInfoProto` at
model load time, ONNX schema shape inference does not reject rank
mismatches before kernel execution. This PR adds runtime rank checks
(and reorders shape validation before dimension indexing) across six CPU
and contrib operator paths so invalid runtime inputs return a clean
`INVALID_ARGUMENT` / `FAIL` `Status` instead of triggering `gsl::span`
contract-violation `std::terminate()` aborts (or reading past empty
buffers when trailing dimensions are zero):
1. **`ConvAttributes::ValidateInputShape`
(`onnxruntime/core/providers/cpu/nn/conv_attributes.h`)**:
- Checks `input_shape.NumDimensions() >= 3` (minimum rank for 1D
convolution `[N, C, D1]`) after verifying `input_shape.NumDimensions()
== weight_shape.NumDimensions()` and before indexing `weight_shape[0]`,
`input_shape[1]`, and `weight_shape[1]`. Previously, when both `X` and
`W` had rank 0 or rank 1 at runtime (e.g., `X={3}, W={3}, group=3`),
`input_shape.NumDimensions() != weight_shape.NumDimensions()` passed and
`input_shape[1]` / `weight_shape[1]` indexed past the 1-element
`gsl::span`; when both had rank 2 (e.g., `X={1, 1}, W={1, 1}`),
`ValidateInputShape` succeeded with zero spatial dimensions
(`kernel_rank == 0`).
2. **`ConvInteger::Compute`
(`onnxruntime/core/providers/cpu/quantization/conv_integer.cc`)**:
- Moves `ORT_RETURN_IF_ERROR(conv_attrs_.ValidateInputShape(X, W));`
before indexing `X->Shape()[0]`, `X->Shape()[1]`, and `W->Shape()[0]`.
Previously, rank-0 or rank-1 `X` or rank-0 `W` indexed `Shape()[0..1]`
out of bounds before `ValidateInputShape` was reached.
3. **`QLinearConv<ActType>::Compute`
(`onnxruntime/core/providers/cpu/quantization/qlinearconv.cc`)**:
- Moves `ORT_RETURN_IF_ERROR(conv_attrs_.ValidateInputShape(X->Shape(),
W_shape, channels_last_));` before indexing `X->Shape()[0]` and
`W_shape[0]`.
4. **`contrib::CropAndResize<T>::Compute`
(`onnxruntime/contrib_ops/cpu/crop_and_resize.cc`)**:
- Checks `x_dims.NumDimensions() == 4`, `rois_dims.NumDimensions() ==
2`, and `batch_indices_dims.NumDimensions() == 1` before indexing
`x_dims[1]`, `batch_indices_dims[0]`, and `rois_dims[1]` (which
previously ran before `CheckROIAlignValidInput`).
- In addition to preventing low-rank `gsl::span` aborts, enforcing exact
ranks before indexing ensures that `Shape().Size()` equals the product
of the validated dimensions (`x_dims[0..3]`, `rois_dims[0..1]`, and
`batch_indices_dims[0]`), keeping `rois_data`, `batch_indices_data`, and
`X_data` reads in range even when dynamic runtime inputs have a trailing
zero dimension (such as `rois` with shape `{1, 4, 0}` or `batch_indices`
with shape `{1, 0}`).
5. **`EinsumComputePreprocessor::ProcessSubscripts`
(`onnxruntime/core/providers/cpu/math/einsum_utils/einsum_compute_preprocessor.h`)**:
- Moves the excess subscript-label check (`if (dim_counter >= rank)`)
ahead of `auto dim_value = dims[dim_counter];`. Previously, `if
(++dim_counter > rank)` ran at the end of the character loop body after
`dims[dim_counter]` had already been indexed out of bounds.
6. **`AffineGrid<T>::Compute`
(`onnxruntime/core/providers/cpu/tensor/affine_grid.cc`)**:
- Checks `size_shape.NumDimensions() == 1` before indexing
`size_shape.GetDims()[0]` and reading `size_data[0..3]` /
`size_data[0..4]`. Without this check, a rank-0 `size` tensor aborts on
an empty `GetDims()` span, and enforcing rank 1 ensures
`size_shape.GetDims()[0]` equals the element count of `size_data`.
## Tests
- Added regression unit tests pinned to `DefaultCpuExecutionProvider()`
with `test.AddShapeToTensorData(false)` so dynamic runtime shapes reach
the CPU kernels:
- `ConvTest.Rank1InputAndWeightRejected` and
`ConvTest.Rank2InputAndWeightRejected` (`conv_op_test.cc`)
- `ConvIntegerTest.Rank1InputRejected` and
`ConvIntegerTest.Rank2InputAndWeightRejected` (`conv_integer_test.cc`)
- `QLinearConvTest.Rank0InputRejected` (`qlinearconv_op_test.cc`)
- `CropAndResizeTest.InvalidInputRanksRejected`
(`crop_and_resize_op_test.cc`)
- `Einsum.SubscriptLabelsExceedInputRankRejected` (`einsum_test.cc`)
- `AffineGridTest.size_input_is_rank_0` and
`AffineGridTest.size_input_rank_2_trailing_zero_rejected`
(`affine_grid_test.cc`)
- Verified in `onnxruntime_test_all`
(`--gtest_filter="ConvTest.*:ConvIntegerTest.*:QLinearConvTest.*:CropAndResizeTest.*:Einsum.*:AffineGridTest.*"`):
all 9 new regression tests fail or abort before the fix and pass (`9/9`)
with the fix applied, and the full 302-test filter passes with zero
regressions (`299 PASSED`, `3 SKIPPED`).
---
*Note: This patch was prepared with AI assistance and verified by human
review and local test execution.*