fix(ai): handle malformed GGUF headers without panicking (#5345)

- pre-scan the GGUF metadata KV section and reject values gguf-parser-go would panic on, recurse on without bound, or overallocate for: wrong typed well-known keys, zero alignment, nested or oversized fully decoded arrays, an unreasonable `split.tensors.count`, and empty strings the parser would misread. Keys are matched the way the parser sees them. The KV count, fully read string bytes, and skipped string counts are bounded too. The file becomes an unknown instead of taking down the scan.
- `copyHeader` now bounds its own read to the max header size.
- non-finite floats in `header` (including inside arrays) become `"NaN"`, `"+Inf"` or `"-Inf"` so JSON encoding can't fail. `header` and `metadataHash` are otherwise unchanged.
- a `metadataHash` failure is now an error instead of an empty hash.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-29 14:03:31 +00:00
committed by GitHub
parent a4d3aedf4e
commit ae7843e17c
5 changed files with 605 additions and 44 deletions
+31 -3
View File
@@ -72,8 +72,32 @@ func (b *testGGUFBuilder) withUint32KV(key string, value uint32) *testGGUFBuilde
return b
}
// withRawKV adds a KV whose value bytes are written verbatim, for arrays and malformed values.
func (b *testGGUFBuilder) withRawKV(key string, valueType uint32, raw []byte) *testGGUFBuilder {
b.kvPairs = append(b.kvPairs, testKVPair{key: key, valueType: valueType, value: raw})
return b
}
// ggufArray encodes an array value: item type, length, then the already encoded items.
func ggufArray(itemType uint32, n uint64, items []byte) []byte {
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, itemType)
binary.Write(buf, binary.LittleEndian, n)
buf.Write(items)
return buf.Bytes()
}
// writeLen writes a count or length, which is 32-bit in GGUF v1 and 64-bit after.
func (b *testGGUFBuilder) writeLen(n uint64) {
if b.version <= 1 {
binary.Write(b.buf, binary.LittleEndian, uint32(n))
return
}
binary.Write(b.buf, binary.LittleEndian, n)
}
func (b *testGGUFBuilder) writeString(s string) {
binary.Write(b.buf, binary.LittleEndian, uint64(len(s)))
b.writeLen(uint64(len(s)))
b.buf.WriteString(s)
}
@@ -85,10 +109,10 @@ func (b *testGGUFBuilder) build() []byte {
binary.Write(b.buf, binary.LittleEndian, b.version)
// Write tensor count
binary.Write(b.buf, binary.LittleEndian, b.tensorCount)
b.writeLen(b.tensorCount)
// Write KV count
binary.Write(b.buf, binary.LittleEndian, uint64(len(b.kvPairs)))
b.writeLen(uint64(len(b.kvPairs)))
// Write KV pairs
for _, kv := range b.kvPairs {
@@ -97,6 +121,10 @@ func (b *testGGUFBuilder) build() []byte {
// Write value type
binary.Write(b.buf, binary.LittleEndian, kv.valueType)
// Write value based on type
if raw, ok := kv.value.([]byte); ok {
b.buf.Write(raw)
continue
}
switch kv.valueType {
case ggufTypeString:
b.writeString(kv.value.(string))