Skip to content

Forgelm export never writes the <gguf>.sha256 sidecar that verify-gguf and the docs rely on, so verify-gguf passes a tampered export with exit 0 (CR-A09-005) #84

Open

Description

@cemililik

Wave 0 · theme D0-2: GGUF integrity chain: the .sha256 sidecar · highest severity Critical · 6 finding(s) · effort medium

Summary

An operator exports a GGUF and gates serving on 'forgelm verify-gguf model.gguf', relying on the documented post-export tamper detection. Tensor bytes are modified while the GGUF stays parseable. The verdict is exit 0, because no sidecar exists and the recorded digest in model_integrity.json is never consulted.

Primary finding

CR-A09-005 — Forgelm export never writes the .sha256 sidecar that verify-gguf and the docs rely on, so verify-gguf passes a tampered export with exit 0

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:420-423,434-436 (S5 open; C-4)

What happens. export_model computes the GGUF SHA-256 (forgelm/export.py:461) but only returns it and appends it to model_integrity.json's exported_artifacts (export.py:207-239, 493-494). It never writes .sha256. verify_gguf compares bytes only when that sidecar exists (verify/_gguf.py:103-105) and otherwise returns valid (175-181). It never reads model_integrity.json, and verify_integrity reads only 'artifacts' (_model_integrity.py:111). The verify_gguf docstring (_gguf.py:59-62) and the docs (verify_gguf_subcommand.md:46, gguf-export.md:19-22, verify-gguf.md) state that the exporter writes the sidecar.

Where.

Evidence.

  • Searching forgelm/ for '.sha256' finds sidecar writers only in utils.py:64-148 (archives). export.py only computes _sha256_file and stores it under exported_artifacts. The magic-flip run (r/fx/flip.gguf with a valid .sha256 sidecar) exited 1 with checks.sidecar_present:false.
  • mkgguf.py builds a real GGUF with gguf.GGUFWriter (gguf is installed in the base venv). Results: tampered_nosidecar (last 4 tensor bytes changed) gave EXIT=0 'GGUF magic OK, metadata parsed'. tampered_rewritten_sidecar gave EXIT=0 'SHA-256 sidecar match'. A search of forgelm/ for '.sha256' writers outside verify/ finds none.
  • rg finds no writer of '.sha256' for GGUF in forgelm/; stubbed exports (q8_0, q4_k_m f16, adapter) left no .sha256 in export/ (ls). A tensor-byte flip with no sidecar -> verify-gguf exit 0 (tamper_nosc). Docs: verify-gguf.md:103, gguf-export.md:19,22, verify_gguf_subcommand.md:46.

Recommended fix. Have export write '.sha256' in sha256sum format, or make verify-gguf and verify-integrity consult exported_artifacts. Fix the docstring and the gguf-export.md sample output. Set sidecar_present before the magic early return. Log integrity-update failures at WARNING and write the manifest atomically.

Other findings with the same root cause

