Uncontrolled recursion (CWE-674) in gguf-py GGUFReader._get_field_parts via nested-array KV metadata β uncaught RecursionError DoS
Severity: Medium (availability / denial-of-service on model load)
Category: Malformed model file β GGUF ($4k format), metadata KV parser
Weakness: CWE-674 Uncontrolled Recursion β uncaught RecursionError
Affected software
- Package:
gguf(gguf-py, part of theggml-org/llama.cppproject) β version 0.19.0 (pip) - File:
gguf/gguf_reader.py, functionGGUFReader._get_field_parts()(lines 221β257) - Tested environment: gguf 0.19.0, numpy 2.5.1, CPython 3.13.12, default
sys.recursionlimit= 1000, Linux x86-64
Summary
GGUFReader._get_field_parts() parses an ARRAY-typed metadata KV value by reading the
array's element type (raw_itype, a uint32 read straight from the file) and its length,
then recursing into _get_field_parts(offs, raw_itype[0]) for every element. The element
type is itself attacker-controlled and may equal GGUFValueType.ARRAY (value 9), producing
self-recursion with no nesting-depth limit and no validation.
Recursion depth equals the file's array-nesting depth, and each level costs only 12 bytes
(u32 element-type = ARRAY + u64 count = 1). Once depth exceeds Python's recursion limit
(~1000), an uncaught RecursionError is raised and propagates out of GGUFReader.__init__
(via _build_fields β _get_field_parts), crashing any caller that opens the file. A ~60 KB
file (depth 5000) is enough to guarantee the crash regardless of a raised recursion limit.
Root cause
gguf/gguf_reader.py, lines 238β255 (the # Handle arrays. branch):
# Handle arrays.
if gtype == GGUFValueType.ARRAY:
raw_itype = self._get(offs, np.uint32) # element type β attacker-controlled
offs += int(raw_itype.nbytes)
alen = self._get(offs, np.uint64) # element count β attacker-controlled
offs += int(alen.nbytes)
aparts: list[npt.NDArray[Any]] = [raw_itype, alen]
data_idxs: list[int] = []
# FIXME: Handle multi-dimensional arrays properly instead of flattening
for idx in range(alen[0]):
curr_size, curr_parts, curr_idxs, curr_types = self._get_field_parts(offs, raw_itype[0])
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# raw_itype[0] can be GGUFValueType.ARRAY (9) β unbounded self-recursion
if idx == 0:
types += curr_types
idxs_offs = len(aparts)
aparts += curr_parts
data_idxs += (idx + idxs_offs for idx in curr_idxs)
offs += curr_size
return offs - orig_offs, aparts, data_idxs, types
The developer-visible comment
# FIXME: Handle multi-dimensional arrays properly instead of flattening sits directly above
the unguarded recursive loop. Because raw_itype[0] is read verbatim from the file and passed
back into _get_field_parts as the element type, an array whose element type is itself ARRAY
recurses one level deeper per nesting, with no depth accounting.
Reachability (real callers)
- transformers β
transformers/modeling_gguf_pytorch_utils.py:599reader = GGUFReader(gguf_checkpoint_path), reached via the documented workflowAutoModel.from_pretrained(..., gguf_file="evil.gguf")/AutoModelForCausalLM.from_pretrained(..., gguf_file=...). - Direct API β any code doing
gguf.GGUFReader("model.gguf"). - Tooling β
gguf-dump.py/gguf_dump.pyshipped with the package.
The crash occurs inside GGUFReader.__init__, i.e. at file-open time, before any tensor data
is touched β so it triggers on the very first act of inspecting an untrusted GGUF file.
Proof of Concept
gen.py (included) writes a GGUF v3 file: header
magic 'GGUF' + version 3 + tensor_count 0 + kv_count 1, then one KV: key 'x',
value_type = ARRAY (9), followed by depth repetitions of [u32(type=ARRAY) + u64(count=1)],
terminated by [u32(type=UINT8) + u64(count=0)].
python gen.py nested 5000 # writes nested.gguf (~60 KB)
python -c "import gguf; gguf.GGUFReader('nested.gguf')"
Captured evidence (verbatim)
$ ../gguf-venv/bin/python gen.py nested 5000
wrote nested.gguf depth 5000
$ ls -l nested.gguf
-rw-rw-r-- 1 kali kali 60049 ... nested.gguf
$ ../gguf-venv/bin/python -c "import gguf; gguf.GGUFReader('nested.gguf')"
Traceback (most recent call last):
File "<string>", line 1, in <module>
import gguf; gguf.GGUFReader('nested.gguf')
File ".../gguf/gguf_reader.py", line 169, in __init__
offs = self._build_fields(offs, kv_count)
File ".../gguf/gguf_reader.py", line 298, in _build_fields
field_size, field_parts, field_idxs, field_types = self._get_field_parts(offs, raw_kv_type[0])
File ".../gguf/gguf_reader.py", line 248, in _get_field_parts
curr_size, curr_parts, curr_idxs, curr_types = self._get_field_parts(offs, raw_itype[0])
[Previous line repeated 990 more times]
File ".../gguf/gguf_reader.py", line 240, in _get_field_parts
raw_itype = self._get(offs, np.uint32)
File ".../gguf/gguf_reader.py", line 203, in _get
arr = self.data[offset:end_offs].view(dtype=dtype)[:count]
File ".../numpy/_core/memmap.py", line 312, in __array_finalize__
if hasattr(obj, '_mmap') and np.may_share_memory(self, obj):
RecursionError: maximum recursion depth exceeded
EXIT=1
Negative controls
--- negative controls ---
loaded depth200 ok fields 4 # depth 200 (< recursionlimit) loads cleanly
NEG CONTROL good.gguf: loaded OK, x = [1, 2, 3] # well-formed array<uint8> [1,2,3] parses correctly
depth=200(below the recursion limit) β the same code path returns normally with 4 fields.- A well-formed single-level
array<uint8>value[1, 2, 3]loads and reads back as[1, 2, 3].
This isolates the defect to unbounded recursion depth, not to array parsing in general.
transformers reachability grep
--- transformers reachability grep ---
599: reader = GGUFReader(gguf_checkpoint_path)
Impact
An attacker who can get a victim to open an untrusted .gguf file (Hub download, model
conversion service, CI, inference server auto-loading community GGUFs) causes an uncaught
RecursionError that terminates the loader process. The file is tiny (~60 KB for depth 5000)
and cheap to craft; raising the Python recursion limit does not help because the depth is
attacker-controlled and unbounded. No tensor data required.
Suggested fix
Bound array nesting depth (e.g. pass and check a depth parameter, rejecting beyond a small
constant), and/or validate that an array's element type is not itself ARRAY unless an explicit
multi-dimensional format is supported. Convert the unhandled deep-nesting case into a clean
ValueError (already the pattern for unknown field types at line 257) rather than letting a
RecursionError escape.
Dedup note
- Distinct from public CVEs. Known GGUF-adjacent CVEs are in different components/mechanisms:
CVE-2025-2099 / CVE-2025-6921 (transformers Marian/EnglishNormalizer ReDoS β different component),
CVE-2026-5760 (SGLang GGUF SSTI β different library), CVE-2025-66960 (Ollama
readGGUFV1Stringβ different library, different mechanism). None targets gguf-py_get_field_partsrecursion. - Distinct from our prior gguf/ggml findings. This is the "R2 nested-array recursion DoS"
in the gguf-py parser β separate from R1 (gguf-py array-length OOM,
gguf-slen-oom-poc), R3 (llama.cppconvert_llama_ggml_to_gguf.pyint64-overflow,huntr-poc-ggml-oob), the transformersGGUFTokenizerSkeletonO(NΒ²) tokenizer DoS (huntr-poc-gguf-transformers-dos), the gguf vocab type-confusion, and the tensor-shape div-by-zero findings. Different mechanism (uncontrolled recursion in the KV metadata parser) and different sink (_get_field_partsself-recursion).
Files in this repo
gen.pyβ PoC generator (python gen.py nested <depth>; also contains thepoc_goodcontrol).nested.ggufβ depth-5000 crash artifact (~60 KB).good.ggufβ well-formed control (array<uint8> [1,2,3]).