YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

GGUF/ZIP Structural Polyglot β€” Cross-Implementation Format-Level Gap + Incomplete Detection in a Production Scanner

Status: Dual disclosure in preparation β€” to 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.

Summary

The GGUF specification never requires an implementation to verify that a file's total length matches exactly what its declared header, key-value metadata, and tensor data actually consume. Confirmed empirically across three independent GGUF implementations, in three different languages, by three different authors/teams:

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

All three parse a file consisting of [valid GGUF content][arbitrary trailing bytes] as a fully valid GGUF file, silently ignoring everything after the last tensor's data.

Separately, Python's 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.

Combining these two facts: [valid GGUF prefix][valid ZIP/NPZ suffix] is simultaneously valid under both formats β€” a genuine, working structural polyglot.

Contrast: formats that are NOT vulnerable to this specific technique

The same technique was tested against two other formats and found not vulnerable, for informative, structural reasons:

  • ONNX (protobuf): ParseFromString() requires the entire buffer to parse as valid protobuf fields; trailing non-protobuf bytes break parsing (DecodeError).
  • safetensors (Rust): has an explicit 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.

This makes the pattern precise: polyglot-vulnerability correlates with whether a format's parser validates that it consumed the entire file, versus trusting a validated prefix and silently ignoring the rest.

Concrete exploitation scenario (demonstrated, not hypothetical)

poc_gguf_polyglot_smuggling.py builds a single 760-byte file with a .gguf extension that:

  • Looks completely clean to any GGUF-aware scanner: one tensor named dummy.weight = [0,0,0,0], metadata name "tiny-demo-model" β€” nothing to flag.
  • Actually carries a hidden NPZ payload with different, real content (real_backdoor_weights = [1337, 6666, 9999], exfil_marker = [42, 42, 42]) that a GGUF-only scanner never inspects.

The script demonstrates three stages against the identical file:

=== 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.

Stage A and Stage B are not contrived β€” they represent any pipeline where one stage validates GGUF-ness and a different stage (a generic archive-indexer, backup/dedup tool, or anything that might treat an upload as "possibly a ZIP-based sidecar bundle") extracts the ZIP content.

Real-world detection test: Promptfoo's modelaudit

To move beyond a hypothetical pipeline, the same file was scanned with modelaudit β€” 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)

This is a genuinely useful, honest result, not a clean bypass:

  • 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).
  • However, this check identifies only the symptom (unexpected trailing bytes), not the cause. 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.
  • The finding's severity is "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."

Suggested fixes

For the GGUF specification / 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.

For 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.

Files in this disclosure

  • 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

Disclosure

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

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support