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:
- Update the Python package via pip: pip install --upgrade "onnx>=1.22.0"
- Invoke onnx.checker.check_model(model) on all untrusted ONNX models prior to materializing tensors or initializing ReferenceEvaluator.
- Verify that exception handling blocks around model loading catch both onnx.checker.ValidationError and Python ValueError exceptions.
References
- GitHub Security Advisory GHSA-q69g-f4f5-v8m7
- NVD - CVE-2026-115922
- CVE.org Record - CVE-2026-115922
- ONNX PR #8033: Validate unpacked sub-byte tensor dimensions in Python
- ONNX PR #8038: Validate sub-byte tensor sizes in C++ checker
- ONNX PR #8066: Cherry-pick fixes into rel-1.22.0
- Commit dce5876d6cc6cc30157af7d26a752ef44976da8e (Python Fix)
- Commit 05c1bc3bceceb8d3cf11e9e7fc7f19f971c44988 (C++ Checker Fix)
- Commit 8de2cf6aa47928e6515ac6d9e1a8b4758886a7df (Release 1.22.0 Cherry-Pick)
- ONNX v1.22.0 Release
- CVEProject CVE-2026-115922 JSON Record
Read the full report for CVE-2026-115922 on our website for more details including interactive diagrams and full exploit analysis.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.