CoolFace
Modelpublic

MBM7/ollama-gguf-tensor-aliasing-poc

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
Model Card

Ollama GGUF Parser — Tensor Offset Aliasing and Duplicate-Name Confusion

Status: Preparing for Huntr submission Target: ollama/ollamafs/gguf package (fs/gguf/gguf.go) Class: CWE-1284/CWE-20 (missing structural invariant) + CWE-706 (Use of Incorrectly-Resolved Name) Severity: Medium-High — silent model-integrity violation, not a crash.

Summary

Ollama's own Go GGUF parser (fs/gguf/gguf.go, the newer of two in-repo implementations, with explicit MaxStringLength/MaxArraySize/MaxTensorDims hardening already in place) has no validation that:

  1. 1.Different tensors' declared byte offsets don't overlap, or
  2. 2.Tensor names are unique.

Both gaps allow one tensor's real data to become silently unreachable — replaced, without any error, by a different tensor's data.

Bug 1 — Tensor Offset Aliasing

(*File).readTensor() reads each tensor's offset directly from the file with no check against other tensors' declared ranges:

go
offset, err := read[uint64](f)
...
ti := TensorInfo{
    Name:   name,
    Offset: offset,
    Shape:  shape,
    Type:   TensorType(type_),
}

Two tensors with different names, shapes, or types can be crafted to declare the same (or overlapping) offset. Confirmed with a 192-byte file: blk.0.attn_q.weight (real data 1,2,3,4) and blk.0.attn_k.weight (declared data 9.9,9.9,9.9,9.9, patched offset to alias the first tensor). Reading both via the public TensorReader() API returns identical bytes for both — attn_k.weight's real data is completely unreachable.

Bug 2 — Duplicate Tensor Name Confusion

Tensors are stored in a plain slice (tensors []*Tensor, via append), with no uniqueness check on Name. (*File).TensorInfo(name) resolves lookups via slices.IndexFunc, which always returns the first match:

go
func (f *File) TensorInfo(name string) TensorInfo {
    if index := slices.IndexFunc(f.tensors.values, func(t TensorInfo) bool {
        return t.Name == name
    }); ...
}

Two tensors with the same name but different, non-overlapping, real offsets and real data are accepted with no error. Confirmed: duplicate_name (offset 0, data 1,2,3,4) and duplicate_name (offset 32, data 9,9,9,9) — TensorReader("duplicate_name") always returns the first tensor's bytes, for both logical entries. The second tensor's real data is unreachable via any name-based access, which is the normal way this API is used.

Notably: neither the Python reference implementation (gguf-py) nor the native C++ reference implementation (ggml-org/llama.cpp) protects against offset-aliasing either — but both of those explicitly reject duplicate tensor names outright. Ollama's Go implementation validates neither.

Proof of Concept

  • poc_ollama_gguf_aliasing.py — builds the two crafted .gguf files (pure stdlib + numpy, no dependency on any GGUF writer library).
  • harness_main.go — a small Go program that opens a given .gguf file with the real fs/gguf package and prints, for each tensor, its declared offset and the actual bytes read via the public API — showing the aliasing/confusion directly. Includes setup instructions for both a standard Go >= 1.26 environment and a workaround for older toolchains (the package has zero external dependencies, so it can be tested via an isolated copy with an older go directive without modifying any logic).
bash
python3 poc_ollama_gguf_aliasing.py
go run harness_main.go go_poc_offset_alias.gguf
go run harness_main.go go_poc_duplicate_name.gguf

Results

=== go_poc_offset_alias.gguf ===
[0] name="blk.0.attn_q.weight" declared_offset=0 actual_bytes_read=0000803f000000400000404000008040
[1] name="blk.0.attn_k.weight" declared_offset=0 actual_bytes_read=0000803f000000400000404000008040

(identical bytes for both, despite different names/declared shapes)

=== go_poc_duplicate_name.gguf ===
[0] name="duplicate_name" declared_offset=0  actual_bytes_read=0000803f000000400000404000008040
[1] name="duplicate_name" declared_offset=32 actual_bytes_read=0000803f000000400000404000008040

(second entry's declared offset is 32, but reading by name still returns the first tensor's bytes)

Impact

A GGUF file loaded by Ollama can declare a full, plausible-looking set of named weight tensors while several of them silently alias or shadow each other's data — the file "loads successfully" with no error, but some declared tensors' real weight data is unreachable. This can be used to disguise an incomplete, corrupted, or backdoored model as a complete one, and can cause silently incorrect inference results with no diagnostic signal.

Suggested fix

In readTensor() (or wherever tensors are appended to f.tensors), track allocated [offset, offset+size) ranges and reject overlaps; separately, reject duplicate tensor names during parsing, matching the behavior already present in both the Python and C++ reference implementations.

Disclosure

Please do not use this PoC against production systems you do not own or have explicit permission to test.