Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

CVE-2026-115922: CVE-2026-115922: Denial of Service via Unchecked Expansion of Packed Sub-Byte Tensors in ONNX

CVE-2026-115922: Denial of Service via Unchecked Expansion of Packed Sub-Byte Tensors in ONNX Vulnerability ID: CVE-2026-115922 CVSS Score: 6.2 Published: 2026-10-28 Open Neural Network Exchange (ONNX) prior to vers

CVE-2026-115922: Denial of Service via Unchecked Expansion of Packed Sub-Byte Tensors in ONNX

Vulnerability ID: CVE-2026-115922
CVSS Score: 6.2
Published: 2026-10-28

Open Neural Network Exchange (ONNX) prior to version 1.22.0 contains a memory exhaustion vulnerability (CWE-789) in its Python tensor unpacking helpers and C++ model checker. Specifically, the _unpack_2bit() and _unpack_4bit() functions in onnx.numpy_helper unconditionally resize unpacked NumPy arrays to match attacker-controlled TensorProto.dims metadata without verifying that the underlying raw_data or int32_data buffers contain a corresponding number of packed bits. Furthermore, onnx.checker.check_tensor() previously bypassed length verification for sub-byte data types. Processing an untrusted ONNX model with packed sub-byte tensors (UINT2, INT2, UINT4, INT4, or FLOAT4E2M1) allows an unauthenticated attacker to trigger disproportionate memory allocations from minimal payloads, resulting in an out-of-memory (OOM) crash and denial of service.

TL;DR

ONNX versions prior to 1.22.0 fail to validate the packed buffer length against declared tensor dimensions when unpacking 2-bit and 4-bit tensor types in Python. An attacker can supply a 1-byte tensor payload with high dimension metadata to force NumPy to allocate gigabytes of zero-filled memory via ndarray.resize(), causing an immediate denial of service.

⚠️ Exploit Status: POC