These close with the same fix; update them in the same pull request.

  • DOC-D1B04-003 (docs, Critical) — docs/reference/verify_gguf_subcommand.md: false claim — The exporter writes the <path>.sha256 sidecar by default.
    severity Critical · already tracked: KNOWN-OPEN CLI-03 — docs/roadmap/phase-16-trust-surface-hardening.md:419-424,434-436; docs/roadmap/risks-and-decisions.md:174
    Where. docs/reference/verify_gguf_subcommand.md:46 (Turkish mirror: docs/reference/verify_gguf_subcommand-tr.md:46) · type: false claim
    The document says. The exporter writes the <path>.sha256 sidecar by default.
    What the code does. forgelm export writes no sidecar. It only appends the digest to model_integrity.json 'exported_artifacts', when that file exists, and to the JSON output. Without a sidecar the SHA-256 layer is skipped: a GGUF with a byte flipped in its tensor data verified 'OK … GGUF magic OK, metadata parsed' with exit 0. The verifier docstring repeats the false claim.
    Code references. forgelm/export.py:205-239, forgelm/export.py:461-494, forgelm/verify/_gguf.py:61
    Proposed correction. Fix the code: have forgelm export write <output>.sha256 (sha256sum format). Until then: 'forgelm export does not write a sidecar; create one with sha256sum model.gguf > model.gguf.sha256, or verify against model_integrity.json exported_artifacts.'
    Turkish mirror. the Turkish page has the same error; fix both in the same PR

  • DOC-D3B01-004 (docs, Critical) — docs/usermanuals/en/deployment/verify-gguf.md: false claim — The exporter writes the sidecar by default; 'Always export with a sidecar (the default)'; GGUF Export is 'the production side that writes…
    severity Critical · already tracked: KNOWN-OPEN TRUST-04 docs/roadmap/phase-16-trust-surface-hardening.md:418-424; docs/roadmap/risks-and-decisions.md:174
    Where. docs/usermanuals/en/deployment/verify-gguf.md:8,103,156,173 (and Quick start :57-66) (Turkish mirror: docs/usermanuals/tr/deployment/verify-gguf.md:8,103,156,173) · type: false claim
    The document says. The exporter writes the sidecar by default; 'Always export with a sidecar (the default)'; GGUF Export is 'the production side that writes the sidecar this verifier consumes'; verify-gguf keeps a tampered GGUF from reaching the runtime.
    What the code does. forgelm export computes the digest only into ExportResult.sha256, the JSON envelope and model_integrity.json exported_artifacts. No <output>.sha256 file is written for a GGUF anywhere in forgelm/; the only .sha256 writer is the checkpoint-archive helper. On a fresh export verify-gguf therefore runs only the magic check (plus metadata when gguf is installed), and a post-export modification that…
    Code references. forgelm/export.py:193-238, forgelm/export.py:440-494, forgelm/cli/subcommands/_export.py:16-42, forgelm/utils.py:132-150
    Proposed correction. Fix the code: write <output_path>.sha256 (sha256sum format) in export_model. Until then: "forgelm export does not write a sidecar in this version. Create one with sha256sum model.gguf > model.gguf.sha256 (or from the sha256 field of forgelm export --output-format json) and keep it out of band."
    Turkish mirror. the Turkish page has the same error; fix both in the same PR

  • DOC-D3B08-001 (docs, Critical) — docs/usermanuals/en/deployment/gguf-export.md: false claim — Export produces 'a single .gguf file plus a .sha256 manifest' (line 19: 'wrote model.gguf and model.gguf.sha256'; line 8: 'a SHA-256 m…
    severity Critical · already tracked: KNOWN-OPEN TRUST-04 docs/roadmap/phase-16-trust-surface-hardening.md:423-424
    Where. docs/usermanuals/en/deployment/gguf-export.md:22 (Turkish mirror: docs/usermanuals/tr/deployment/gguf-export.md:19) · type: false claim
    The document says. Export produces 'a single .gguf file plus a .sha256 manifest' (line 19: 'wrote model.gguf and model.gguf.sha256'; line 8: 'a SHA-256 manifest'). verify-gguf then reports 'sha256 matches manifest' (line 77) and 'catches corruption from bad transfers, silent disk errors' (line 81).
    What the code does. forgelm export writes no sidecar. export_model only computes the digest into ExportResult and appends it to model_integrity.json if that file already exists in model_path (export.py:207-239, 461-496); nothing in forgelm/ writes a GGUF .sha256 file. verify-gguf compares SHA-256 only when .sha256 exists, and otherwise checks just the magic and the metadata parse (_gguf.py:103-171). Its own do…
    Code references. forgelm/export.py:207-239, forgelm/export.py:461-496, forgelm/verify/_gguf.py:58-63, forgelm/verify/_gguf.py:103-171
    Proposed correction. Fix the code: have export_model write a sha256sum-format <output>.sha256 sidecar next to the GGUF it produced, so verify-gguf's documented check engages. Until then add a warning: "forgelm export does not write a .sha256 sidecar. Create one yourself (sha256sum model.gguf > model.gguf.sha256) — without it forgelm verify-gguf checks only t…
    Turkish mirror. the Turkish page has the same error; fix both in the same PR

  • DOC-D4B07-002 (docs, Critical) — site/features.html: false claim — forgelm verify-gguf re-checks the SHA-256 + size manifest stamped at export time before you ship a quantized model. (HTML baseline: '...re-checks the SHA-256 sidec…
    severity Critical · already tracked: KNOWN-OPEN TRUST-04 docs/roadmap/phase-16-trust-surface-hardening.md:418-424
    Where. site/features.html:433 (rendered EN text site/js/translations.js:1042; the same claim appears in all 6 locales, e.g. translations.js:1147, 4477) · type: false claim
    The document says. forgelm verify-gguf re-checks the SHA-256 + size manifest stamped at export time before you ship a quantized model. (HTML baseline: '...re-checks the SHA-256 sidecar (when present)')
    What the code does. forgelm export computes the sha256 and size_bytes but records them only in model_dir/model_integrity.json under exported_artifacts (export.py:206-226, 461-497). It never writes a <path>.sha256 sidecar. verify-gguf checks only the magic header, the metadata parse and an optional <path>.sha256 sidecar (verify/_gguf.py:17, 46-80). It never reads model_integrity.json and never checks size. The ve…
    Code references. forgelm/export.py:206-226, forgelm/export.py:461-497, forgelm/verify/_gguf.py:17, forgelm/verify/_gguf.py:46-80
    Proposed correction. Fix the code: make forgelm export write <output>.sha256 next to every GGUF it produces (the design verify/_gguf.py:59-62 describes), or have verify-gguf compare against model_integrity.json exported_artifacts (sha256 + size_bytes). Until then, a temporary doc warning: "forgelm verify-gguf validates the GGUF magic header, parses the metadata…

  • DOC-D3B01-012 (docs, High) — docs/usermanuals/en/deployment/verify-gguf.md: false claim — The exporter writes .gguf.sha256 by default
    severity High · already tracked: KNOWN-OPEN TRUST-04 docs/roadmap/phase-16-trust-surface-hardening.md:418-424
    Where. docs/usermanuals/en/deployment/verify-gguf.md:103; gguf-export.md:19,22 ; docs/reference/verify_gguf_subcommand.md:46 · type: false claim
    The document says. The exporter writes .gguf.sha256 by default
    What the code does. export_model never writes a sidecar; it only appends to model_integrity.json.
    Code references. forgelm/export.py:207-239, forgelm/export.py:373-496
    Proposed correction. Implement sidecar writing (preferred, see F2) or correct the docs.
    Turkish mirror. the Turkish page has the same error; fix both in the same PR

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.
  • The release note names the contract this fix restores.

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.

Activity

  1. added
    bugSomething isn't working
    documentationImprovements or additions to documentation
    backlog: knownAlready tracked in docs/roadmap
    securitySecurity-relevant
    severity: criticalBreaks a stated security or safety contract
    source: review-2026-10Found by the October 2026 code and documentation review
    wave: 0Roadmap wave 0: immediate (patch release)
    on Oct 9, 2026
  2. added
    blockedWaits for another issue (see Blocked by); set and removed automatically, never by hand
    on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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 workingdocumentationImprovements or additions to documentationsecuritySecurity-relevantseverity: criticalBreaks a stated security or safety contractsource: review-2026-10Found by the October 2026 code and documentation reviewwave: 0Roadmap wave 0: immediate (patch release)

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions