YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Unbounded bytearray(header_len) allocation in tensorizer TensorDeserializer β memory-exhaustion DoS loading an untrusted .tensors file
Target / scope
- Project / repo:
coreweave/tensorizer(https://github.com/coreweave/tensorizer) - Affected tool & version:
tensorizer2.12.1 (latest on PyPI at time of writing;pip install tensorizer==2.12.1) - Affected format: Tensorizer
.tensorsserialized tensor/model files - Component:
tensorizer/serialization.pyβ_TensorHeaderDeserializer.from_io() - Vulnerability class: CWE-789 Memory Allocation with Excessive Size Value / CWE-400 Uncontrolled Resource Consumption (Denial of Service)
- Attack vector: Maliciously crafted
.tensorsfile loaded byTensorDeserializer(the standard load path:TensorDeserializer(...),load_into_module,read_tensors)
SCOPE NOTE β please confirm before triage. This finding is in tensorizer itself, a standalone CoreWeave serialization library with its own GitHub repository and its own
.tensorsformat. It is not a finding in modelaudit / picklescan / modelscan / fickling, and.tensorsis not one of huntr's commonly-listed model formats (joblib/keras/gguf/safetensors/pickle/npy/h5/tflite/PMML/etc.). Ifcoreweave/tensorizeris not an in-scope huntr target, this report should be redirected (e.g. reported directly to CoreWeave under their security policy) rather than triaged as a huntr bounty. I am flagging this explicitly rather than over-claiming scope.
Severity (honest)
- Impact: Denial of Service only. No code execution, no file read/write, no information disclosure. The allocation is driven before any tensor data is interpreted, so it does not yield an OOB read/write primitive β it is purely resource exhaustion.
- Realistic dollar tier: DoS, low tier (β up to $1.5k equivalent) under the standard ladder (RCE > file-access > DoS).
.tensorsis not in the $4k format set, and even if it were, this is a pure DoS, not RCE/file-access. Treat as a Low/Medium report. I am deliberately not inflating this to a high-value class. - CVSS-ish framing: AV:N (file is typically fetched from a remote/HTTP/S3/Redis source β tensorizer's primary advertised use case) / AC:L / PR:N / UI:R (a load must be triggered) / availability-only. Roughly CVSS 6.5 (Medium) if the loader runs in a service that ingests third-party models; lower if loads are always operator-initiated on trusted files.
Summary
TensorDeserializer parses each tensor's on-disk header by first reading an 8-byte little-endian uint64 length prefix straight out of the file, then eagerly allocating a bytearray of exactly that many bytes β before validating it against the bytes actually remaining in the stream, against the file size, or against any sane upper bound. An attacker who controls the .tensors file sets this header_len to an arbitrary 64-bit value. A file of only a few hundred KB can therefore force:
- an instant
MemoryError(e.g.header_len = 2**47= 128 TiB β the allocation is refused), crashing the loading process; or - a real multi-gigabyte commit (e.g.
header_len = 8 GiB) that is allocated and zero-filled, with an amplification factor of ~33,000x (256 KB file β 8 GiB RSS). Tuned to just under host RAM, this drives the machine into swap-thrash / the Linux OOM killer.
Because tensorizer explicitly markets .tensors as a fast, safe alternative to pickle that loads models directly "from HTTP/HTTPS, Redis, and S3 endpoints" (per its own package README), a malicious file pulled from an untrusted model source crashing or OOM-killing the loader is a realistic availability attack.
Root cause
tensorizer/serialization.py, _TensorHeaderDeserializer.from_io() (line numbers from the 2.12.1 wheel):
604: header_len_segment: ClassVar[struct.Struct] = struct.Struct("<Q")
...
641: header_len_bytes = reader.read(cls.header_len_segment.size)
642: offset = cls.header_len_segment.size
643: header_len: int = cls.header_len_segment.unpack(header_len_bytes)[0] # attacker-controlled uint64 from FILE
644: if header_len == 0:
645: return None
646: buffer = bytearray(header_len) # <-- eager, UNBOUNDED allocation
647: buffer[:offset] = header_len_bytes
648: with memoryview(buffer) as mv:
649: reader.readinto(mv[offset:]) # short read silently leaves the rest zero-filled
header_len comes directly from untrusted file bytes. There is:
- no comparison to the remaining stream length / file size,
- no maximum-header-size constant,
- no incremental/chunked read.
bytearray(header_len) commits and zeroes the full requested size up front. The subsequent readinto does not protect anything: if the file is short, it simply does a partial read and leaves the remainder of the giant buffer zeroed (so a small file with a large header_len still costs the full allocation).
The same unbounded-from-uint64 pattern is reachable on the normal load path: TensorDeserializer.__init__ β header iteration β _TensorHeaderDeserializer.from_io() for each tensor described in the file metadata.
Proof of concept
Build: start from a valid single-tensor file produced by the real TensorSerializer, then surgically overwrite only the per-tensor header's 8-byte <Q length prefix. (The PoC locates that prefix via the metadata QQQ header_offset field β no guessing.) Everything else in the file stays valid, so the file is accepted and parsed as normal until the allocation.
Assertion: loading the crafted file with a normal TensorDeserializer(path, device="cpu") and iterating its keys must either raise MemoryError or commit multi-GB RSS from a sub-MB file.
Captured output (reproduce.py, tensorizer 2.12.1, Python 3.12.10, 16 GB RAM host):
tensorizer 2.12.1
python 3.12.10
========================================================================
[marker] wrote benign marker: .../Temp/tensorizer_dos_marker.txt
------------------------------------------------------------------------
[base ] unmodified valid.tensors loads in 0.001s (sanity OK)
------------------------------------------------------------------------
[craft ] evil_hdrlen_128tib.tensors: size=262315B header_len 74 -> 140737488355328 (0x800000000000)
[load 1] header_len=2**47 (128 TiB) -> MemoryError in 0.03s
------------------------------------------------------------------------
[craft ] evil_hdrlen_8gib.tensors: size=262315B header_len 74 -> 8589934592 (0x200000000)
[load 2] header_len=8 GiB (256 KB file) -> completed in 7.03s, RSS +8590 MB
------------------------------------------------------------------------
RESULT: DoS CONFIRMED
128 TiB header_len -> MemoryError in 0.03s (allocation refused)
8 GiB header_len -> committed +8590 MB RSS from a 256 KB file
Interpretation:
- Case 1 (
header_len = 2**47): a 256 KB file makes the loader request 128 TiB; CPython refuses and raisesMemoryError, terminating the load. Unconditional crash, no large RAM needed on the victim. - Case 2 (
header_len = 8 GiB): the same 256 KB file makes the loader actually commit and zero-fill 8 GiB (+8590 MB RSS measured) in ~7 s. Amplification β 33,000x. Set just below victim RAM, this thrashes / triggers the OOM killer.
Artifacts in this directory:
reproduce.pyβ self-contained, portable (Linux/Windows viatempfile.gettempdir()) reviewer script. Writes a benign/tmpmarker, builds the valid file, crafts both malicious files, loads them, and asserts the DoS.poc_header_len_dos.pyβ the original development PoC (same logic, includes a clean baseline + RSS instrumentation).evil_hdrlen_128tib.tensors,evil_hdrlen_8gib.tensorsβ pre-crafted malicious files (256 KB each). Load either withTensorDeserializerto reproduce directly without rebuilding.
Impact / threat model
tensorizer's stated purpose is fast model loading from remote, potentially untrusted sources (HTTP/HTTPS, S3, Redis), and it is positioned as a safe pickle alternative. Realistic scenarios:
- A model-serving / inference platform (e.g. an autoscaling vLLM-style fleet, which is exactly tensorizer's design target) pulls a
.tensorsartifact by name/URL from object storage or a model hub. A single poisoned artifact OOM-kills the worker on load β repeatable, cheap (the malicious file is tiny), and pre-authentication from the worker's perspective. - A multi-tenant or community model registry serving
.tensorsfiles: one malicious upload crashes every consumer that loads it. - CI / batch pipelines that deserialize third-party
.tensorscheckpoints.
Severity is bounded to availability: no RCE, no data exfiltration. But it is a reliable, low-cost crash/OOM of any process that loads attacker-influenced .tensors data, which contradicts the format's "safe to load untrusted" positioning.
Suggested remediation
Bound header_len before allocating, and avoid committing the full buffer up front:
- Sanity cap: reject
header_lenlarger than a sane maximum header size (headers are tiny β a few KB; even a generous cap of, say, a few MB is far above any legitimate header). Raise a clear deserialization error instead of allocating. - Bound against the stream: when the total file size is known (the file/seekable stream case, which is the common path), require
header_offset + header_len <= file_sizebefore allocating. - Defensive read: read the header incrementally /
readintoagainst a right-sized buffer derived from validated bounds, rather thanbytearray(attacker_uint64).
Example minimal fix:
MAX_HEADER_LEN = 64 * 1024 * 1024 # generous upper bound; real headers are KB-scale
header_len = cls.header_len_segment.unpack(header_len_bytes)[0]
if header_len == 0:
return None
if header_len < cls.header_len_segment.size or header_len > MAX_HEADER_LEN:
raise ValueError(f"corrupt or malicious .tensors header_len: {header_len}")
buffer = bytearray(header_len)
(The same bounding discipline should be applied to any other length/size field in the header and metadata that is read as a width-N integer and used to size an allocation or a read.)
Dedupe / prior-art note (honest)
- I could not find a published CVE / GHSA / huntr report describing this specific unbounded
bytearray(header_len)allocation intensorizer. If CoreWeave or huntr has an existing internal/duplicate report for unbounded header/length allocation in.tensorsparsing, treat this as a duplicate. - This is the generic "trust a length field from an untrusted file, allocate it eagerly" bug class (CWE-789). The novelty claim is only that it is present and weaponizable in tensorizer 2.12.1's
.tensorsheader parser on the default load path β not that the bug class is new. - See the SCOPE NOTE above: confirm
coreweave/tensorizeris an in-scope target before triaging as a paid huntr finding; otherwise this belongs upstream with CoreWeave directly.