CVE-2026-55093: tract-nnef Integer Overflow Vulnerability
MODERATECVE-2026-55093 is a MODERATE (CVSS 6.1) integer overflow vulnerability (CWE-190: Integer Overflow or Wraparound) affecting tract-nnef libraries in the crates.io ecosystem. The issue involves integer overflow in nnef `.dat` tensor parser yields an out-of-bounds read on model load. Remediation is available by upgrading affected packages to version 0.23.1, 0.22.2, 0.21.16.
CVE-2026-55093 at a Glance
Security Impact
CVSS: 6.1
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H).
How to Fix CVE-2026-55093
To remediate this vulnerability, upgrade all affected tract-nnef dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade tract-nnef to 0.23.1
Upgrade tract-nnef to 0.22.2
Upgrade tract-nnef to 0.21.16
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for tract-nnef
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-55093, inspect your project dependencies for tract-nnef:
inspect dependencies for tract-nnef
Analyze the terminal output to locate any version of tract-nnef in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.23.1), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-190: Integer Overflow or Wraparound, where the product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. this occurs when an integer value is incremented to a value that is too large to store in the associated representation. when this occurs, the value may become a very small or negative number. In tract-nnef, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-55093 Affected tract-nnef Versions
3 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
tract-nnef
|
crates.io |
≥ 0.23.0
< 0.23.1
|
0.23.1
|
|
tract-nnef
|
crates.io |
≥ 0.22.0
< 0.22.2
|
0.22.2
|
|
tract-nnef
|
crates.io |
< 0.21.16
|
0.21.16
|
References
Frequently Asked Questions
What is CVE-2026-55093?
Which tract-nnef versions are affected?
What version fixes CVE-2026-55093?
Is tract-nnef affected?
What is the CVSS score of CVE-2026-55093?
What CWE is associated with CVE-2026-55093?
How can I check crates.io dependencies for CVE-2026-55093?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-x5mv-8wgw-29hg",
"modified" : "2026-06-18T15:05:23Z",
"published" : "2026-06-18T15:05:23Z",
"aliases" : [ "CVE-2026-55093" ],
"summary" : "tract-nnef: integer overflow in NNEF `.dat` tensor parser yields an out-of-bounds read on model load",
"details" : "- **Component:** `tract-nnef` (`nnef/src/tensors.rs::read_tensor`) + `tract-data` (`data/src/tensor.rs`)\n- **Affected versions:** `< 0.21.16`, `0.22.0`–`0.22.2`, `0.23.0`–`0.23.1` — the dense `DatLoader` path was unguarded across all three release lines; patched in 0.21.16 / 0.22.2 / 0.23.1\n- **Class:** CWE-190 (integer overflow) → CWE-125 (out-of-bounds read)\n- **Trigger:** loading a crafted NNEF model archive (`*.nnef.tgz` / `*.nnef.tar` / dir) via the public `tract_nnef::nnef().model_for_path` / `model_for_read`\n- **Impact:** `read_tensor` returns a memory-unsafe tensor (reported `len` 2^61 over a 56-byte heap allocation). Always-on primitive: a **bounded heap out-of-bounds read** during model build (`as_uniform`), an adjacent-heap information-disclosure reachable via the public load API. The resulting slice is an unsound `from_raw_parts(ptr, 2^61)` that **SIGSEGVs (DoS)** on any access past the mapped region (demonstrated by direct access). No out-of-bounds write and no RCE were achieved — tract's const-folding/`as_uniform` fast-paths fold simple consuming graphs without the full read.\n- **Severity:** Medium\n\n## Summary\n\n`read_tensor` builds a tensor `shape` from attacker-controlled 32-bit dimensions and computes the element count `len = product(shape)` and the byte allocation `product(shape) * size_of(dt)` with **unchecked `usize` arithmetic**. In `--release` (no `overflow-checks`), both products wrap modulo 2^64. An attacker chooses dimensions so that the wrapped products collapse to a small value that satisfies the header consistency check, while the *true* element count remains astronomically large. `read_tensor` returns `Ok` with a `Tensor` whose reported `len` (e.g. 2^61+7) is far larger than its backing heap allocation (e.g. 56 bytes). The unchecked slice accessor `as_slice_unchecked` (`from_raw_parts(ptr, self.len)`) then produces a slice spanning ~18 exabytes over a 56-byte buffer. The out-of-bounds read fires automatically during model build (no inference required), reachable through the default `DatLoader` resource loader.\n\n## Root cause\n\n`nnef/src/tensors.rs`, `read_tensor`:\n\n```\nlet shape: TVec<usize> = header.dims[0..header.rank as usize].iter().map(|d| *d as _).collect();\nlet len = shape.iter().product::<usize>(); // (1) unchecked, wraps\n...\n} else if header.bits_per_item != u32::MAX\n && len * (header.bits_per_item as usize / 8) != header.data_size_bytes as usize // (2) wrapped == u32\n{\n bail!(...);\n}\n...\nlet mut tensor = unsafe { Tensor::uninitialized_dt(dt, &shape)? }; // (3) alloc off the same wrapped product\n...\nreader.read_exact(plain.as_bytes_mut())?; // storage-bounded read, no overflow here\nOk(tensor)\n```\n\n`data/src/tensor.rs`, `uninitialized_aligned_dt`:\n\n```\nlet bytes = shape.iter().cloned().product::<usize>() * dt.size_of(); // (3) wraps to the same small value\nlet storage = ... Blob::new_for_size_and_align(bytes, alignment) ...;\n...\ntensor.update_strides_and_len(); // len = product(shape), wraps, no clamp\n```\n\nThe three quantities — the consistency-check LHS `(2)`, the allocation `(3)`, and the reported `len` — are all the same wrapped `product(shape)*size_of`, so they stay mutually consistent and **the consistency check at `(2)` cannot catch the overflow**. `data_size_bytes` is a `u32`, so the attacker simply sets it to the wrapped value.\n\nCorruption sink — `data/src/tensor.rs::as_slice_unchecked` (and `data/src/tensor/plain_view.rs::as_slice_unchecked`):\n\n```\nif self.storage.byte_len() == 0 { &[] }\nelse { std::slice::from_raw_parts(self.as_ptr_unchecked(), self.len()) } // len = 2^61 over a 56-byte alloc\n```\n\nThe only guard is `byte_len() == 0`. A small **non-zero** allocation defeats it and yields an unsound oversized slice.\n\n## Witness (F64)\n\n```\ndims = [33955849, 7005787, 359, 3, 3, 3] (rank 6, each <= u32::MAX)\nproduct(shape)= 2_305_843_009_213_693_959 = 2^61 + 7\nbits_per_item = 64 (F64), item_type = 0, item_type_vendor = 0\ndata_size_bytes = 56 # == (2^61+7)*8 mod 2^64\n```\n\n- `len * (bits/8) mod 2^64 = (2^61+7)*8 mod 2^64 = 56 == data_size_bytes` → consistency check passes.\n- allocation = `(2^61+7)*8 mod 2^64 = 56` bytes (7 × F64).\n- reported `len` = `2^61+7` elements.\n\nOnly the `is_copy()` numeric arms (F16/F32/F64/int, and likely the `complex` arms) are exploitable. F64 is the cleanest (`bits/8` divides evenly). The `bool`, `String`, and block-quant paths are each guarded by an independent mechanism (size_of==1 prevents byte/element divergence; `String` bails on a missing `num_traits::Zero` impl; block-quant has its own `ensure!(expected_len == data_size_bytes)` and uses non-plain `Exotic` storage).\n\n## Reachability (load-time, public API)\n\n```\nnnef().model_for_read(tar)\n -> proto_model_for_read nnef/src/framework.rs:303\n -> DatLoader.try_load (any *.dat) nnef/src/resource.rs:97 (default loader, framework.rs:33)\n -> read_tensor -> Ok(Tensor{len=2^61+7, storage=56B}) nnef/src/tensors.rs:61\n -> into_typed_model -> variable() fragment nnef/src/ops/nnef/deser.rs:74\n ensure!(tensor.shape() == &*shape) deser.rs:122 (attacker matches shape in graph.nnef -> passes)\n -> Const::new -> wire_node core/src/model/typed.rs:67\n -> Const::output_facts core/src/ops/konst.rs:54\n -> TypedFact::try_from core/src/model/fact.rs:459\n -> Tensor::as_uniform -> is_uniform_t::<f64> data/src/tensor.rs:1099\n -> as_slice_unchecked::<f64> data/src/tensor.rs:1044\n -> from_raw_parts(ptr, 2^61+7) over 56-byte buffer -> OOB READ\n```\n\nNo shape-vs-storage re-validation exists anywhere on this path (`proto.validate()` checks only the AST; `Const::new` checks only `is_plain`; `check_for_access` checks only the datum type; even the *safe* `PlainView::as_slice` does `from_raw_parts(ptr, self.len)` with no length guard).\n\n## Execution (proof of concept)\n\nReproduced against the crate at the affected revision, `--release`, x86_64-linux. Three scenarios:\n\n1. **Direct `read_tensor`** — feed the crafted 128-byte header + 56-byte payload:\n - `read_tensor -> Ok`, `shape=[33955849,7005787,359,3,3,3]`, `len()=2305843009213693959`, `as_bytes().len()=56`, `as_slice::<f64>().len()=2305843009213693959`.\n - `s[7]` (first element past the 56-byte allocation) returns `0x0000000000000041` → **heap OOB read** (adjacent-heap disclosure).\n - `s[1<<40]` → **SIGSEGV** (signal 11).\n2. **Public load API** — build a malicious `.nnef.tar` (`graph.nnef` with `variable(label='weights', shape=[...])` + `weights.dat`) and call `nnef().model_for_read()`:\n - returns `Ok` with one `Const` node, `out[0].fact.uniform=Some(...)`, `len()=2305843009213693959` over a 56-byte buffer → confirms `as_uniform`/`is_uniform_t`/`as_slice_unchecked` performed an **OOB read on load** (bounded over-read here because `is_uniform`'s `.all()` short-circuits on the uniform `0x41` payload).\n3. **Optimized graph** — same archive but the const is consumed (`output = mul(weights, weights)`), then `into_optimized` / `run`:\n - **Does not crash.** With both a uniform (`0x41×56`) and a non-uniform (`0..56`) payload, `into_optimized` const-folds `mul(const, const)` to a single node **without a full-length materialization** of the oversized const, and `run` completes. A reliable arbitrary-length crash through a *normal optimized graph* was therefore NOT demonstrated; the always-on primitive is the bounded load-time over-read (scenario 2), and the wild-slice SIGSEGV is shown via direct access (scenario 1).\n\nRunnable PoC sources are available to the maintainers on request.\n\n## Detection\n\n- **Static:** flag `*.iter().product::<usize>()` over externally-controlled dimensions without `checked_*`/`try_into`, especially when the result feeds an allocation and a separately-tracked `len`.\n- **Runtime / fleet:** crash telemetry showing SIGSEGV inside `is_uniform_t` / `from_raw_parts` during NNEF model load; an ASAN build flags `heap-buffer-overflow READ` in `read_tensor`→`as_uniform`.\n- **Input filter (compensating):** reject NNEF `.dat` tensors where `product(dims)` overflows `u64`, or where `product(dims) * size_of(dt) != data_size_bytes` computed in **checked** arithmetic, before constructing the tensor.\n- **YARA-ish heuristic for `.dat` blobs:** NNEF magic `4E EF 01 00`, `rank<=8`, and any `dim >= 0x10000` whose checked product with the others overflows.\n\n## Mitigation (suggested fix)\n\nIn `read_tensor`, compute the element count and byte size with checked arithmetic and reject on overflow, mirroring the guard already present on the block-quant path (`ensure!(expected_len == data_size_bytes)` added in `eacd13ccb`):\n\n```\nlet len = shape.iter().try_fold(1usize, |a, &d| a.checked_mul(d))\n .context(\"tensor shape product overflows usize\")?;\nlet byte_size = len.checked_mul(dt.size_of())\n .context(\"tensor byte size overflows usize\")?;\nensure!(byte_size == header.data_size_bytes as usize, \"shape/len vs data_size_bytes mismatch\");\n```\n\nDefense in depth: make `Tensor::uninitialized_aligned_dt` reject when `product(shape)*size_of` overflows, and add a `len * size_of == storage.byte_len()` invariant check in the `as_slice*` accessors (or at `Tensor` construction) so a `len`/storage mismatch can never reach `from_raw_parts`.\n\nMapping: CWE-190, CWE-125; mitigations align with input validation (OWASP ASVS V5) and safe integer handling (CERT INT32-C analogue).\n\n## Prior art / why this is not already fixed\n\n- `eacd13ccb` (2026-03-23, \"Add blob-size validation to BlockQuantStorage constructors\") added overflow/blob-size validation **only to the block-quant path**; the dense `DatLoader`/`read_tensor` path was left unguarded. The maintainers fixed the sibling and missed this one.\n- PR #745 (\"Fix UB by creating uninit Tensors with a non-null pointer\") is a *different* UB (null base pointer on zero-length slices) in the same module family.\n- No CVE / RustSec / GHSA / OSV / Huntr entry matches this bug; last change to `nnef/src/tensors.rs` predates HEAD and added no overflow guard to the dense path.\n\n---\n\nReported by: s1ko (s1ko@riseup.net · github.com/s1ko)",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "crates.io",
"name" : "tract-nnef"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0.23.0"
}, {
"fixed" : "0.23.1"
} ]
} ]
}, {
"package" : {
"ecosystem" : "crates.io",
"name" : "tract-nnef"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0.22.0"
}, {
"fixed" : "0.22.2"
} ]
} ]
}, {
"package" : {
"ecosystem" : "crates.io",
"name" : "tract-nnef"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "0.21.16"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/sonos/tract/security/advisories/GHSA-x5mv-8wgw-29hg"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/sonos/tract"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-125", "CWE-190" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-06-18T15:05:23Z",
"nvd_published_at" : null
}
}