tensorizer-dos-poc / README.md
alexschose's picture
PoC: tensorizer .tensors deserializer DoS (unbounded alloc + int overflow)
ec8e6b0 verified
|
Raw
History Blame Contribute Delete
3.38 kB
---
license: mit
---
# tensorizer `.tensors` deserializer β€” DoS PoC (unbounded allocation + integer overflow)
Proof-of-concept malicious model files for two denial-of-service flaws in the
[`tensorizer`](https://pypi.org/project/tensorizer/) `.tensors` binary deserializer
(`tensorizer==2.12.1`, the CoreWeave/vLLM fast model-load format).
Both fire at **`TensorDeserializer(...)` construction** (default `lazy_load=False`), i.e. merely
opening an attacker-supplied `.tensors` file β€” no `.load()` call required.
## Affected
- Package: `tensorizer==2.12.1` (PyPI latest, 2026-06-20); HEAD `652d3c016c84836bba97153e108821c11428ac40`.
- File: `tensorizer/serialization.py`.
## Reproduce
```bash
python -m venv venv && . venv/bin/activate
pip install tensorizer==2.12.1
# Finding A β€” unbounded allocation (CWE-789): 73-byte file declares a 256 TiB metadata blob
python -c "import tensorizer; tensorizer.TensorDeserializer('poc_a_meta_281tib.tensors')"
# -> MemoryError at tensorizer/serialization.py:841 (encoded_metadata = reader.read(total_len))
# Finding B β€” numpy.prod int64 silent overflow (CWE-190): meta-tensor shape wraps the declared size
python -c "import tensorizer; tensorizer.TensorDeserializer('poc_b_neg_assert.tensors')"
# -> AssertionError at tensorizer/serialization.py:3137 (assert is_meta or needed_buffer_size == header.data_length)
# Control β€” a well-formed file loads cleanly (1 tensor)
python -c "import tensorizer; print(len(list(tensorizer.TensorDeserializer('valid.tensors').keys())), 'tensor')"
```
## Files
| File | Bytes | Trigger |
|--|--|--|
| `poc_a_meta_281tib.tensors` | 73 | Finding A β€” metadata `total_len` = `0xFFFFFFFFFFFF` (256 TiB) β†’ `MemoryError` @ `serialization.py:841` (main thread, no `try`). Amplification β‰ˆ 3.86e12Γ—. |
| `poc_a_header_1gib.tensors` | 262 KB | Finding A β€” per-tensor `header_len` = 1 GiB β†’ `bytearray(header_len)` allocates 1 GiB with no bound (process RSS 176 β†’ 1200 MiB), proving the missing length check. |
| `poc_b_neg_assert.tensors` | 262 KB | Finding B β€” v5 long-shape meta-tensor, `numpy.prod((2**62, 2))` wraps int64 to `-2**63` β†’ `AssertionError` @ `serialization.py:3137`. |
| `valid.tensors` | 262 KB | Control β€” well-formed single-tensor file (loads clean). |
| `generate_poc_a.py`, `generate_poc_b.py` | β€” | Scripts that synthesize the files from a valid baseline. |
## Root cause (summary)
- **A:** file-declared length prefixes (`_MetadataDeserializer.from_io` `total_len` @ `:841`; `_TensorHeaderDeserializer.from_io` `header_len` @ `:646`) drive allocations with no upper bound and no check against remaining file size.
- **B:** `TensorEntry.deserialized_length` (`:231`) computes `num_elements = numpy.prod(self.shape)` in fixed-width int64, which silently wraps; the wrapped size is summed at `:1964` and reused as the buffer size, defeating the `:3137` assert.
Memory-corruption was investigated and **refuted** β€” numpy's `PyArray_NewFromDescr` performs overflow-safe size
arithmetic and a hard buffer-length check, so the impact is process-termination DoS, not OOB read/write.
## Proposed fix
- **A:** bound each file-declared length against the remaining input size (or a configurable cap) before allocating.
- **B:** compute the element count with arbitrary-precision Python ints (`math.prod`) and explicitly overflow-check before allocating.