The Model File Is the Payload: GGUF Parser Attack Surface and the Local AI Supply Chain
Four CVEs in the llama.cpp GGUF parser prove a model file is executable code at parse time; a Jinja2 SSTI in Python bindings and a server-side completion-endpoint RCE broaden the surface. What six bugs mean for Ollama, LM Studio, and your homelab intake process.
It is 10pm and I am pulling a new Qwen3.6 quant. The file lands, looks right, and I load it before I have checked anything. If you run local AI, you do this too: a few gigabytes from a search result, treated as logistics.
That step is where the attack lives. A GGUF model file is not data as far as your inference stack is concerned. It is a binary that a parser reads, and parsing here executes native C and C++ code with whatever the inference user can do. Four CVEs in the llama.cpp C++ GGUF parser prove a model file is executable code at parse time, plus a Jinja2 SSTI in the Python bindings and a server-side completion-endpoint RCE show how broad this surface actually is.
I write this from the bench: Qwen3.6 GGUFs through a ROCm build of llama.cpp on a Radeon R9700, a self-hosted Ollama for convenience clients, models sourced from Hugging Face with hashes I check myself. Network exposure (ports, auth, the 175,000-instance problem) is a different article; the Local LLM Security Hardening post, cross-linked below, covers that side.
The CVE cluster, verified
The claim "the model file is code" is not a metaphor. It is a list of bugs in the GGUF parser of llama.cpp, the reference implementation that Ollama, LM Studio, Jan, and most self-hosted inference tooling depends on. A GGUF is a binary container: key-value header plus tensors, read by native code running as your inference user, with your secrets in reach. Every entry below was verified against NVD, upstream ggml-org/llama.cpp git, and vendor advisories; mainline reference is build b10588 (master, 2026-08-22).
| CVE | Flaw | Fix point |
|---|---|---|
| CVE-2024-21825 | Heap overflow, GGUF array/string parse (Talos 8.8 / NVD 9.8) | commit a4b07c057a55, 2024-01-30 |
| CVE-2024-23496 | Integer overflow in gguf_fread_str (Talos 8.8 / NVD 9.8) | same commit as above |
| CVE-2024-34359 ("Llama Drama") | Jinja2 SSTI in chat-template metadata (9.6, GitHub CNA) | llama-cpp-python v0.2.72; not a C++ core bug |
| CVE-2026-21869 | OOB write via negative n_discard, requires --context-shift (NVD 9.8; GHSA "Low" is a registration artifact) | c78fb909b237, 2026-04-23; hardening 159d093a43e8, 2026-06-19 |
| CVE-2026-27940 | Integer overflow in gguf_init_from_file_impl; RCE PoC published; quantize/imatrix/tools paths only | 418dea39cea8, 2026-02-24, release b8146 |
| CVE-2026-33298 | Integer overflow in ggml_nbytes; the "pre-b7824" tensor-size issue is this bug | b7824 (4e5b83b22691), 2026-01-24 |
All six are fixed in mainline b9670 or earlier. The 2024 Talos pair (TALOS-2024-1912/-1913) predates the ggml-org advisory program, so there is no GHSA; the fix commit is the anchor. CVE-2024-34359 is included because it is the best-documented "model file runs code" story, not a core-parser bug. CVE-2026-21869 carries a ratings conflict (GHSA Low vs NVD 9.8); treat 9.8 as the conservative number, with the --context-shift requirement as the practical caveat.
The four GGUF parser bugs are model-file vulnerabilities: they trigger when your inference stack loads and parses a .gguf file in your hands. CVE-2024-34359 is a different class entirely: Jinja2 SSTI in llama-cpp-python, which renders templates from Python code at load time rather than parsing the GGUF binary itself. CVE-2026-21869 is a server completion-endpoint RCE via negative n_discard JSON parameter; it fires when an attacker sends a crafted request to the inference server's API, no model file involved.
And the open part: on 2026-05-15 an anonymous researcher posted six GGUF parser weaknesses to oss-security (Openwall message 135266653, "Multiple Vulnerabilities in llama.cpp GGUF Format Parsers", no CVEs assigned, no maintainer reply). Re-reading master source on 2026-08-23: V-01 (no upper bound on general.alignment; 32-bit GGML_PAD overflow feeding a seek primitive) and V-02 (the 1 GB string/array caps) are still unpatched in b10588; V-03 (missing n_dims check in the Python reader, roughly a 32 GB map) was patched 2026-08-04, ten weeks after disclosure; V-04 and V-05 are partially addressed; V-06's premise was stale. None of the six has a CVE, so scanner-driven patching will not track them. "Run current mainline" is necessary, and it is not a guarantee.
How a malicious model reaches your inference host
The attack is boring. That is why it works: the attacker does not exploit your parser on the first try. They get you to download the wrong file, and let the parser finish the job. Every example is public and verified.
Typosquatting, in the wild. In May 2026 HiddenLayer documented a Hugging Face repo, Open-OSS/privacy-filter, that typosquatted OpenAI's Privacy Filter release with a near-verbatim model card. It reached number one on HF trending in under 18 hours with 244,000 downloads and 667 likes, numbers HiddenLayer flagged as almost certainly inflated. The payload was a Rust infostealer, base64-encoded and dropped at run time through start.bat (Windows) or loader.py (Linux). If your intake process is "search HF and click the first result," you were the victim.
The pull path as delivery. GreyNoise's Ollama honeypots captured over 91,000 attack sessions from October 2025 to January 2026; one campaign fed malicious registry URLs to Ollama's /api/pull, forcing the server to connect out to attacker infrastructure. Same vector, roles reversed: the model is the payload, the pull request is the delivery. /api/pull also has its own history: Probllama (CVE-2024-37032, unauthenticated RCE via path traversal, fixed in Ollama 0.1.34).
The payload inside the file. JFrog's initial Hugging Face scan found roughly 100 malicious models, mostly pickle-based; the GGUF-specific finding is the one that matters here: a GGUF can carry a Jinja2 chat template, and loading it through a renderer that executes templates runs that template as Python. The publicly known exploitable instance is CVE-2024-34359, via llama-cpp-python below 0.2.72. JFrog's scope note is easy to misread: arbitrary code execution does not occur when the file is loaded into llama-cpp-python 0.2.72+ or other GGUF-compatible runtimes. For a homelab running llama.cpp or Ollama directly, the template vector is largely patched; for anything running llama-cpp-python in the loop, it is not.
The poisoned conversion. Hugging Face's safetensors conversion bot, which converts PyTorch repos into the safe format, has been hijacked. HiddenLayer's "Silent Sabotage" research showed the conversion flow itself could be weaponized, so the "safe" conversion you requested can land in a poisoned repo.
Why homelabs, specifically. The model-file attack does not require your ports to be open. You do not need 11434 listening. You need to want a quant at 10pm.
The full chain, end to end: typosquatted repo, hype via trending and likes, download through a "faster mirror" link, first load on an unpatched engine or a template-rendering library, code execution as the inference user, then keys, dotfiles, and whatever that user can reach are gone. Every link in that chain has a cheap control. The checklist below puts one on each.
Ecosystem impact: who inherits these bugs
Ollama inherits the parser, build by build. Ollama vendors a specific build of llama.cpp into its Go binary rather than calling the parser dynamically. Ollama 0.30 (June 2026) routes the Hugging Face GGUF path entirely through the vendored llama.cpp, and later releases keep updating the embedded build (0.32.x ships b10068+). Any CVE in the vendored code path is inherited until that build is updated, and the app version and the embedded build tag are decoupled: a newer Ollama release could ship an older, unpatched llama.cpp commit if the vendoring lagged. The check is not "which Ollama am I running?" It is "which llama.cpp build did this release vendor?"
The SSTI scope is narrower than the panic. The Jinja2 template vector reaches llama-cpp-python below 0.2.72 and SGLang (CVE-2026-5760, CVSS 9.8, unauthenticated RCE), not Ollama directly. The fix on that side is a sandboxed template environment, not a model-side change.
GGUF versus safetensors, honestly compared. GGUF stores the Jinja2 chat template inside the same binary and is parsed by the C/C++ parser with the CVE history above. Safetensors keeps metadata separate, its spec guarantees no callable code in the file (confirmed by the Hugging Face security audit), and it has no known parser RCEs. The catch: safetensors does not carry quants, so it is not a GGUF replacement on the llama.cpp path. Where your stack takes safetensors (transformers, vLLM and friends), prefer it; everything else here stays about GGUF.
Provenance mechanisms, state of play. OpenSSF Model Signing v1.0 (April 2025) with Sigstore integration exists, but there is no enforcement layer: local runners do not verify signatures at load time. Hugging Face's Guardian scanner has covered 4+ million models, but it is post-hoc detection, and the scanners are bypassable (Hive Security's "Poisoned AI" work moved payloads into 7z containers to slip past picklescan). An Internetware 2025 study found 4.8% of identified HF typosquats showed explicit malicious intent, including first-file trust exploits where the UI shows the clean file and the payload sits in a later quantized file. Net: format security is patched and patching; provenance is aspirational; the floor is your own intake discipline.
The intake checklist: what happens before a model touches the GPU
On this homelab, every model file goes through the same gauntlet before it is allowed anywhere near the R9700. It is deliberately boring, and the steps are the gauntlet itself, in order.
1. Verify the source before the download. Type the repository URL. Do not click through from a tweet, a forum thread, or a "faster mirror" link, because typosquatting lives in links. Check the org verification badge, the account age, the model card, and whether the author has other real models. A repo two days old and already number one on trending is a red flag, not a success metric. One character off is the whole attack.
2. Download from the canonical origin. For Hugging Face, the hf CLI:
hf download <org>/<repo> <file>.gguf --local-dir ./intake
For GitHub releases, use the release page URL directly. If you must use a third-party mirror, fetch the canonical file first and compare hashes.
3. Verify the SHA256 before the first load.
sha256sum qwen3.6-32b-q4_k_m.gguf
Compare against the hash the author published in a second channel: release notes, model card, project blog. A .sha256 file sitting next to the weights in the same repo is not independent evidence; the same uploader controls both files. It is a transport check, not a trust anchor.
4. Inspect the metadata before loading. A GGUF's key-value section is readable without running anything: dump it with gguf-dump.py from the llama.cpp repo or the gguf Python package. Look for the chat template (the jinja field), huge string fields, tensor counts that do not match the architecture, and any key that looks like it wants a program to read it. Read the chat template in full: it can contain code, and that is the Llama Drama vector from the table above.
5. First load in quarantine. A parse is an execution context, so the first load of anything untrusted happens on a box with nothing worth stealing. Ours: a small Ubuntu VM, no LAN membership, no SSH keys, no cloud credentials, inference user with no sudo. The container version, with no network at all:
docker run --network none --rm -v ./intake:/models -w /models \
<your-llama.cpp-image> ./main -m untrusted-model.gguf -p "test" -n 16
And for the parse VM, nftables default-deny on egress so even a successful exploit cannot phone home:
nft add table ip output
nft add chain ip output '{ type filter hook output priority 0 \; policy drop \; ip daddr 127.0.0.1/8 accept \; }'
Run one or two prompts. Check the output is sane. Watch the firewall log for blocked outbound attempts. If it loads clean and answers like the model it claims to be, it moves to the real host. If it misbehaves, the blast radius is a VM you can delete.
6. Record the intake. One line per model: repo, URL, date, SHA256, engine version and build, who approved:
qwen3.6-32b-q4_k_m.gguf | hf://<org>/<repo>@rev1 | 2026-08-22 | sha256:9f2a... | llama.cpp b9xxx (ROCm) | glenn
This is your provenance log: the file you open when something goes wrong.
7. Keep the parser current. Note the llama.cpp build number in the manifest, not just "llama.cpp". The fixes in the table landed in specific builds, and the manifest line is what lets you answer "which build am I actually running?" without archaeology.
8. Re-hash after updates, and periodically. Re-downloading a model is a new intake; full gauntlet again. And re-hash the files you already run now and then: a compromised upstream repo can swap files silently, and the download-time hash check does not catch a file that was already on disk.
The air-gapped variant, for models that matter. Do the intake on a dedicated machine that holds no secrets: download with a dedicated account, verify the hash, copy to USB, verify on arrival, move it to the inference host. The USB goes one direction only. Twenty minutes of work; the download step is out of the trust boundary.
Signing and provenance: what actually works today
Signing for model weights is mostly a promise.
What works today. Author signatures, where authors bother: some GGUF quants ship a .sig with a public key, Minisign or GPG, and gpg --verify model.gguf.sig model.gguf verifies the exact bytes against a key you can independently obtain. The problem is coverage: most community quants do not sign, and "well-known author" is not a signature. Hugging Face commit signing verifies who pushed the commit, but it is commit-level, not a pin on the weight bytes, and most repos do not sign commits at all. Safetensors is not signing but a structurally safer container: Rust, externally audited, no code path at load time. Registry hygiene (HF org badges, scanners like picklescan) is a floor, not a wall: a badge verifies the org's identity, not that the file is what it claims.
What is aspirational. There is an open feature request (ggml-org/llama.cpp issue #15250) for built-in load-time checks, --verify-sha256 and --verify-sig on the model path. Not implemented: the format verifies nothing about itself, and every guarantee has to come from outside the load path. Hugging Face has no generally available file-level weight signing, so "I downloaded it from HF" is an origin claim, not an integrity claim. And there is no Sigstore-style transparency for weights with real adoption yet; cosign is mature for container images, and the model world is moving in that direction.
What you can do this weekend. Keep the intake manifest; it is a poor man's signature, but it is yours and auditable. For models you convert yourself, GPG-sign the GGUF after conversion and publish the hash: a public claim that a swapped file breaks. Where a quant you depend on ships signatures, add the gpg --verify line to your intake script. And convert from safetensors yourself when you can: download the upstream safetensors, hash-check it, run llama.cpp's convert_hf_to_gguf.py, hash the output. A ready-made quant skips all of it, which is exactly why the ready-made quant needs the gauntlet.
Risk assessment: what to prioritize, and why
The honest ordering for a homelab. The biggest risk to a local LLM is still network exposure, covered in the cross-linked Local LLM Security Hardening post and in the inference stack hardening guide I am writing from the same research. One line: if the endpoint is closed to the world, your residual risk is intake.
The model-file attack is the second priority, not the seventh. It is narrower, but it does not require your ports to be open, and it arrives through the one relationship you have built on trust: you asked for the file.
Questions that size your risk. What user does the engine run as? If it is root or a user with sudo, parse-time code execution is host compromise, full stop. What secrets are in reach? SSH keys, cloud credentials, the docker socket, router admin: if the inference user can read them, assume a malicious model can too. Is tool calling enabled? Roughly half of the exposed instances studied by SentinelOne/Censys on 175,000 hosts allowed it. Do you ever pull from URLs, or only pinned refs? The GreyNoise campaign was literally malicious URLs.
Mitigations, cheapest first. Network hygiene (bind to localhost or keep behind a LAN firewall) is the highest expected-value fix and is covered elsewhere. Then source discipline (step 1, kills typosquatting), hash verification (step 3, kills mirror poisoning), quarantine first load (step 5, kills parse-time execution for anything you did not make), engine currency (step 7, makes build status a lookup), and the provenance log plus signatures where they exist (step 6, the thing that turns a suspected compromise into a postmortem you can actually write).
If you suspect a model was malicious. Odd behavior, an outbound connection you do not recognize, a process you did not start. Stop the engine. Do not delete the GGUF: it is evidence and your repro case. Record its SHA256 and compare against the manifest. Check the inference user's cron, new SSH keys, and recent outbound connections. Then assume the account is compromised: rotate anything that user could read; reimage if the engine ran as root.
The model file is code. You already run code you have not read, every day. This is the supply chain that finally matches that reality, and the gauntlet above is just as much ceremony as any other "never run untrusted binaries" rule.
Sources
- NVD: CVE-2024-21825, -23496, -34359, CVE-2026-21869, -27940, -33298; Talos TALOS-2024-1912/-1913 (2024 pair).
- ggml-org/llama.cpp GHSA-8947-pfff-2f3c, GHSA-3p4r-fq3f-q74v, GHSA-96jg-mvhq-q7q7; upstream commits a4b07c057a55, 418dea39cea8 (b8146), 4e5b83b22691 (b7824), c78fb909b237, 159d093a43e8, 5788b510a1e3.
- oss-security, "Multiple Vulnerabilities in llama.cpp GGUF Format Parsers" (2026-05-15, anonymous, no CVEs assigned; message ID 135266653 on lists.openwall.net / openwall.com archives).
- JFrog GGUF-SSTI writeup (research.jfrog.com/model-threats/gguf-ssti); abetlen/llama-cpp-python GHSA-56xg-wfcc-g829; SGLang CVE-2026-5760 (CERT/CC VU#915947, CSA research note).
- Ollama 0.30 GGUF blog (ollama.com); Probllama CVE-2024-37032 (fixed 0.1.34).
- HiddenLayer: Open-OSS/privacy-filter typosquat (May 2026, via CSO/Infosecurity); "Silent Sabotage" (safetensors conversion hijack).
- Hive Security "Poisoned AI" (2026); JFrog HF scan (~100 malicious models, via Dark Reading).
- GreyNoise "Threat Actors Actively Targeting LLMs" (Jan 2026, 91,403 sessions); Ollama exposure ~175,000 instances (SentinelOne/Censys; Ollama field counts from Malwlab's own scanning work).
- Hugging Face: security-gpg docs, safetensors security audit (NCC Group), Guardian 4M-model report; OpenSSF Model Signing v1.0; Sigstore model transparency (Red Hat).
- Internetware 2025, "Exploring Typo Squatting Threats in the Hugging Face Ecosystem"; ggml-org/llama.cpp issue #15250 (open).
Cross-links
- Local LLM Security Hardening: Threat Model for Self-Hosted AI Infrastructure
- Local AI Inference Stack Hardening Guide (companion piece, in production)