| # ONNX ReferenceEvaluator β Unbounded Shape/Count Memory & Resource Exhaustion (4 operators) |
|
|
| **Status:** Preparing for Huntr submission |
| **Package:** [`onnx`](https://pypi.org/project/onnx/) (PyPI) β official Linux Foundation / Microsoft package, `onnx.reference.ReferenceEvaluator` |
| **Affected files:** `onnx/reference/ops/op_constant_of_shape.py`, `op_tile.py`, `op_expand.py`, `op_range.py` |
| **Class:** CWE-789 (Convergent Untrusted-Length Allocation) / CWE-400 (Uncontrolled Resource Consumption), root cause CWE-20 (Improper Input Validation) |
| **Severity:** High β models under 220 bytes cause multi-gigabyte memory consumption, either silently succeeding or crashing the process, with no validation opportunity for the caller. |
|
|
| ## Summary |
|
|
| `onnx.reference.ReferenceEvaluator` is the pure-Python reference implementation shipped inside the main `onnx` package (distinct from the separate, compiled `onnxruntime` engine) β used for testing, CI validation, spec-compliance checking, and lightweight inference without a full runtime. |
|
|
| Four of its operator kernels take a shape / repeat-count / range-length value directly from a graph tensor β in practice, an ordinary tiny `Constant` node embedded in the model file β and pass it straight into a numpy allocation call with **no upper-bound validation**: |
|
|
| | Operator | File | Vulnerable call | |
| |---|---|---| |
| | `ConstantOfShape` | `op_constant_of_shape.py` | `np.full(tuple(data), value)` | |
| | `Tile` | `op_tile.py` | `np.tile(x, repeats)` | |
| | `Expand` | `op_expand.py` | `np.ones(shape, dtype=data.dtype) * data` | |
| | `Range` | `op_range.py` | `np.arange(start, end, step)` | |
|
|
| In every case the *declaring* value is a handful of bytes (a single `int64`), while the resulting allocation can be gigabytes. None of the four check the requested element count against any bound before calling into numpy. |
|
|
| ## Proof of Concept |
|
|
| `poc_onnx_reference_evaluator_bomb.py` builds four minimal, fully valid ONNX models with `onnx.helper` (no hand-crafted binary patching needed β the vulnerable value is just an ordinary small `Constant` node) and runs each through `ReferenceEvaluator` in a subprocess, measuring peak RSS / outcome. |
|
|
| ```bash |
| pip install onnx numpy |
| python poc_onnx_reference_evaluator_bomb.py |
| ``` |
|
|
| ### Measured results |
|
|
| All four models declare **500,000,000** elements. |
|
|
| | Operator | Model size | Result | Peak RSS / time | |
| |---|---:|---|---| |
| | `ConstantOfShape` | 165 bytes | **silent success**, no exception | 1967 MB, 3.5β10.5 s | |
| | `Tile` | 177 bytes | **silent success**, no exception | 1967 MB, 0.4 s | |
| | `Expand` | 167 bytes | crash / **OOM-killed** | up to 3857 MB before kill | |
| | `Range` | 215 bytes | crash / **OOM-killed** | killed | |
|
|
| Two of the four (`ConstantOfShape`, `Tile`) return **"successfully"** with a valid-looking large array and zero error signal β the caller has no way to detect anything went wrong short of monitoring process memory externally. The other two (`Expand`, `Range`) crash the process outright. Either way, a file under 220 bytes controls the outcome. |
|
|
| ## Impact |
|
|
| Any code path that loads and evaluates an untrusted ONNX file through `ReferenceEvaluator` β CI test harnesses that run community-submitted models, model validation/preview services, lightweight pure-Python inference without `onnxruntime` β can be forced into multi-gigabyte memory consumption or an outright crash by a file of a few hundred bytes, with the model appearing completely ordinary (it's a normal `Constant` β operator graph, not a malformed file). |
|
|
| ## Suggested fix |
|
|
| Before allocating in each of the four kernels, validate the requested element count (product of shape dims for `ConstantOfShape`/`Expand`, `size(x) Γ prod(repeats)` for `Tile`, `(end - start) / step` for `Range`) against a configurable sane maximum, and raise a clear error immediately instead of proceeding to allocate. |
|
|
| ## Relationship to prior findings |
|
|
| This is the same broad class (CWE-789, CULA) previously confirmed in numpy, joblib, zarr, hickle, skops, anndata, fastparquet, and keras+h5py β but this is the first instance found in ONNX's own reference evaluator, and it's systemic across four separate operator implementations rather than a single code path, discovered by pattern-matching the first confirmed instance (`ConstantOfShape`) against sibling operators known to have similar historical issues elsewhere in the ONNX ecosystem. |
|
|
| ## Disclosure |
|
|
| Please do not use this PoC against production systems you do not own or have explicit permission to test. |
|
|