CVE-2026-55093: tract-nnef Integer Overflow Vulnerability

MODERATE
Advisory Identifier: GHSA-x5mv-8wgw-29hg
CVSS 6.1
CWE-190 CWE-125
Published
Advisory Summary

CVE-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

CVE Identifier CVE-2026-55093
GHSA Identifier GHSA-x5mv-8wgw-29hg
Severity MODERATE
CVSS v3.1 Base Score 6.1
CWE Classification CWE-190: Integer Overflow or Wraparound
Affected Product tract-nnef
Package Ecosystem crates.io
Published Date
Last Updated

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).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability High (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.

tract-nnef fixed: 0.23.1
Upgrade tract-nnef to 0.23.1
tract-nnef fixed: 0.22.2
Upgrade tract-nnef to 0.22.2
tract-nnef fixed: 0.21.16
Upgrade tract-nnef to 0.21.16
Verify Direct and Transitive Dependencies

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

- **Component:** `tract-nnef` (`nnef/src/tensors.rs::read_tensor`) + `tract-data` (`data/src/tensor.rs`) - **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 - **Class:** CWE-190 (integer overflow) → CWE-125 (out-of-bounds read) - **Trigger:** loading a crafted NNEF model archive (`*.nnef.tgz` / `*.nnef.tar` / dir) via the public `tract_nnef::nnef().model_for_path` / `model_for_read` - **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. - **Severity:** Medium ## Summary `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. ## Root cause `nnef/src/tensors.rs`, `read_tensor`: ``` let shape: TVec<usize> = header.dims[0..header.rank as usize].iter().map(|d| *d as _).collect(); let len = shape.iter().product::<usize>(); // (1) unchecked, wraps ... } else if header.bits_per_item != u32::MAX && len * (header.bits_per_item as usize / 8) != header.data_size_bytes as usize // (2) wrapped == u32 { bail!(...); } ... let mut tensor = unsafe { Tensor::uninitialized_dt(dt, &shape)? }; // (3) alloc off the same wrapped product ... reader.read_exact(plain.as_bytes_mut())?; // storage-bounded read, no overflow here Ok(tensor) ``` `data/src/tensor.rs`, `uninitialized_aligned_dt`: ``` let bytes = shape.iter().cloned().product::<usize>() * dt.size_of(); // (3) wraps to the same small value let storage = ... Blob::new_for_size_and_align(bytes, alignment) ...; ... tensor.update_strides_and_len(); // len = product(shape), wraps, no clamp ``` The 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. Corruption sink — `data/src/tensor.rs::as_slice_unchecked` (and `data/src/tensor/plain_view.rs::as_slice_unchecked`): ``` if self.storage.byte_len() == 0 { &[] } else { std::slice::from_raw_parts(self.as_ptr_unchecked(), self.len()) } // len = 2^61 over a 56-byte alloc ``` The only guard is `byte_len() == 0`. A small **non-zero** allocation defeats it and yields an unsound oversized slice. ## Witness (F64) ``` dims = [33955849, 7005787, 359, 3, 3, 3] (rank 6, each <= u32::MAX) product(shape)= 2_305_843_009_213_693_959 = 2^61 + 7 bits_per_item = 64 (F64), item_type = 0, item_type_vendor = 0 data_size_bytes = 56 # == (2^61+7)*8 mod 2^64 ``` - `len * (bits/8) mod 2^64 = (2^61+7)*8 mod 2^64 = 56 == data_size_bytes` → consistency check passes. - allocation = `(2^61+7)*8 mod 2^64 = 56` bytes (7 × F64). - reported `len` = `2^61+7` elements. Only 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). ## Reachability (load-time, public API) ``` nnef().model_for_read(tar) -> proto_model_for_read nnef/src/framework.rs:303 -> DatLoader.try_load (any *.dat) nnef/src/resource.rs:97 (default loader, framework.rs:33) -> read_tensor -> Ok(Tensor{len=2^61+7, storage=56B}) nnef/src/tensors.rs:61 -> into_typed_model -> variable() fragment nnef/src/ops/nnef/deser.rs:74 ensure!(tensor.shape() == &*shape) deser.rs:122 (attacker matches shape in graph.nnef -> passes) -> Const::new -> wire_node core/src/model/typed.rs:67 -> Const::output_facts core/src/ops/konst.rs:54 -> TypedFact::try_from core/src/model/fact.rs:459 -> Tensor::as_uniform -> is_uniform_t::<f64> data/src/tensor.rs:1099 -> as_slice_unchecked::<f64> data/src/tensor.rs:1044 -> from_raw_parts(ptr, 2^61+7) over 56-byte buffer -> OOB READ ``` No 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). ## Execution (proof of concept) Reproduced against the crate at the affected revision, `--release`, x86_64-linux. Three scenarios: 1. **Direct `read_tensor`** — feed the crafted 128-byte header + 56-byte payload: - `read_tensor -> Ok`, `shape=[33955849,7005787,359,3,3,3]`, `len()=2305843009213693959`, `as_bytes().len()=56`, `as_slice::<f64>().len()=2305843009213693959`. - `s[7]` (first element past the 56-byte allocation) returns `0x0000000000000041` → **heap OOB read** (adjacent-heap disclosure). - `s[1<<40]` → **SIGSEGV** (signal 11). 2. **Public load API** — build a malicious `.nnef.tar` (`graph.nnef` with `variable(label='weights', shape=[...])` + `weights.dat`) and call `nnef().model_for_read()`: - 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). 3. **Optimized graph** — same archive but the const is consumed (`output = mul(weights, weights)`), then `into_optimized` / `run`: - **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). Runnable PoC sources are available to the maintainers on request. ## Detection - **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`. - **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`. - **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. - **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. ## Mitigation (suggested fix) In `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`): ``` let len = shape.iter().try_fold(1usize, |a, &d| a.checked_mul(d)) .context("tensor shape product overflows usize")?; let byte_size = len.checked_mul(dt.size_of()) .context("tensor byte size overflows usize")?; ensure!(byte_size == header.data_size_bytes as usize, "shape/len vs data_size_bytes mismatch"); ``` Defense 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`. Mapping: CWE-190, CWE-125; mitigations align with input validation (OWASP ASVS V5) and safe integer handling (CERT INT32-C analogue). ## Prior art / why this is not already fixed - `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. - 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. - 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. --- Reported by: s1ko (s1ko@riseup.net · github.com/s1ko)

CVE-2026-55093 Affected tract-nnef Versions

3 component(s)
Fixed Versions:
0.23.10.22.20.21.16
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?
CVE-2026-55093 is a security vulnerability in tract-nnef involving integer overflow in NNEF `.dat` tensor parser yields an out-of-bounds read on model load under CWE-190: Integer Overflow or Wraparound.
Which tract-nnef versions are affected?
Affected versions include releases prior to 0.23.1, 0.22.2, 0.21.16 across affected packages: tract-nnef.
What version fixes CVE-2026-55093?
Upgrading to 0.23.1, 0.22.2, 0.21.16 (or the latest patched release for your respective branch) resolves the vulnerability.
Is tract-nnef affected?
Yes, tract-nnef is affected in releases prior to 0.23.1, 0.22.2, 0.21.16. Upgrade your dependency to 0.23.1, 0.22.2, 0.21.16 or later.
What is the CVSS score of CVE-2026-55093?
CVE-2026-55093 has a CVSS v3.1 base score of 6.1 (MODERATE) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H.
What CWE is associated with CVE-2026-55093?
CVE-2026-55093 is classified under CWE-190: Integer Overflow or Wraparound (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.).
How can I check crates.io dependencies for CVE-2026-55093?
Run the dependency tree command: 'inspect dependencies for tract-nnef'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "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
  }
}