---
title: "Discussion on \"Your Model File Is Untrusted Input\" | Hashnode"
description: "Discussion on \"Your Model File Is Untrusted Input\". Sixty-five bytes are enough to kill llama.cpp. Not by corrupting memory, by handing the GGUF loader a tensor dimension of zero and letting a."
url: "https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17"
---

[Feed](https://hashnode.com/feed) /

Discussion

# [Your Model File Is Untrusted Input](https://harrisonsec.hashnode.dev/your-model-file-is-untrusted-input?utm_source=hashnode&utm_medium=feed)

Sixty-five bytes are enough to kill llama.cpp. Not by corrupting memory, by handing the GGUF loader a tensor dimension of zero and letting a.

Sep 20 · 8 min read · harrisonsec.hashnode.dev  
Read the article

-   [#c-programming](https://hashnode.com/tag/c-programming)
-   [#security](https://hashnode.com/tag/security)
-   [#ai](https://hashnode.com/tag/ai)

## Written by

[Harrison Guo](https://hashnode.com/@harrisonguo)

@harrisonguo · WhiteRock, CA

  
[View profile](https://hashnode.com/@harrisonguo)

Security engineer and distributed systems practitioner. Writing about systems internals, performance, security research, and AI applications. Building harrisonsec.com.

## Responses 7

Markdown supported · ⌘↵ to post

[Ahmet Özel](https://hashnode.com/@ahmetozel)

[Sep 20](https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17/comment/6ab047b3a6a0a69027a0a8a1)

"The loader is the trust boundary" belongs on the first page of every model-hosting guide. People apply real scrutiny to pickle files because that lesson was learned loudly, then download a .gguf with the mental model of a JPEG, while the parser turning it into heap allocations and tensor shapes is C++ reading attacker-controlled counts.

A zero dimension killing llama.cpp on a division is close to the friendly version of this bug. The same class quietly hands you an allocation size someone else chose.

The practical takeaway for anyone running a model server: neither the hub nor the file extension is a trust boundary, so validate shapes before the loader gets to, and treat model fetching with the same care as any other untrusted download.

[Harrison Guo](https://hashnode.com/@harrisonguo)

[Sep 20](https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17/comment/6ab047b3a6a0a69027a0a8a1#reply-6ab071974c9ca26d6762cdb3)

"Friendly version" is right. A divide by zero fails close to the trust boundary, where the cause is still visible. An unchecked count reused as a size can fail much farther downstream—as an overflow, an oversized allocation, or a bad shape—with a much less useful error.

Pickle got scrutiny because the exploit was easy to demonstrate. GGUF feels inert because it contains tensors rather than executable code, but its header values still feed a C++ parser. The validator therefore has to check what the downstream code requires, not merely what the format allows.

[Harrison Guo](https://hashnode.com/@harrisonguo)

[Oct 5](https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17/comment/6ab047b3a6a0a69027a0a8a1#reply-6ac3ecfb747c74fc70737200)

You've extended it to exactly the right place. The 'inert' framing is where people get lulled: there's no executable content, so it feels safe, but the parser is doing arithmetic on attacker-controlled counts, sizes and offsets before it validates any of them. A lie in a tensor count or a dimension becomes a multiply that overflows, an allocation that's too small, a loop that walks off the end, and as you say the same shape shows up in fonts, images, any format a fast parser consumes. That's the concrete form of 'check what the consumer requires': validate that each count and offset is consistent with what the loader will actually do with it, the allocation it implies and the bytes it will read, not merely that the header is well-formed. Spec-conformant is a promise about the file; survivable is a promise about the consumer, and only the second one closes these.

[Ahmet Özel](https://hashnode.com/@ahmetozel)

[Oct 6](https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17/comment/6ab047b3a6a0a69027a0a8a1#reply-6ac53972f34f21b458e7462b)

That consumer-level invariant is the part I would encode in the test corpus. For each tensor, the checked product of dimensions, byte size and offset range should agree before allocation or pointer traversal; checking each dimension in isolation does not establish that agreement.

The legitimate empty-tensor case also deserves its own fixture so a defensive patch cannot simply reject everything with a zero. Running malformed and valid boundary cases under sanitizers would test the arithmetic and the consuming path together, rather than only testing that the header parser returns an error.

[Mihai\_LeanZero](https://hashnode.com/@mihaileanzero)

[Sep 20](https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17/comment/6ab01e3278453c51009de1c9)

The ne\[j\] >= 0 line is the best part of this writeup. It's correct, it passed review, and it's still the reason the crash below it went unquestioned for as long as it did. I run into the same shape reviewing agent output before it gets applied to anything real - a schema-valid check that reads as "this is safe" when the code underneath assumed something narrower than schema-valid ever guaranteed. Collapsing the per-dimension checks into ggml\_nelements(info.t) > 0 is the same move as replacing three guard clauses with the one statement that actually says what the invariant was. The diffusion zero-tensor correction is the part I'd have gotten wrong too - rejecting zero outright is the obvious fix and it's the wrong one.

[Harrison Guo](https://hashnode.com/@harrisonguo)

[Sep 20](https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17/comment/6ab01e3278453c51009de1c9#reply-6ab071b94af8bc4d03a135d5)

That agent-output parallel is useful. `ne[j] >= 0` correctly answers "is this dimension negative?" The downstream code needed the answer to a different question: "does this tensor contain anything?" Because both look like dimension validation, a reviewer sees the first check and moves on.

Rejecting zero everywhere would also be wrong; legitimate zero-length tensors exist. The fix is to enforce non-emptiness at the path that requires it. Here, `ggml_nelements(info.t) > 0` states the assumption the allocation path was already making.

[Ahmet Özel](https://hashnode.com/@ahmetozel)

[Sep 21](https://hashnode.com/posts/your-model-file-is-untrusted-input/6ab00cad5eae2d1a84213c17/comment/6ab01e3278453c51009de1c9#reply-6ab0ccf367ba30d90bcb94e2)

"The validator has to check what the downstream code requires, not merely what the format allows" is the line I would take away, and it names why format-level validation keeps disappointing people. A spec-conformant file is a much weaker guarantee than a file the consumer can survive, and the gap between those two is exactly where these bugs live.

The inert-looking part matters too. A .gguf holds tensors rather than code, so it reads as data in a way a pickle never did, and that intuition is precisely what stops anyone asking who parses it.

The same reasoning covers any binary artifact a fast parser consumes — fonts, images, model files — where the danger is not executable content but the arithmetic done on attacker-controlled counts before anything gets validated.
