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.

Unvalidated ARRAY element count in gguf-py GGUFReader β†’ unbounded allocation / non-terminating loop (memory-exhaustion DoS)

Target: gguf (gguf-py, the llama.cpp Python GGUF library), PyPI package gguf 0.19.0 Vulnerable file: gguf/gguf_reader.py β€” GGUFReader._get_field_parts() (ARRAY branch) and GGUFReader._get() Class: CWE-770 (Allocation of Resources Without Limits or Throttling) / CWE-834 (Excessive Iteration) Impact: Denial of service (unbounded heap allocation β†’ OOM / process kill; without a memory cap, host memory exhaustion and an effectively non-terminating loop) triggered at file-open time, before any tensor data is read. Trigger: Opening/parsing an untrusted .gguf file with gguf.GGUFReader(path).

Verified against real PyPI gguf 0.19.0, numpy 2.5.1, CPython 3.13 (venv /home/kali/hunt-workspace/gguf-3rdbug-venv).


Root cause

When GGUFReader parses a metadata key/value whose value type is ARRAY (type id 9), it reads an attacker-controlled 64-bit element count and loops that many times, reading one element per iteration. The count is never validated against the number of bytes actually remaining in the file.

gguf/gguf_reader.py (v0.19.0), the ARRAY branch of _get_field_parts():

221:    def _get_field_parts(
222:        self, orig_offs: int, raw_type: int,
223:    ) -> tuple[int, list[npt.NDArray[Any]], list[int], list[GGUFValueType]]:
...
238:        # Handle arrays.
239:        if gtype == GGUFValueType.ARRAY:
240:            raw_itype = self._get(offs, np.uint32)
241:            offs += int(raw_itype.nbytes)
242:            alen = self._get(offs, np.uint64)          # <-- attacker-controlled 64-bit count
243:            offs += int(alen.nbytes)
244:            aparts: list[npt.NDArray[Any]] = [raw_itype, alen]
245:            data_idxs: list[int] = []
246:            # FIXME: Handle multi-dimensional arrays properly instead of flattening
247:            for idx in range(alen[0]):                 # <-- iterates count times, count never validated
248:                curr_size, curr_parts, curr_idxs, curr_types = self._get_field_parts(offs, raw_itype[0])
249:                if idx == 0:
250:                    types += curr_types

The per-element read goes through _get():

197:    def _get(
198:        self, offset: int, dtype: npt.DTypeLike, count: int = 1, override_order: ... = None,
199:    ) -> npt.NDArray[Any]:
200:        count = int(count)
201:        itemsize = int(np.empty([], dtype = dtype).itemsize)
202:        end_offs = offset + itemsize * count
203:        arr = self.data[offset:end_offs].view(dtype=dtype)[:count]   # empty slice past EOF -> nbytes == 0, NO exception
204:        return arr.view(...)

The bug is the interaction of two facts:

  1. The count is unbounded and unvalidated. alen[0] is a raw 64-bit value straight from the file (e.g. 9223372036854775807). Nothing checks it against len(self.data) - offs.
  2. Reading past EOF does not raise β€” it returns an empty array. self.data is an np.memmap; slicing past the end yields an empty (nbytes == 0) slice rather than an error. For a scalar element type the recursive _get_field_parts() call therefore returns curr_size == 0, so offs += curr_size (line ~272) advances by zero.

Consequently, once the read offset reaches EOF the loop makes no forward progress yet still runs for all alen[0] iterations, and on every iteration it appends a fresh numpy array entry to the aparts list (aparts += curr_parts). The result is unbounded heap growth and an effectively infinite loop, entirely inside GGUFReader.__init__ (__init__ β†’ _build_fields β†’ _get_field_parts), i.e. at file-open time before any tensor payload is touched.

PoC

gen_arraylen.py writes a 49-byte GGUF v3 file:

'GGUF' | version=3 | tensor_count=0 | kv_count=1
KV: key='x' | value_type=ARRAY(9) | array_element_type=INT8(1) | array_length=<count>
(no element bytes β€” the file ends right after the 8-byte count)

