EnigmaConsultant's picture
Upload folder using huggingface_hub
236711b verified
|
Raw
History Blame Contribute Delete
8.44 kB
---
license: other
tags:
- security
- fuzzing
- onnx
- null-pointer-dereference
- denial-of-service
---
# ONNX core parser: NULL-pointer dereference in FunctionProto shape inference (DoS)
**Status:** gated PoC, access restricted for coordinated disclosure via huntr.com (Model File
Vulnerability program). Contact protectai-bot / huntr maintainers for access.
## Summary
`onnx/onnx` (the core protobuf model parser/checker library that ships with every
`pip install onnx`, and that ONNX Runtime, model-surgery tools, and MLOps pipelines call
directly on **untrusted, externally-sourced** `.onnx` files) crashes with a NULL-pointer
dereference (SIGSEGV) when shape inference is run over a model-local `FunctionProto` whose
call site omits a trailing optional input, **if that same (unbound) formal parameter name is
also used as one of the function's own output names**.
This is reachable via:
- `onnx.checker.check_model(model, full_check=True)` β€” the officially documented, commonly
recommended way to fully validate an untrusted ONNX model before use.
- `onnx.shape_inference.infer_shapes(model)` β€” used directly by many downstream tools
(ONNX Runtime tooling, `onnx-graphsurgeon`, model optimizers/converters, MLOps ingestion
pipelines) to propagate shapes over third-party models.
Both entry points crash the host **Python process itself** (native SIGSEGV, not a catchable
Python exception) on a **159-byte** crafted `.onnx` file β€” i.e. any service that loads and
validates/shape-infers an attacker-supplied ONNX file goes down.
## Root cause
`onnx/shape_inference/implementation.cc`, `ShapeInferenceImplBase::Process(const FunctionProto&, InferenceContext&)`:
```cpp
for (int i = 0; i < num_func_inputs; ++i) {
const auto& parameter_name = func_proto.input().Get(i);
const auto* const type_ptr = (i < num_actual_inputs) ? ctx.getInputType(i) : nullptr;
// nullptr is valid, and indicates a missing optional input
if (type_ptr != nullptr) {
types_cache[i] = *type_ptr;
value_types_by_name[parameter_name] = &types_cache[i];
} else {
value_types_by_name[parameter_name] = nullptr; // <-- intentional null, by design
}
}
```
When a function call site provides fewer actual inputs than the function's formal parameter
list (a legal, documented way to pass "missing optional inputs" to a function), the missing
parameter's name is registered in `value_types_by_name` with an explicit `nullptr` value.
This is intentional and, by itself, correct.
The bug is that two *consumers* of `value_types_by_name` later dereference that stored
pointer **without checking it for null**, whenever the same name is looked up again:
1. `ShapeInferenceImplBase::UpdateType()` (line ~367) β†’ `mergeShapesAndTypes(*inferred_type, iter->second)`
β†’ `checkShapesAndTypes(inferred_type, *existing_type)` (line 198, 112) dereferences
`existing_type` directly β†’ `TypeProto::value_case()` on a null `this` β†’ SEGV.
Triggered when any node *inside* the function body reuses the missing parameter's name as
its own output name (so `UpdateType` is invoked again for that name).
2. `ShapeInferenceImplBase::Process(const FunctionProto&, InferenceContext&)` (line 670),
the function-output copy-back loop:
```cpp
for (int i = 0; i < func_proto.output_size(); ++i) {
auto iter = value_types_by_name.find(output_name);
if (iter != value_types_by_name.cend()) {
ctx.getOutputType(i)->CopyFrom(*(iter->second)); // <-- null deref if iter->second==nullptr
}
}
```
Triggered directly when the function declares the missing parameter's name as one of its
own **outputs** (a pure pass-through, no processing node required at all β€” this is the
PoC below).
Both are two call sites of the **same** underlying oversight (introduced in
[onnx/onnx#5066](https://github.com/onnx/onnx/pull/5066), merged April 2023, "Fix an issue
in function type/shape inference to support optional inputs" β€” the fix added the
intentional-null storage but never audited/guarded its later readers). The bug has been
present, unfixed, for 3+ years and reproduces on current `main`
(`964b886d46d337fa775c6e4012a90534b28cd3ca`, 2026-07-07).
## PoC
`poc_minimal.onnx` (159 bytes) β€” built with `make_min_poc.py` (included) using the standard
`onnx.helper` API:
- One model-local function `custom.domain::PassThroughY`, formal inputs `["x", "y"]`,
formal outputs `["y"]`, **zero nodes** in its body (pure name pass-through).
- One call node in the main graph invokes it with only **one** actual input (`["fx"]`),
leaving formal parameter `"y"` unbound.
```python
import onnx
m = onnx.load("poc_minimal.onnx")
onnx.checker.check_model(m, full_check=True) # SIGSEGV
# or:
onnx.shape_inference.infer_shapes(m) # SIGSEGV
```
Reproduces on a stock `pip install onnx` (tested against onnx 1.22.0, PyPI wheel, no custom
build) β€” see `repro_pip_onnx.txt`.
## ASan stack trace (from-source ASan+AFL build of onnx/onnx main, matching line numbers)
See `asan_trace.txt`. Top of stack:
```
AddressSanitizer: SEGV on unknown address 0x000000000010
#0 onnx::TypeProto::MergeImpl(...) onnx-ml.pb.cc:11593
#1 ShapeInferenceImplBase::Process(FunctionProto const&, InferenceContext&) implementation.cc:670
#2 InferShapeForFunctionNodeInternal(...) implementation.cc:879
#3 ShapeInferenceImplBase::ProcessCall(...) implementation.cc:896
#4 ShapeInferenceImplBase::Process(NodeProto&) implementation.cc:489
#5 ShapeInferenceImplBase::Process(GraphProto&) implementation.cc:599
#6 InferShapesImpl(...) implementation.cc:789
#7 onnx::shape_inference::InferShapes(ModelProto&, ...) implementation.cc:824
#8 onnx::checker::check_model(ModelProto const&, ...) checker.cc:1187
```
## How this was found
Coverage-guided AFL++ fuzzing (`afl-clang-fast++` + `AFL_USE_ASAN=1`) directly against the
unmodified onnx/onnx C++ core (`checker::check_model` + `shape_inference::InferShapes`),
built from source against a matching protobuf 6.31.1/v31.1 (Abseil-backed) static build, and
seeded with a small hand-written corpus of structurally valid ONNX models covering Add/Conv/
If/Loop/local-function/Sequence graphs. AFL found 13 raw crashing inputs across a
~25-minute, single-machine (4-core) campaign; all 13 were manually triaged by true root
cause (not just distinct stack hashes) down to **one** underlying bug at two call sites, and
then hand-minimized to the 159-byte PoC above (no nodes, no tensors, no shapes β€” just the
function-signature/call-arity mismatch that triggers the null store).
## Dedup / prior-art check
- Github-issue-searched onnx/onnx for prior reports referencing `checkShapesAndTypes`,
`mergeShapesAndTypes`, `value_types_by_name`, `ProcessCall`,
`InferShapeForFunctionNodeInternal`, and "function optional input segfault" β€” no matching
open or closed issue found.
- Confirmed distinct from the closest-sounding prior reports: onnx/onnx#5219 (closed; a
*different* function β€” the `shape` **data-propagation** helper, not general
type/shape-inference merge), #5989 (schema-level empty input/output mismatch, not
function-call arity), #5212 (segfault specific to `data_prop=True` on nested functions).
- The introducing PR (#5066, April 2023) is old and merged; the bug has had 3+ years of
exposure (including onnx/onnx's own OSS-Fuzz atheris harnesses, live since ~mid-June 2026)
without being caught, most likely because triggering it requires a specific, non-trivial
`FunctionProto`/optional-input/name-reuse structure that raw-byte-mutation fuzzing rarely
reaches without a domain-aware seed β€” exactly the gap a seeded, coverage-guided native
build closes.
## Impact
Any process that runs `onnx.checker.check_model(untrusted_model, full_check=True)` or
`onnx.shape_inference.infer_shapes(untrusted_model)` over a file it does not fully trust β€”
model marketplaces, CI/CD model-validation gates, MLOps ingestion services, ONNX converters
β€” can be crashed (denial of service) by a 159-byte file with no special privileges. This is
a native SIGSEGV, not a Python exception, so it cannot be caught by a `try/except` around the
call; it takes down the whole process (and, in naive multi-tenant setups, everything else
running in it).