YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
GGUF KV Array Field Unbounded Length β CPU/Memory Amplification DoS
Status: Reported to Huntr (pending)
Package: gguf (PyPI) β official gguf-py reader/writer from ggml-org/llama.cpp
File / function: gguf/gguf_reader.py, GGUFReader._get_field_parts() (~line 247)
Class: CWE-834 (Excessive Iteration) / CWE-400 (Uncontrolled Resource Consumption), root cause CWE-20 (Improper Input Validation)
Severity: High β a few hundred bytes of crafted input can consume gigabytes of RAM and/or hang the parsing process for minutes with no exception raised, on any application that loads a GGUF file (the standard format for llama.cpp-compatible LLM weights, used across the local-inference ecosystem: llama.cpp, Ollama, LM Studio, ktransformers, and many others that reuse this same gguf library).
Summary
When GGUFReader parses a key/value metadata field of type ARRAY, it reads the declared element count (alen) as a raw uint64 straight from the file β with no upper bound check β and then uses it directly as a Python loop counter:
# gguf/gguf_reader.py, GGUFReader._get_field_parts()
alen = self._get(offs, np.uint64)
offs += int(alen.nbytes)
aparts: list[npt.NDArray[Any]] = [raw_itype, alen]
data_idxs: list[int] = []
for idx in range(alen[0]): # <-- attacker-controlled, unbounded
curr_size, curr_parts, curr_idxs, curr_types = self._get_field_parts(offs, raw_itype[0])
...
aparts += curr_parts
data_idxs += (idx + idxs_offs for idx in curr_idxs)
offs += curr_size
Each loop iteration performs a numpy.memmap slice read and appends to a Python list β regardless of whether real data still exists in the file at that offset. numpy.memmap slicing silently clips out-of-bounds reads instead of raising an exception, so the loop never fails fast; it just keeps running (and allocating) alen times, even when the file itself is only a few hundred bytes long.
A second, related trigger point exists at the top level: GGUFReader.__init__ reads kv_count (also an unchecked uint64) from the header and passes it straight into _build_fields(offs, kv_count), which loops for _ in range(count). On a file with no real KV entries past the header, this crashes with an uncaught IndexError instead of a clean parse error β same root cause (CWE-20), a different and even smaller trigger.
Proof of Concept
poc_gguf_array_length_dos.py:
- Writes a normal, well-formed 169-byte GGUF file containing one small KV array field (5 integers).
- Binary-patches only the 8-byte declared-length field of that array β nothing else in the file changes, and the file size stays 169 bytes.
- Parses the patched files with the real
GGUFReaderand measures wall-clock time and peak RSS.
pip install gguf numpy
python poc_gguf_array_length_dos.py
Measured results (169-byte file in all cases)
Declared alen |
Parse time | Peak RSS | Amplification (RSS bytes : file bytes) |
|---|---|---|---|
| 5 (legit baseline) | 0.002 s | 31.5 MB | 1 : 195,275 (interpreter baseline) |
| 100,000 | 0.76 s | 125 MB | 1 : 774,629 |
| 500,000 | 4.0 s | 499 MB | 1 : 3,097,957 |
| 1,000,000 | 8.2 s | 970 MB | 1 : 6,021,774 |
| 50,000,000 | did not finish (60 s timeout) | β | extrapolated: tens of GB / permanent hang |
RSS growth is close to linear (~1 KB per declared array element), so there is no ceiling short of process/OOM limits β a value near UINT64_MAX in the same 169-byte file is expected to hang or exhaust memory on essentially any machine.
Why this matters
- Trivial to trigger: a single 8-byte field in an otherwise completely valid, small GGUF file.
- No exception, no early warning: the process just keeps consuming memory/CPU; nothing in the calling application (llama.cpp bindings, ktransformers, model-hosting services that auto-preview/validate uploaded GGUF files, etc.) gets a chance to reject the file before resources are exhausted.
- Format is high-value and widely reused:
gguf-pyis the reference GGUF reader reused by many downstream inference projects, so a fix here benefits the whole ecosystem rather than one application.
Suggested fix
Validate alen (and kv_count, tensor_count) against a sane upper bound β and, more robustly, against the number of bytes actually remaining in the memory-mapped file β before using it as a loop count or allocation size, raising a clean ValueError immediately instead of iterating.
Relationship to prior findings
This is a distinct vulnerability class from other model-file-parsing findings in the same research line:
- Different from "CULA" (Convergent Untrusted-Length Allocation, CWE-789) findings in numpy/joblib/zarr/hickle/skops/anndata/fastparquet/keras β those are a single upfront
np.empty()-style allocation sized from an untrusted length; this is an unbounded Python loop with per-iteration allocation (CWE-834). - Different from a recursion-depth finding in
protobuf'sjson_format.ParseDict()(CWE-674) β this is iteration-count-based, not recursion-based.
Disclosure
Reported via Huntr under the GGUF / llama.cpp model-file-vulnerability scope. Please do not use this PoC against production systems you do not own or have explicit permission to test.