Views
No views yet
ggml-org/llama.cpp (GGUF specification, via Huntr) and to promptfoo/modelaudit (via their coordinated disclosure process)
Class: CWE-436 (Interpretation Conflict), CWE-20 (Improper Input Validation)
Severity: Medium — not a crash/RCE by itself; a security-control-bypass / detection-gap primitive whose real-world impact depends on what a specific pipeline does with the "other" interpretation of the file.| Implementation | Language | Author | Result |
|---|---|---|---|
gguf-py | Python | ggml-org/llama.cpp (reference) | Accepts trailing garbage |
fs/gguf | Go | ollama/ollama (independent reimplementation) | Accepts trailing garbage |
gguf crate | Rust | Jiayu Liu (independent, crates.io) | Accepts trailing garbage |
[valid GGUF content][arbitrary trailing bytes] as a fully valid GGUF file, silently ignoring everything after the last tensor's data.zipfile module (and ZIP readers generally) locate the "End of Central Directory" record by scanning backward from EOF — meaning a ZIP archive parses correctly even when arbitrary bytes precede it.[valid GGUF prefix][valid ZIP/NPZ suffix] is simultaneously valid under both formats — a genuine, working structural polyglot.ParseFromString() requires the entire buffer to parse as valid protobuf fields; trailing non-protobuf bytes break parsing (DecodeError).buffer_end + N_LEN + n != buffer_len check — the total file size must exactly equal declared header + data, with zero tolerance for extra bytes. This appears to be a deliberate hardening: Trail of Bits' 2023 audit of safetensors (commissioned by Hugging Face, EleutherAI, and Stability AI) explicitly found and had fixed a "polyglot files" issue in that format — direct historical precedent that this vulnerability class is real, recognized, and has previously required a fix in adjacent tooling.poc_gguf_polyglot_smuggling.py builds a single 760-byte file with a .gguf extension that:dummy.weight = [0,0,0,0], metadata name "tiny-demo-model" — nothing to flag.real_backdoor_weights = [1337, 6666, 9999], exfil_marker = [42, 42, 42]) that a GGUF-only scanner never inspects.=== Stage A: security scanner (GGUF-aware) ===
model name : 'tiny-demo-model'
tensors : [('dummy.weight', [0.0, 0.0, 0.0, 0.0])]
verdict : APPROVED -- looks like a tiny, harmless demo model.
=== Stage B: downstream tool (treats file as ZIP/NPZ) ===
found hidden array 'real_backdoor_weights.npy': [1337. 6666. 9999.]
found hidden array 'exfil_marker.npy': [42 42 42]
This data was NEVER inspected by Stage A's GGUF scan.modelauditmodelaudit — Promptfoo's open-source, SOC2-certified, production ML-model security scanner (Promptfoo is now part of OpenAI; modelaudit has a dedicated, named GGUF/GGML scanner advertised to ensure files "are structurally sound and don't contain hidden threats").=== Stage C: modelaudit (Promptfoo/OpenAI production scanner) ===
rule S902: Size mismatch for tensor dummy.weight (severity=warning)modelaudit does detect an anomaly — rule S902, "Tensor Size Consistency Check" — flagging that the file is larger (632 bytes after the tensor) than the declared tensor data requires (16 bytes, ±32-byte alignment tolerance).modelaudit ships its own separate ZIP Archive Scanner (with zip-bomb, directory-traversal, and nested-content checks) and Pickle Scanner — either of which would meaningfully inspect the smuggled content — but neither is invoked recursively on the unexplained trailing bytes of a file already classified as GGUF."warning", not "critical". modelaudit supports per-rule severity overrides (-S CODE=LEVEL) and SARIF output specifically for CI/CD triage — a very common real-world pattern is to gate deployment only on critical/error-severity findings and merely log warnings, in which case this specific detection would not block a release by itself, even though the raw CLI exit code (1) technically indicates "issues found."ggml-org/llama.cpp: require (or at minimum strongly recommend, with implementations expected to warn/reject) that the total consumed byte count (header + KV metadata + tensor info + tensor data, including alignment padding) equals the total file size, the same way safetensors already does. This is a spec-level, cross-implementation gap, not a fix in any single one of the three tested implementations.modelaudit: when the GGUF scanner's own S902 (tensor size mismatch / unexpected trailing bytes) check fires, recursively apply the existing ZIP Archive Scanner (and other appropriate scanners) to the unexplained trailing region, rather than only reporting the size discrepancy in isolation. Consider whether "unexplained trailing bytes matching a valid ZIP/archive signature" warrants a higher default severity than a generic alignment warning, given the demonstrated smuggling potential.poc_gguf_polyglot_smuggling.py — self-contained PoC (builds the file, runs all three stages)modelaudit_output.json — raw JSON output from the real modelaudit scan for reference