Technical Details

  • Vulnerability ID: CVE-2026-115922 / GHSA-q69g-f4f5-v8m7
  • Weakness Class: CWE-789: Memory Allocation with Excessive Size Value
  • CVSS v3.1 Score: 6.2 Medium (CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
  • Affected Component: onnx.numpy_helper (_unpack_4bit, _unpack_2bit) & onnx.checker
  • Affected Versions: ONNX < 1.22.0
  • Patched Version: ONNX 1.22.0
  • Impact: Denial of Service via Uncontrolled Memory Allocation (OOM)
  • EPSS / KEV Status: 0.131% (2.68th Percentile) / Not in CISA KEV

Affected Systems

  • Open Neural Network Exchange (onnx) Python package (< 1.22.0)
  • Open Neural Network Exchange (onnx) C++ core library (< 1.22.0)
  • onnx: < 1.22.0 (Fixed in: 1.22.0)

Code Analysis

Commit: dce5876

Validate unpacked sub-byte array size against math.prod(dims) in _unpack_4bit and _unpack_2bit before resizing (PR #8033)

--- a/onnx/numpy_helper.py
+++ b/onnx/numpy_helper.py
@@ -3,6 +3,7 @@
 from __future__ import annotations

+import math
 import sys
 from typing import TYPE_CHECKING, Any

@@ -110,9 +111,16 @@ def _unpack_4bit(
     array_high >>= np.uint8(4)
     result[0::2] = array_low
     result[1::2] = array_high
-    if result.size == np.prod(dims, dtype=np.int64) + 1:
+    expected_elements = math.prod(dims)
+    if result.size == expected_elements + 1:
         # handle single-element padding due to odd number of elements
         result = result[:-1]
+    if expected_elements > result.size:
+        raise ValueError(
+            f"Packed 4-bit data ({data.size} bytes, {result.size} elements unpacked) "
+            f"is too small for the declared shape {list(dims)} "
+            f"({expected_elements} elements required)."
+        )
     result.resize(dims, refcheck=False)
     return result

@@ -147,9 +155,16 @@ def _unpack_2bit(
     result[1::4] = (data >> 2) & 0x03
     result[2::4] = (data >> 4) & 0x03
     result[3::4] = (data >> 6) & 0x03
-    if result.size > np.prod(dims, dtype=np.int64):
+    expected_elements = math.prod(dims)
+    if result.size > expected_elements:
         # handle padding due to non multiple of 4 elements
-        result = result[: np.prod(dims, dtype=np.int64)]
+        result = result[:expected_elements]
+    if expected_elements > result.size:
+        raise ValueError(
+            f"Packed 2-bit data ({data.size} bytes, {result.size} elements unpacked) "
+            f"is too small for the declared shape {list(dims)} "
+            f"({expected_elements} elements required)."
+        )
     result.resize(dims, refcheck=False)
     return result

Commit: 05c1bc3

Enforce minimum raw_data and int32_data size checks for packed sub-byte tensor types in C++ check_tensor() (PR #8038)

--- a/onnx/checker.cc
+++ b/onnx/checker.cc
@@ -175,6 +175,31 @@ void check_tensor(const TensorProto& tensor, const CheckerContext& ctx) {
     if (tensor.data_type() == TensorProto::STRING) {
       fail_check("STRING data (tensor name: ", tensor.name(), ") should not be stored in raw_data field");
     }
+    // Validate that raw_data is large enough for the declared packed sub-byte type and shape.
+    int64_t expected_bytes = 0;
+    switch (tensor.data_type()) {
+      case TensorProto::UINT4:
+      case TensorProto::INT4:
+      case TensorProto::FLOAT4E2M1:
+        expected_bytes = (nelem + 1) / 2; // 2 elements per byte, ceiling division
+        break;
+      case TensorProto::UINT2:
+      case TensorProto::INT2:
+        expected_bytes = (nelem + 3) / 4; // 4 elements per byte, ceiling division
+        break;
+      default:
+        break;
+    }
+    if (expected_bytes > 0 && static_cast<int64_t>(tensor.raw_data().size()) < expected_bytes) {
+      fail_check(
+          "TensorProto (tensor name: ",
+          tensor.name(),
+          ") raw_data size (",
+          tensor.raw_data().size(),
+          " bytes) is too small for the declared shape and packed type (",
+          expected_bytes,
+          " bytes required).");
+    }
     return;
   } else {
@@ -215,10 +240,40 @@ void check_tensor(const TensorProto& tensor, const CheckerContext& ctx) {
       case TensorProto::FLOAT8E8M0:
       case TensorProto::UINT4:
       case TensorProto::INT4:
+      case TensorProto::FLOAT4E2M1:
+        check_field(int32_data);
+        if (nelem > 0) {
+          // Each int32 stores 4 bytes = 8 4-bit elements.
+          const int64_t expected_int32s = (nelem + 7) / 8;
+          if (static_cast<int64_t>(tensor.int32_data().size()) < expected_int32s) {
+            fail_check(
+                "TensorProto (tensor name: ",
+                tensor.name(),
+                ") int32_data size (",
+                tensor.int32_data().size(),
+                ") is too small for the declared shape and packed type (",
+                expected_int32s,
+                " int32 values required).");
+          }
+        }
+        break;
       case TensorProto::UINT2:
       case TensorProto::INT2:
-      case TensorProto::FLOAT4E2M1:
         check_field(int32_data);
+        if (nelem > 0) {
+          // Each int32 stores 4 bytes = 16 2-bit elements.
+          const int64_t expected_int32s = (nelem + 15) / 16;
+          if (static_cast<int64_t>(tensor.int32_data().size()) < expected_int32s) {
+            fail_check(
+                "TensorProto (tensor name: ",
+                tensor.name(),
+                ") int32_data size (",
+                tensor.int32_data().size(),
+                ") is too small for the declared shape and packed type (",
+                expected_int32s,
+                " int32 values required).");
+          }
+        }
         break;

Commit: 8de2cf6

Cherry-pick PR #8033 and PR #8038 into release branch rel-1.22.0 (PR #8066)

Mitigation Strategies

  • Upgrade the ONNX library to version 1.22.0 or later to enforce bounds checking in both Python unpacking routines and the C++ model checker.
  • Implement pre-deserialization dimension and buffer size checks on all sub-byte TensorProto objects (UINT2, INT2, UINT4, INT4, FLOAT4E2M1) when operating on legacy ONNX versions.
  • Enforce process-level virtual memory limits (RLIMIT_AS) and container cgroup memory quotas on services that parse or evaluate untrusted ONNX models.

Remediation Steps:

  1. Update the Python package via pip: pip install --upgrade "onnx>=1.22.0"
  2. Invoke onnx.checker.check_model(model) on all untrusted ONNX models prior to materializing tensors or initializing ReferenceEvaluator.
  3. Verify that exception handling blocks around model loading catch both onnx.checker.ValidationError and Python ValueError exceptions.

References

Read the full report for CVE-2026-115922 on our website for more details including interactive diagrams and full exploit analysis.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.