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:
- The count is unbounded and unvalidated.
alen[0]is a raw 64-bit value straight from the file (e.g.9223372036854775807). Nothing checks it againstlen(self.data) - offs. - Reading past EOF does not raise β it returns an empty array.
self.datais annp.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 returnscurr_size == 0, sooffs += 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 raisesRecursionErrorby 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
- -
We're not able to determine the quantization variants.