Skip to content

verify-gguf.md presents the unkeyed .sha256 sidecar as a signature and promises a tampered GGUF 'never reaches' the runtime (CR-A09-007) #446

Open

Description

@cemililik

Wave 1 · theme D1-8: Docs describing features that do not exist (doc side of code findings) · highest severity High · 1 finding(s) · effort medium

Summary

A hub publisher or a writer to the shared export directory modifies the GGUF and writes a matching .sha256. The CI gate 'verify-gguf ... | jq -e .valid' passes with exit 0, and the page told the operator a tampered GGUF would never reach the runtime and that the sidecar is what the sender 'signed'.

Primary finding

CR-A09-007 — verify-gguf.md presents the unkeyed .sha256 sidecar as a signature and promises a tampered GGUF 'never reaches' the runtime

severity High · reachability default · intent contract-violation · confidence confirmed · independently confirmed · reproduced end to end · already tracked: KNOWN-OPEN TRUST-04 — docs/roadmap/phase-16-trust-surface-hardening.md:423-424 (S5 open)

What happens. verify-gguf.md:8 (TR:8) says to use verify-gguf as the final pre-serve gate 'so a corrupted or tampered GGUF never reaches llama.cpp, Ollama, vLLM, or LM Studio'. Line 14 (TR:14, 'imzalanan') tells operators who receive a GGUF from a third-party trainer or hub that recomputing the layers detects 'drift between what they sent and what they signed'. The sidecar is a plain, unkeyed SHA-256 read from <path>.sha256 next to the file (_gguf.py:104-151). There is no HMAC or signature, unlike the audit log's FORGELM_AUDIT_SECRET. Anyone who can write the GGUF can rewrite the sidecar, and a third party that sends both files controls both. The verifier therefore detects transit or disk corruption only, not adversarial tampering by the artefact producer (IN-10, the producer is the adversary). The page admits the regenerate-sidecar bypass at line 164 but keeps the stronger claims at lines 8, 14 and 43 ('exit 6 (post-export tamper)'). This is a doc overclaim about a verify-* gate. The no-sidecar-by-default half is the already-reported 'export never writes the sidecar' finding and is not repeated here.

Where.

Evidence.

  • I flipped one byte in the tensor data of a valid GGUF (flip.gguf), regenerated the sidecar with shasum -a 256 flip.gguf > flip.gguf.sha256, and ran fpy -m forgelm verify-gguf flip.gguf. Result: 'OK: flip.gguf / GGUF magic OK, metadata parsed, SHA-256 sidecar match', and valid=True maps to exit 0 (_verify_gguf.py:125). With the sidecar removed, the same file and a file with 1000 appended bytes also give valid=True.

Recommended fix. Reword lines 8, 14 and 43 (EN+TR): the sidecar detects accidental corruption, not tampering by anyone who can write the sidecar, and it is not a signature. Optionally add a keyed digest (HMAC with FORGELM_AUDIT_SECRET), as the audit chain has.

Acceptance criteria

  • A regression test reproduces the failure scenario and asserts the corrected behaviour.
  • Documentation listed above is corrected in English and in its Turkish mirror in the same PR.
  • Every finding in this issue is resolved or explicitly closed with a reason.

Found by the October 2026 ForgeLM code and documentation review against commit cab2947. Code links point at that commit; line numbers may have moved since. Finding IDs (CR-…, DOC-…, TECH-…) are stable references for this backlog.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: verifyCode area: verifybacklog: knownAlready tracked in docs/roadmapblockedWaits for another issue (see Blocked by); set and removed automatically, never by handbugSomething isn't workingsecuritySecurity-relevantseverity: highWrong behaviour or a false claim users rely onsource: review-2026-10Found by the October 2026 code and documentation reviewwave: 1Roadmap wave 1: high severity, small effort

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions