Guard three graph-optimizer passes against unbounded model-supplied indices (#31670)
## Description
Three graph-optimizer passes that run at model load time index into a
vector using a
count/axis/index taken directly from the model graph, without validating
it against the
bounds of the vector being indexed:
1. **`GatherSliceToSplitFusion`**
(`onnxruntime/core/optimizer/gather_fusion.cc`): reads an
`axis` from a candidate `Gather`/`Slice` consumer and indexes the shared
input's shape
with it (`shape->dim(axis)`). ONNX's own shape inference for
`Gather`/`Slice` only range-checks
`axis` when it can resolve the shapes of all relevant inputs; if it
cannot (e.g. an unresolvable
indices/axis input shape), that check is skipped, and this fusion pass
previously had no
independent bounds check of its own.
2. **Transpose optimizer's `HandleTile`**
(`onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc`):
assumes a constant `Tile` `repeats` initializer has one entry per
dimension of the
preceding `Transpose`'s rank (taken from that node's `perm` attribute),
and indexes it
accordingly. Similarly, ONNX's own `Tile` shape inference only validates
`repeats.size()`
against the input rank when it can resolve the data input's shape; if it
cannot, this
validation is skipped, and there was no independent check before
indexing `repeats`.
3. **`WhereDummyDq`**
(`onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc`):
unconditionally reads `DequantizeLinear`'s 3rd input (`x_zero_point`, at
index 2) to
fetch its initializer. Per the ONNX spec, `x_zero_point` is an
**optional** input, so a
valid `DequantizeLinear` node can have only 2 inputs (`x`, `x_scale`),
making this an
out-of-bounds read on `InputDefs()`.
## Fix
Each pass now validates the model-supplied value against the actual
bound before using it
to index, and safely skips the optimization (rather than
asserting/crashing) when the value
is out of range:
- `GatherSliceToSplitFusion`: skip fusing this candidate if `axis < 0 ||
axis >= rank`.
- `HandleTile`: return `false` (leave the node alone) if `repeats.size()
!= rank`.
- `WhereDummyDq`: log a warning and skip inserting a dummy DQ if the DQ
node has fewer than
3 inputs.
## Other execution providers
All three passes are execution-provider-agnostic graph transformations
that run before EP
partitioning, so no EP-specific (e.g. CUDA) equivalent exists or needs a
separate fix.
## Testing
Added regression tests, using `TestGraphTransformer` (which applies the
transformer and
inspects the resulting graph without executing the model, since some of
the malformed
inputs used to reach these code paths are not valid inputs to actually
run):
- `graph_transform_test.cc`: `GatherSliceToSplitFusion_OutOfRangeAxis` —
a `Gather` node
with an out-of-range `axis`, where the sibling indices input has no
static shape so
ONNX's own inference cannot catch it ahead of time. Verifies the fusion
pass leaves the
graph unchanged.
- `transpose_optimizer_test.cc`: `TestTileRepeatsRankMismatchNoOpt` — a
`Transpose -> Tile
-> Transpose` pattern where `repeats` is shorter than the rank implied
by the
`Transpose`'s `perm`, with the data input's shape left unresolved.
Verifies both
Transposes and the Tile remain untouched.
- `qdq_transformer_test.cc`: `WhereDummyDqTest_DqWithoutZeroPoint` — a
`DequantizeLinear`
with only 2 inputs (no zero-point) feeding a `Where` node. Verifies no
dummy DQ is
inserted and the graph is otherwise unmodified.
All three new tests were confirmed to fail (or crash) when the
corresponding fix was
temporarily reverted, and pass cleanly with the fix in place. The full
`GraphTransformationTests`, `TransposeOptimizerTests`, and
`QDQTransformerTests` suites
were also run locally with no regressions.
## Motivation
These three passes run by default during `CreateSession` (default
optimization level),
processing whatever graph structure the model declares. Guarding the
indexing operations
against out-of-range model-supplied values makes these passes resilient
to malformed or
adversarial models without changing behavior for well-formed ones.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 77dcaf1b-748a-4379-94a7-478f7a924d73