YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
ONNX Runtime Resize Op: Heap Buffer Overflow via ParseScalesData
Vulnerability Summary
Component: onnxruntime/core/providers/cpu/tensor/upsamplebase.h - ParseScalesData()
Type: Heap Buffer Overflow (CWE-122)
Entry Point: Model load → InferenceSession::Run() → Upsample<T>::Compute() → ParseScalesData()
Trigger: Loading and running a crafted .onnx model file containing a Resize op with oversized scales tensor
Impact: Heap corruption → potential Remote Code Execution
Affected Version: onnxruntime 1.23.2 (latest) and all prior versions with Resize op (opset >= 18)
Root Cause
In upsamplebase.h, ParseScalesData() performs a memcpy without verifying the destination buffer has sufficient capacity:
// upsamplebase.h line 543-551
ParseScalesData(const Tensor* scale, InlinedVector<float>& scales, int64_t rank) const {
const auto* scale_data = scale->Data<float>();
int64_t scales_size = scale->Shape().Size(); // attacker-controlled from model
ORT_RETURN_IF_NOT(scales_size > 0, "..."); // only checks > 0
if (scales.empty()) { // FALSE at runtime!
scales.resize(onnxruntime::narrow<size_t>(scales_size));
}
memcpy(scales.data(), scale_data, SafeInt<size_t>(scales_size) * sizeof(float)); // OVERFLOW
When called from Compute() at runtime (upsample.cc line 1390), scales is pre-initialized with input_dims.size() elements (e.g., 4 for NCHW input), making it non-empty. The resize() is skipped, but memcpy writes scales_size elements (attacker-controlled from model) into the undersized buffer.
Why Scales Are Not Cached (Trigger Condition)
The constructor caching condition at line 294:
if (get_scale && scale->Shape().Size() > 0 && ((opset < 18) || (rank > 0 && opset >= 18)))
When the model uses opset >= 18 and input X has no shape annotation (dynamic shape), rank = -1 at construction time. The caching condition fails, so scales_cached_ = false. At runtime, the vulnerable ParseScalesData path is taken.
Attack Vector
- Attacker crafts a
.onnxmodel with a Resize op (opset 19) - Input X has no shape annotation (common in dynamic-shape models)
- Scales tensor is embedded as initializer with 256+ elements (vs expected 4 for NCHW)
- Victim loads model with
onnxruntime.InferenceSession() - Victim runs inference →
Compute()→ParseScalesData()→ heap overflow memcpywrites 1024+ bytes into a 16-byte buffer → heap corruption
Files
poc_resize_overflow.onnx— Malicious ONNX model filetrigger.py— Reproduction script (generates model + triggers crash)
Reproduction
pip install onnxruntime==1.23.2 onnx numpy
python3 trigger.py
Expected Output
[+] Malicious model saved: poc_resize_overflow.onnx
scales tensor: 256 elements (expected: 4 for NCHW input)
opset: 19, input X shape: dynamic (rank unknown)
[*] Loading model: poc_resize_overflow.onnx
[*] Running inference...
scales_array capacity: ~16 bytes (4 floats)
memcpy size: 1024 bytes (256 floats)
expected: heap/stack corruption -> crash
Segmentation fault (core dumped)
Environment
- OS: Ubuntu 22.04 (WSL2) / tested on Linux x86_64
- Python: 3.10
- onnxruntime: 1.23.2 (latest release)
- onnx: 1.21.0
Suggested Fix
In ParseScalesData(), always resize the destination buffer to match scales_size before memcpy:
// Fix: always ensure buffer is large enough, regardless of empty() state
if (scales.size() < static_cast<size_t>(scales_size)) {
scales.resize(onnxruntime::narrow<size_t>(scales_size));
}
memcpy(scales.data(), scale_data, SafeInt<size_t>(scales_size) * sizeof(float));
Or alternatively, validate scales_size against current buffer size:
ORT_RETURN_IF_NOT(scales_size <= static_cast<int64_t>(scales.size()),
"scales tensor size exceeds expected rank");