Repository navigation
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
Description
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.
forgelm/verify/_gguf.py:59-62(docstring claim), 103-105 (sidecar-only compare), 175-181 (final valid=True)forgelm/verify/_gguf.py:103-151(confirmed)forgelm/verify/_gguf.py:104-105forgelm/export.py:207-231(_update_integrity_manifest writes only exported_artifacts), 461 (digest), 493-494forgelm/verify/_model_integrity.py:103-111(reads only 'artifacts')docs/usermanuals/en/deployment/gguf-export.md:73-79forgelm/verify/_gguf.py:61-62forgelm/export.py:460-496(export_model's tail ends at 496, not 497)docs/reference/verify_gguf_subcommand.md:46,165docs/usermanuals/en/deployment/gguf-export.md:19-22and 73-79
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>.sha256sidecar 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>.sha256sidecar 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: haveforgelm exportwrite<output>.sha256(sha256sum format). Until then: 'forgelm export does not write a sidecar; create one withsha256sum 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>.sha256file 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 exportdoes not write a sidecar in this version. Create one withsha256sum model.gguf > model.gguf.sha256(or from thesha256field offorgelm 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
.gguffile plus a.sha256manifest' (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.gguffile plus a.sha256manifest' (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>.sha256sidecar next to the GGUF it produced, so verify-gguf's documented check engages. Until then add a warning: "forgelm exportdoes not write a.sha256sidecar. Create one yourself (sha256sum model.gguf > model.gguf.sha256) — without itforgelm verify-ggufchecks 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-ggufre-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-ggufre-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>.sha256sidecar. verify-gguf checks only the magic header, the metadata parse and an optional<path>.sha256sidecar (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: makeforgelm exportwrite<output>.sha256next 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-ggufvalidates 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.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
docs(gguf): say how the intermediate file is named and that the manif…
feat(export): write the SHA-256 sidecar verify-gguf relies on, and do…