File size: 5,024 Bytes
27bfb5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# GGUF Nested Array (ARRAY-of-ARRAY) Uncontrolled Recursion β€” RecursionError DoS

**Status:** Preparing for Huntr submission
**Package:** [`gguf`](https://pypi.org/project/gguf/) (PyPI) β€” official gguf-py from [ggml-org/llama.cpp](https://github.com/ggml-org/llama.cpp)
**File / function:** `gguf/gguf_reader.py`, `GGUFReader._get_field_parts()`
**Class:** CWE-674 (Uncontrolled Recursion)
**Severity:** Medium β€” an uncaught, unhandled exception crashes any application calling `GGUFReader()` on a ~12KB crafted file, with no exception type most code would think to catch.

## Summary

When a GGUF key/value metadata field has type `ARRAY`, `GGUFReader._get_field_parts()` reads the array's *element* type and recursively calls itself to parse each element:

```python
if gtype == GGUFValueType.ARRAY:
    raw_itype = self._get(offs, np.uint32)
    ...
    for idx in range(alen[0]):
        curr_size, curr_parts, curr_idxs, curr_types = self._get_field_parts(offs, raw_itype[0])
```

There is no check that the element type (`raw_itype`) isn't itself `ARRAY`, and no recursion depth limit. A GGUF file can declare an array whose elements are arrays, whose elements are arrays, nested as deep as the file's author chooses.

**The reference C++ implementation explicitly rejects this.** In `ggml/src/gguf.cpp`'s type-dispatch switch:

```c
case GGUF_TYPE_ARRAY:
default:
    {
        GGML_LOG_ERROR("%s: key '%s' has invalid GGUF type %d\n", ...);
        ok = false;
    } break;
```

Encountering `ARRAY` as an array's element type is treated as invalid input and cleanly rejected. The Python bindings have no equivalent check.

## Proof of Concept

`poc_gguf_nested_array_recursion.py` builds a series of small, otherwise-valid GGUF files with a single KV field nested to increasing depth, and shows the divergence:

```bash
pip install gguf numpy
python poc_gguf_nested_array_recursion.py [path to a compiled llama-gguf binary, optional]
```

### Results

| Nesting depth | File size | Python `gguf-py` | Native C++ reader |
|---:|---:|---|---|
| 1 | 70 bytes | accepted silently | **rejected cleanly** ("invalid GGUF type") |
| 10 | 178 bytes | accepted silently | (not tested, same as depth 1) |
| 100 | 1,258 bytes | accepted silently | β€” |
| 1,000 | 12,058 bytes | **`RecursionError: maximum recursion depth exceeded`** | β€” |
| 5,000 | 60,058 bytes | **`RecursionError: maximum recursion depth exceeded`** | β€” |

Even a *single* level of nesting is a genuine parity gap (Python accepts what the reference C++ implementation explicitly rejects as malformed), and by depth ~1000 β€” matching Python's default `sys.setrecursionlimit()` β€” it becomes an uncaught crash.

The `RecursionError` is not caught anywhere in `gguf-py`; it propagates all the way out of `GGUFReader.__init__()`:

```
Traceback (most recent call last):
  File "...", line 3, in <module>
  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]
  ...
RecursionError: maximum recursion depth exceeded
```

Any application calling `GGUFReader(path)` without a specific `except RecursionError` handler β€” unusual, since most file-parsing code anticipates `ValueError`/`OSError`, not `RecursionError` β€” crashes with an unhandled exception.

## Impact

Any service that loads untrusted or third-party GGUF files with `gguf-py` (model preview/validation services, conversion tooling, CI harnesses testing community-submitted models) can be crashed by a ~12KB file that otherwise looks like an ordinary, if oddly-structured, metadata field.

## Suggested fix

In `_get_field_parts()`, reject `ARRAY` as a valid array-element type the same way the C++ implementation does (this alone fixes the underlying parity gap and, as a side effect, removes the unbounded-recursion path entirely, since the recursion can only occur through nested arrays).

## Relationship to other reports

Same vulnerability *class* (CWE-674, Uncontrolled Recursion) as a separately-reported finding in `protobuf`'s `json_format.ParseDict()` β€” but a completely different library, format, and code path. Also distinct from this reporter's two other `gguf-py` findings: "KV Array Field Unbounded Length" (CWE-834, iteration-based, not recursion) and "Tensor Data Offset Aliasing" (CWE-1284, a data-integrity issue unrelated to metadata parsing). Also distinct from a separately and independently reported `n_dims`/`GGML_MAX_DIMS` parity gap (a different structural issue β€” per-tensor dimension count vs. this report's array *nesting*).

## Disclosure

Please do not use this PoC against production systems you do not own or have explicit permission to test.