Reproduce:

python gen_arraylen.py huge.gguf 9223372036854775807
python -c "import gguf; gguf.GGUFReader('huge.gguf')"     # hangs, RSS climbs without bound

moderate.gguf uses a finite count of 20,000,000 in an identical 49-byte file and still OOMs β€” a 49-byte input driving multi-GB allocation (severe amplification), so the DoS does not depend on the count being astronomically large.

Captured evidence (real execution, gguf 0.19.0 / numpy 2.5.1 / CPython 3.13)

Run under a 2 GB virtual-memory cap (ulimit -v 2000000) so the run terminates instead of taking down the host:

calling gguf.GGUFReader('huge.gguf') on a 49-byte file...
[t=  2.0s] maxRSS=601 MB (still inside GGUFReader, not returned)
[t=  4.0s] maxRSS=1119 MB (still inside GGUFReader, not returned)
Traceback (most recent call last):
  File "/home/kali/hunt-workspace/gguf-3rdbug/run_huge.py", line 10, in <module>
    gguf.GGUFReader('huge.gguf')
  File ".../gguf/gguf_reader.py", line 169, in __init__
  File ".../gguf/gguf_reader.py", line 298, in _build_fields
  File ".../gguf/gguf_reader.py", line 248, in _get_field_parts
  File ".../gguf/gguf_reader.py", line 236, in _get_field_parts
  File ".../gguf/gguf_reader.py", line 203, in _get
object type name: MemoryError
object repr     : MemoryError()

RSS grew from ~0 to >1 GB in ~4 seconds while still inside GGUFReader.__init__ (the constructor never returns), then the process was killed with MemoryError raised at gguf_reader.py:248 (the array loop) β†’ :236 (scalar _get_field_parts) β†’ :203 (_get). Without a cap this exhausts all host memory and/or hangs indefinitely.

Negative control (proves the crash is the unvalidated count, not merely opening an array)

good.gguf β€” a well-formed INT8 array with count=3 and 3 real element bytes:

parsed OK in 0.0007s
array contents=[1, 2, 3]

A correctly-truncated/valid array parses in ~1 ms and returns [1, 2, 3]. The fault is specifically the declared count exceeding the element bytes actually present, combined with the past-EOF read returning an empty (non-raising) slice β€” not the act of parsing an array.

Suggested fix

Before the loop, validate alen[0] against the bytes remaining (len(self.data) - offs) given the element type's itemsize, and/or bail out of the loop when a per-element read returns nbytes == 0 (no forward progress) instead of silently continuing.

Dedup note

  • Not a known CVE for gguf-py as of packaging (2026-07-16).
  • Distinct from the gguf nested-array recursion bug (separate finding huntr-poc-gguf-nested-array-recursion): that one requires element type == ARRAY(9) so _get_field_parts() self-recurses and raises RecursionError by exhausting the Python call stack. This bug uses a scalar element type (INT8) plus a huge/large count with truncated/absent element bytes, and fails via unbounded heap growth / a non-terminating loop (MemoryError / hang), not recursion depth. Different element type, different failure mode, different faulty line.
  • Distinct from the transformers GGUF O(NΒ²) DoS (different code path; quadratic time, not unbounded allocation), the tensor-shape division-by-zero, and the vocab type-confusion findings.

Files

  • gen_arraylen.py β€” builds the malicious 49-byte file (huge / moderate)
  • huge.gguf β€” count = 9223372036854775807 (2^63 βˆ’ 1)
  • moderate.gguf β€” finite count = 20,000,000 (still OOMs)
  • gen_good.py, good.gguf β€” negative control (valid count=3 array with real bytes)
  • run_huge.py β€” RSS watcher + reader invocation used to capture the evidence above
Downloads last month
-
GGUF
Model size
0 params
Architecture
Hardware compatibility
Log In to add your hardware

We're not able to determine the quantization variants.

Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support